gh-92041: Avoid module scans for frames and tracebacks - #92042
Conversation
|
Every change to Python requires a NEWS entry. Please, add it using the blurb_it Web app or the blurb command-line tool. |
|
Every change to Python requires a NEWS entry. Please, add it using the blurb_it Web app or the blurb command-line tool. |
eendebakpt
left a comment
There was a problem hiding this comment.
Looks good. Is the new case also covered by tests?
|
The recursion-break test suggested code objects should be distinguished by object instance rather than by their hash alone. I'm using id & hash together as best effort to uniquely identify code objects that have no module. I assume identity comparison with weakref would be the ideal solution, but the performance is O(N) rather than O(1), with N = len(sys.modules). The identity approximation seemed a reasonable trade-off to me. |
9bbf165 to
725a326
Compare
|
Rebased past some bad upstream that broke CI. You can view the weakref implementation I tested here.. Also worth noting, the tests I've shown were run by pasting into interactive console. With same test code in a module that is executed, it will run faster (though still slower than with this fix) -- |
|
Also it seems as though the CI has stalled since last update. Not sure if I need to make some random change and force a re-run, or if it's not running because I'm a "first-time contributor" -- the description is a bit vague. 4 expected checks, 2 workflows awaiting approval.. not sure what a 'workflow' vs a 'check' is. |
|
Reminder, columns are stack depth, rows are len(sys.modules), numbers are milliseconds. Without changes: With this PR changes: Also testing with weakref version: Interesting! With this PR changes: With weakref version: I will take a closer look at what else |
|
Also the With this PR changes: With weakref impl: At higher module counts, either implementation uses less microseconds than the current implementation in milliseconds.. |
|
The docs indicate weakref doesn't work with code objects. They appear to work with simple tests in interactive console. However, debugging the weakref implementation I linked earlier, the _moduleless cache fills with dead weakrefs indexed by the same code object id(). The current use of id ^ hash seems optimal. I have previously explored defining _moduleless as an LRU cache. When testing I got the sense |
|
Hmm, looks like I need a maintainer to approve running tests. I can't seem to trigger them despite pressing all the buttons. |
@mdeck could you merge/rebase to current main? Perhaps that will trigger the tests to build |
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
|
Here, sys._getframe() is moduleless, and is cached:
# test3.py
import gc
import inspect
import random
import time
import sys
def run_test(module):
def print_line(h, *vals):
if not h:
print("%15s" % h, *["%9i" % v for v in vals], " <- stack depth")
else:
print("%15s" % h, *["%6.0f us" % v for v in vals])
#
len_sys_modules = [100, 1_000, 10_000]
def add_modules(n):
while len(sys.modules) < n:
sys.modules[f"foo_{random.randint(0,2**64)}"] = module
#
depths = [2, 8, 64]
def nest(depth):
if depth > 0:
return nest(depth-1)
t = time.time()
_ = inspect.stack()
dur = time.time() - t
return dur * 1000 * 1000
#
gc.collect()
if hasattr(inspect, "_moduleless"):
print(f"len moduleless: {len(inspect._moduleless)}")
inspect._moduleless.clear()
#
orig_modules = sys.modules.copy()
print_line("", *depths)
for len_modules in len_sys_modules:
add_modules(n=len_modules)
times = [nest(depth) for depth in depths]
print_line(len(sys.modules), *times)
sys.modules = orig_modules.copy()
print("len sys.modules")
run_test(random)
run_test(sys)
import resource
print(resource.getrusage(resource.RUSAGE_SELF)) |
|
This PR is stale because it has been open for 30 days with no activity. |
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
|
This PR has been substantially reworked to use an O(1) frame-globals lookup without a persistent cache. The expanded semantic and regression coverage and full CPython CI are now green. @berkerpeksag @lysnikolaou, this should be ready for a fresh review when you have a chance. |

Summary
inspect.getmodule()currently falls back to scanning every entry insys.moduleswhen a frame or traceback cannot be resolved through thefilename cache. Repeated inspection of frames executing in an unregistered
namespace repeats that linear scan.
Resolve frame and traceback objects directly from the frame's globals:
__name__;sys.modules;origins to match, including normalized and real-path matches.
This is an O(1) lookup and requires no persistent negative cache or cache
invalidation. Functions, methods, classes, modules, and bare code objects keep
their existing lookup paths. Fileless registered frames keep returning
None.An explicit private
_filenameoverride naming a different file also retainsthe existing filename-based behavior.
Behavior clarification
A frame executing in globals that do not belong to a registered module now
returns
None. Previously it could be associated with an unrelated modulesolely because the two used the same filename.
Performance
Illustrative
timeitmeasurements on CPython 3.12.13, Linux x86-64, with10,000 synthetic entries added to
sys.modules:getmodule()on an unregistered frameinspect.stack()containing that frameThe function-path result verifies that the new type handling does not penalize
objects already handled through
__module__.Tests
Coverage includes:
None, non-module, and mismatchedsys.modulesentries;getabsfile()failure handling;