Skip to content

perf(resolution): memoize path resolution in the symbol-resolution passes (47% faster extract) - #3500

Open
abhay-codes07 wants to merge 2 commits into
Graphify-Labs:v8from
abhay-codes07:perf/resolution-path-memoization
Open

perf(resolution): memoize path resolution in the symbol-resolution passes (47% faster extract)#3500
abhay-codes07 wants to merge 2 commits into
Graphify-Labs:v8from
abhay-codes07:perf/resolution-path-memoization

Conversation

@abhay-codes07

Copy link
Copy Markdown
Contributor

What

Memoize path resolution across the symbol-resolution and id-disambiguation passes. Two commits, one theme:

  1. _source_key_disambiguate_colliding_node_ids computes a source key once per node, per edge endpoint and per raw_call, each call running Path.resolve(). Tens of thousands of calls over a few hundred distinct source_file strings.
  2. 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() walks nt._getfinalpathname / readlink every time on an uncached call; the same corpus paths were re-resolved thousands of times within one run.

Why it's safe

  • Files do not move mid-extract, so a path resolves to the same target throughout a run.
  • cwd is part of the cache key, so a relative path resolved after a chdir misses the cache instead of replaying a stale answer (verified by test).
  • The helpers raise exactly like Path.resolve() — callers keep their own try/except, and an exception is never cached.
  • The graph is byte-identical before and after: I froze the 364-file self-corpus, dumped the sorted node-id and source|relation|target edge sets on v8 and on this branch, and they cmp equal.

Measured

Sequential extract of graphify's own 364-file Python corpus (best of 3, warm import, fresh cache dir each run):

best
v8 26.6s
this branch 14.0s

A 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.py and tests/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 fresh v8 (0.9.58) baseline.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q

abhay-codes07 and others added 2 commits September 11, 2026 23:09
… 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
Copilot AI lite review requested due to automatic review settings September 11, 2026 18:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 targetsgraphify/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 — impact
  • tests/test_build.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_extract.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_js_exported_scalar_bindings.py — impact
  • tests/test_languages.py — impact
  • tests/test_multilang.py — impact
  • tests/test_package_json_subpath_imports.py — impact
  • tests/test_pascal.py — impact
  • tests/test_pascal_resolution.py — impact
  • tests/test_phantom_external_import.py — impact
  • tests/test_python_import_resolution.py — impact
  • tests/test_python_underscore_resolution.py — impact
  • tests/test_rationale.py — impact
  • tests/test_resolve_memoization.py — impact, changed-test
  • tests/test_ruby_resolution.py — impact
  • tests/test_scala_self_type.py — impact
  • tests/test_source_key_memoization.py — impact, changed-test
  • tests/test_src_layout_import_resolution.py — impact
  • tests/test_swift_computed_properties.py — impact
  • tests/test_ts_new_expression_calls.py — impact
  • tests/test_typescript_module_extensions.py — impact
  • tests/test_unmapped_at_alias_resolution.py — impact
  • tests/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).

safishamsi added a commit that referenced this pull request Sep 12, 2026
… 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants