perf(resolution): memoize path resolution in the symbol-resolution passes (47% faster extract) - #3500
Conversation
…ion pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
… passes The symbol-resolution and cross-file import passes call Path.resolve() once per import/export/use fact and per node — ~44k calls over a few hundred distinct corpus paths, each walking nt._getfinalpathname on Windows. A per-(path, cwd) memo collapses that to one syscall per distinct path. On a 364-file self-corpus a sequential extract drops from ~27s to ~14s, graph byte-identical. The cwd is part of the key so a relative path resolved after a chdir misses rather than replaying a stale answer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. 1 change(s) tested, no difference found (not proven).
Graphify review — findings
Memoizes path resolution across the symbol-resolution passes: adds _resolve_cached (backed by an lru-cached _cached_realpath) and routes every Path.resolve() in config lookup, workspace/package discovery, and _apply_symbol_resolution_facts through it, and rewrites _source_key to delegate to an lru-cached _cached_source_key. Both caches key on os.getcwd() so a relative path resolved after a chdir misses rather than replaying a stale result, and exceptions are never cached so callers keep their own try/except. Cuts the ~15s (34% of a sequential 364-file extract) spent re-resolving a few hundred distinct paths tens of thousands of times.
Worth a look
- Process-wide resolved-path cache returns stale symlink targets —
graphify/extractors/resolution.py:1785· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1860 functions depend on the 156 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 585 callers, 44 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
_extract_generic()— 18 callers, 26 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
_resolve_js_module_path()— 34 callers, 9 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
extract_objc()— 27 callers, 9 callees - …and 35 more — each is listed as a finding
Verification — 1860 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 812 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
31 of 270 test file(s) selected (11%) via static blast radius.
tests/test_astro_extraction.py— impacttests/test_build.py— impacttests/test_cjs_module_extension.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_dotnet.py— impacttests/test_extract.py— impacttests/test_forwarding_review_findings.py— impacttests/test_import_extension_resolution.py— impacttests/test_indirect_dispatch.py— impacttests/test_indirect_dispatch_assign_return.py— impacttests/test_indirect_dispatch_getattr.py— impacttests/test_js_exported_scalar_bindings.py— impacttests/test_languages.py— impacttests/test_multilang.py— impacttests/test_package_json_subpath_imports.py— impacttests/test_pascal.py— impacttests/test_pascal_resolution.py— impacttests/test_phantom_external_import.py— impacttests/test_python_import_resolution.py— impacttests/test_python_underscore_resolution.py— impacttests/test_rationale.py— impacttests/test_resolve_memoization.py— impact, changed-testtests/test_ruby_resolution.py— impacttests/test_scala_self_type.py— impacttests/test_source_key_memoization.py— impact, changed-testtests/test_src_layout_import_resolution.py— impacttests/test_swift_computed_properties.py— impacttests/test_ts_new_expression_calls.py— impacttests/test_typescript_module_extensions.py— impacttests/test_unmapped_at_alias_resolution.py— impacttests/test_vue_extraction.py— impact
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify \_apply\_symbol\_resolution\_facts.
The verifier did not have enough to check \_apply\_symbol\_resolution\_facts, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_collect\_js\_symbol\_resolution\_facts.
The verifier did not have enough to check \_collect\_js\_symbol\_resolution\_facts, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `facts` is annotated `_SymbolResolutionFacts` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_collect\_python\_symbol\_resolution\_facts.
The verifier did not have enough to check \_collect\_python\_symbol\_resolution\_facts, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_contained\_in\_package.
The verifier did not have enough to check \_contained\_in\_package, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `resolved` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_find\_js\_config.
The verifier did not have enough to check \_find\_js\_config, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `start_dir` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_find\_js\_project\_anchor.
The verifier did not have enough to check \_find\_js\_project\_anchor, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `start_dir` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_find\_workspace\_root.
The verifier did not have enough to check \_find\_workspace\_root, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `start_dir` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_go\_import\_path\_for\_file.
The verifier did not have enough to check \_go\_import\_path\_for\_file, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `source_file` is annotated `str | Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_js\_source\_path.
The verifier did not have enough to check \_js\_source\_path, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_load\_package\_imports.
The verifier did not have enough to check \_load\_package\_imports, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `start_dir` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_read\_tsconfig\_aliases.
The verifier did not have enough to check \_read\_tsconfig\_aliases, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `tsconfig` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_resolve\_c\_include\_path (not a proof).
The verifier ran both versions of \_resolve\_c\_include\_path on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_resolve\_python\_namespace\_dir.
The verifier did not have enough to check \_resolve\_python\_namespace\_dir, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `current_path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_source\_key.
The verifier did not have enough to check \_source\_key, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
· 43 more finding(s) on lines outside this diff (see the check run).
… run The memoized path/source-key lru_caches are keyed by (path, cwd) with no mtime component; without a per-run clear they replay stale results across repeated extract() calls in a long-lived watch/MCP process. Wire them into the existing cache-clear block. (follow-up to #3500)
What
Memoize path resolution across the symbol-resolution and id-disambiguation passes. Two commits, one theme:
_source_key—_disambiguate_colliding_node_idscomputes a source key once per node, per edge endpoint and per raw_call, each call runningPath.resolve(). Tens of thousands of calls over a few hundred distinctsource_filestrings.Path.resolve()everywhere in the resolution passes — the symbol-resolution facts/apply passes and the cross-file import pass call.resolve()once per import/export/use fact and per node (~44k calls over the same few hundred paths).Both are wrapped in a per-
(path, cwd)lru_cache.resolve()walksnt._getfinalpathname/readlinkevery time on an uncached call; the same corpus paths were re-resolved thousands of times within one run.Why it's safe
chdirmisses the cache instead of replaying a stale answer (verified by test).Path.resolve()— callers keep their owntry/except, and an exception is never cached.source|relation|targetedge sets onv8and on this branch, and theycmpequal.Measured
Sequential extract of graphify's own 364-file Python corpus (best of 3, warm import, fresh cache dir each run):
v8A 47% reduction, entirely in
_disambiguate_colliding_node_ids(34% of the old run) and the resolution passes. No behavior change.Tests
tests/test_source_key_memoization.pyandtests/test_resolve_memoization.py: equality with the pre-memo implementation across relative / absolute / out-of-root / missing / empty inputs, one resolution per distinct path, and cwd-sensitivity. Full suite matches a freshv8(0.9.58) baseline.🤖 Generated with Claude Code
https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q