perf(cache): speed up the warm-load re-anchor of cached AST entries - #3659
abhay-codes07 wants to merge 1 commit into
Conversation
Every warm cache hit re-anchors the stored portable form back to the current root before returning the entry, and two passes over the payload dominated that step on graphify's own corpus: - `_absolutize_source_files_in` rebuilt `Path(source)` and joined it to the resolved root once per node/edge, but every symbol in a file repeats that file's `source_file` — a few hundred distinct values across tens of thousands of items. Memoize the absolutized form per distinct source string so the Path build + join runs once per file, not once per symbol. - `_rewrite_strings` (the `$graphify-root$` id/path re-anchor walk) recursed one frame per container and copied every level with `list(items)`. Rewrite it as an iterative explicit-stack walk — the same change already made to `_walk_python_tree` — keeping the exact contract (fn hits string VALUES only, never dict keys; in-place value mutation during iteration is safe since the container size never changes). Both are pure restructurings: the loaded entry is byte-identical. Verified that a warm extract (which loads the cache) produces byte-identical nodes (id, source_file, origin_file, label) and edges to the cold run that wrote it. On the 364-file corpus the two passes together drop from ~9.8% to ~3.7% of warm extract time (`_absolutize_source_files_in` ~4.4x, the walk ~1.8x). 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.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. 1 change(s) tested, no difference found (not proven).
Graphify review — findings
Speeds up warm cache loads by rewriting _rewrite_strings as an iterative stack walk instead of recursion, dropping the per-level list() copy while keeping the same contract (strings values only, never dict keys, in-place mutation). Adds a per-string memo to _absolutize_source_files_in so each distinct source_file value is absolutized once rather than once per symbol, with None marking values left as-is; non-string values still fall back to the original best-effort coercion without polluting the memo.
No blocking issues surfaced. 4 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1654 functions depend on the 84 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 645 callers, 45 callees - new:
_rebuild_code()— 129 callers, 54 callees - new:
detect()— 112 callers, 15 callees - new:
save_semantic_cache()— 63 callers, 9 callees - new:
load_cached()— 53 callers, 7 callees - new:
file_hash()— 51 callers, 6 callees - new:
extract_corpus_parallel()— 26 callers, 11 callees - new:
dispatch_command()— 2 callers, 125 callees - …and 20 more — each is listed as a finding
Verification — 1654 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: 900 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
23 of 286 test file(s) selected (8%) via static blast radius.
tests/test_cache.py— impacttests/test_charmap_encoding.py— impacttests/test_chunking.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_extract.py— impacttests/test_extract_cache_location.py— impacttests/test_extract_cli.py— impacttests/test_ignore_file_encoding.py— impacttests/test_incremental_mtime_collision.py— impacttests/test_llm_backends.py— impacttests/test_objc_field_table_remap.py— impacttests/test_out_dir_evidence.py— impacttests/test_partial_cache.py— impacttests/test_pipeline.py— impacttests/test_semantic_cache_basename_recovery.py— impacttests/test_semantic_cache_out_root.py— impacttests/test_stale_prune.py— impacttests/test_stat_index_husk.py— impacttests/test_stat_index_portability.py— impacttests/test_watch_manifest_location.py— impacttests/test_word_count_cache.py— impacttests/test_zero_node_no_cache.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 \_absolutize\_source\_files\_in.
The verifier did not have enough to check \_absolutize\_source\_files\_in, 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: no capturable inputs from the test suite; property tier: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_rewrite\_strings (not a proof).
The verifier ran both versions of \_rewrite\_strings 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.
· 28 more finding(s) on lines outside this diff (see the check run).
What
Every warm cache hit re-anchors the stored portable entry (relative
source_filefields,$graphify-root$-marked ids/paths) back to the current root before returning it. Two passes over the payload dominated that step, and both were doing avoidable work:_absolutize_source_files_inrebuiltPath(source)and joined it to the resolved root once per node/edge. But every symbol in a file repeats that file'ssource_file— a few hundred distinct values across tens of thousands of items. Now the absolutized form is memoized per distinct source string, so thePathbuild + join runs once per file instead of once per symbol._rewrite_strings(the$graphify-root$id/path re-anchor walk) recursed one frame per container and made a defensivelist(items)copy of every level. It's now an iterative explicit-stack walk — the same change already applied to_walk_python_tree— keeping the exact contract:fnrewrites string values only, never dict keys, and mutating a value in place during iteration is safe because the container's size never changes.Why it's safe
Both are pure restructurings — the re-anchored entry is byte-identical:
source_filestring and returns the samestr(root_resolved / Path(source))the per-item path produced;Nonecaches the "leave as-is" cases (already-absolute or uncoercible). Non-string (malformed-producer) values keep the original best-effort coercion rather than key the memo.fnto exactly the same set of string values as the recursion;fnis applied to each independently, so the visit-order change (LIFO stack) is irrelevant to the result.Correctness
A warm extract (which loads the cache) produces byte-identical nodes —
id,source_file,origin_file,label— and edges to the cold run that wrote the entries. The cache and portability suites pass (test_cache,test_partial_cache,test_incremental,test_extract_cache_location,test_definition_file_portability,test_stat_index_portability,test_semantic_cache_*,test_zero_node_no_cache,test_word_count_cache— 159 tests).Measured
On graphify's own 364-file corpus, the two passes together drop from ~9.8% to ~3.7% of warm-extract time —
_absolutize_source_files_in~4.4× faster (per-file memo), the re-anchor walk ~1.8× faster (iterative). This is on the hot path of every warmgraphify update.🤖 Generated with Claude Code
https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q