Skip to content

perf(cache): speed up the warm-load re-anchor of cached AST entries - #3659

Open
abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:perf/cache-load-reanchor
Open

abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:perf/cache-load-reanchor

Conversation

@abhay-codes07

Copy link
Copy Markdown
Contributor

What

Every warm cache hit re-anchors the stored portable entry (relative source_file fields, $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_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. Now the absolutized form is memoized per distinct source string, so the Path build + 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 defensive list(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: fn rewrites 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:

  • The memo is keyed on the exact source_file string and returns the same str(root_resolved / Path(source)) the per-item path produced; None caches 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.
  • The iterative walk applies fn to exactly the same set of string values as the recursion; fn is 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 warm graphify update.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q

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
Copilot AI lite review requested due to automatic review settings September 18, 2026 11:40

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.

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 — impact
  • tests/test_charmap_encoding.py — impact
  • tests/test_chunking.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_extract_cli.py — impact
  • tests/test_ignore_file_encoding.py — impact
  • tests/test_incremental_mtime_collision.py — impact
  • tests/test_llm_backends.py — impact
  • tests/test_objc_field_table_remap.py — impact
  • tests/test_out_dir_evidence.py — impact
  • tests/test_partial_cache.py — impact
  • tests/test_pipeline.py — impact
  • tests/test_semantic_cache_basename_recovery.py — impact
  • tests/test_semantic_cache_out_root.py — impact
  • tests/test_stale_prune.py — impact
  • tests/test_stat_index_husk.py — impact
  • tests/test_stat_index_portability.py — impact
  • tests/test_watch_manifest_location.py — impact
  • tests/test_word_count_cache.py — impact
  • tests/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).

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