perf: repo_map edge cache is globally invalidated by any file reindex #20

Closed
opened 2026-07-07 13:49:51 +02:00 by buildagent · 1 comment
Member

Summary

The repo_map edge cache key includes MAX(files.indexed_at), so saving one file busts the entire cache. The next repo_map call then recomputes the whole enclosing-symbol graph from scratch — one correlated subquery per resolved ref — even though only one file's edges changed. On an actively-watched repo the cache effectively never hits during development.

Where

  • crates/daemon/src/local_index.rs:302 (compute_edges, O(resolved_refs) correlated subqueries)
  • crates/daemon/src/local_index.rs:333-355 (edges() cache key = (MAX(indexed_at), resolved_count, target_sum); full-key equality → global miss)
  • crates/indexer/src/writer.rs:294 writes indexed_at = now on every reindexed file, so any real edit advances the key.

Severity

Medium. The recompute is lazy (only on the next repo_map after an edit), repo_map is a user-invoked orientation tool (not on the write/watch hot path), and the per-ref probe uses idx_symbols_file_span (m0009). But the rebuild holds the connection checkout and the cache is defeated for any repo_map during active editing.

Fix

Make edge computation incremental: key/store edges per file_id and recompute only changed files' edge rows on reindex (the watcher already knows which files changed), or persist an enclosing-symbol edge table maintained by the indexer so repo_map reads it instead of recomputing. Likely needs a new migration (m0012+) for the edge table (m0001–m0011 frozen).

Surfaced by the v0.5.8 ultradeep review (performance dimension, CONFIRMED).

Acceptance

  • Editing one file does not force a full graph rebuild on the next repo_map; only the changed file's edges recompute.
  • repo_map ranking output unchanged; repo_map_tests (incl. edge_cache_invalidates_on_resolution_change) stay green.
## Summary The `repo_map` edge cache key includes `MAX(files.indexed_at)`, so saving **one** file busts the entire cache. The next `repo_map` call then recomputes the whole enclosing-symbol graph from scratch — one correlated subquery per resolved ref — even though only one file's edges changed. On an actively-watched repo the cache effectively never hits during development. ## Where - `crates/daemon/src/local_index.rs:302` (`compute_edges`, O(resolved_refs) correlated subqueries) - `crates/daemon/src/local_index.rs:333-355` (`edges()` cache key = `(MAX(indexed_at), resolved_count, target_sum)`; full-key equality → global miss) - `crates/indexer/src/writer.rs:294` writes `indexed_at = now` on every reindexed file, so any real edit advances the key. ## Severity Medium. The recompute is lazy (only on the next `repo_map` after an edit), `repo_map` is a user-invoked orientation tool (not on the write/watch hot path), and the per-ref probe uses `idx_symbols_file_span` (m0009). But the rebuild holds the connection checkout and the cache is defeated for any `repo_map` during active editing. ## Fix Make edge computation incremental: key/store edges per `file_id` and recompute only changed files' edge rows on reindex (the watcher already knows which files changed), or persist an enclosing-symbol edge table maintained by the indexer so `repo_map` reads it instead of recomputing. Likely needs a **new migration (m0012+)** for the edge table (m0001–m0011 frozen). Surfaced by the v0.5.8 ultradeep review (performance dimension, CONFIRMED). ## Acceptance - Editing one file does not force a full graph rebuild on the next `repo_map`; only the changed file's edges recompute. - `repo_map` ranking output unchanged; `repo_map_tests` (incl. `edge_cache_invalidates_on_resolution_change`) stay green.
buildagent added this to the v0.5.9 milestone 2026-07-07 13:50:17 +02:00
Author
Member

Implemented on fix/ultradeep-review-findings (commit 0d79e7c), via fix option 2 (persistent edge table).

The in-memory edge cache keyed on MAX(files.indexed_at) — so any file save busted it and the next repo_map recomputed the whole enclosing-symbol graph — is gone. A persistent symbol_edges table (migration m0013) is maintained by the indexer in rebuild_symbol_edges, called in the same transaction as resolution (alongside the #19 ref_count refresh, gated behind resolution actually running). repo_map reads the table directly: no per-call recompute, no cache to invalidate, always current.

On the incremental option (1): I deliberately chose full-rebuild-on-resolve over per-file incremental. An edge's from_id (enclosing symbol) changes when any file targeting it is reindexed and re-resolved to a new symbol id, so per-file invalidation would miss cross-file edges. The rebuild is one indexed pass over refs, comparable to resolution's own cost and only paid when refs changed — while making repo_map reads unconditionally cheap.

The edge SQL lives once in index::SYMBOL_EDGES_SELECT (shared by the runtime rebuild; mirrored by the m0013 backfill so a freshly-upgraded DB has a graph immediately). CURRENT_VERSION → 13.

Acceptance: editing a file no longer forces a read-time graph rebuild; repo_map ranking output unchanged (repo_map_tests green, now populating symbol_edges via a test mirror of the rebuild). Tests: m0013_backfills_symbol_edges_from_resolved_refs (self-edge dropped), repo_map_reflects_rebuilt_edges, existing e2e_repo_map_rpc proves end-to-end maintenance. Closing.

Implemented on `fix/ultradeep-review-findings` (commit `0d79e7c`), via **fix option 2** (persistent edge table). The in-memory edge cache keyed on `MAX(files.indexed_at)` — so any file save busted it and the next `repo_map` recomputed the whole enclosing-symbol graph — is gone. A persistent `symbol_edges` table (migration **m0013**) is maintained by the indexer in `rebuild_symbol_edges`, called in the **same transaction** as resolution (alongside the #19 ref_count refresh, gated behind resolution actually running). `repo_map` reads the table directly: no per-call recompute, no cache to invalidate, always current. **On the incremental option (1):** I deliberately chose full-rebuild-on-resolve over per-file incremental. An edge's `from_id` (enclosing symbol) changes when *any* file targeting it is reindexed and re-resolved to a new symbol id, so per-file invalidation would miss cross-file edges. The rebuild is one indexed pass over `refs`, comparable to resolution's own cost and only paid when refs changed — while making `repo_map` reads unconditionally cheap. The edge SQL lives once in `index::SYMBOL_EDGES_SELECT` (shared by the runtime rebuild; mirrored by the m0013 backfill so a freshly-upgraded DB has a graph immediately). `CURRENT_VERSION → 13`. **Acceptance:** editing a file no longer forces a read-time graph rebuild; `repo_map` ranking output unchanged (`repo_map_tests` green, now populating `symbol_edges` via a test mirror of the rebuild). Tests: `m0013_backfills_symbol_edges_from_resolved_refs` (self-edge dropped), `repo_map_reflects_rebuilt_edges`, existing `e2e_repo_map_rpc` proves end-to-end maintenance. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#20
No description provided.