Skip to content

perf(code-index): stop the readiness waits competing with the rebuild - #1580

Draft
ScriptedAlchemy wants to merge 6 commits into
masterfrom
cursor/pace-code-index-readiness-waits-b707
Draft

ScriptedAlchemy wants to merge 6 commits into
masterfrom
cursor/pace-code-index-readiness-waits-b707

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Investigation of the graph-rebuild receipt's 90s timeout, focused on the status-polling wait. Stacked on cursor/seat-through-retryable-activation-f5c7 (#1562); reviewing master... will show that branch's commits too.

The 90s timeout was not waiting on anything

Reproduced locally (perf profile, 4 cores). At the moment the deadline fired:

status field value
code_index_freshness.status current
coverage / staleness_state complete / fresh
source_reference / source_revision refs/heads/main / the expected refreshed revision
latest_generation_id generation.v1.51e0facb.00000002.3c00fba0…
code_index_freshness_warning absent
retrieval_serving.seated_generation_age_seconds 89

Every conjunct on the status side held, on the right revision, for the whole 90 seconds. The search response carried that same code_generation — inside preview, because limit: 3 rendered 18 084 characters against the 15 000-character response frame (tracedecay-mcp-catalog), so MCP replaced the body with a retrieval handle:

["handle","original_chars","preview","preview_chars","retrieve_…","truncated"]

results and code_generation are absent at the top level. The predicate read them as missing, could not distinguish that from a warming generation, and spun to its deadline. seated_generation_age_seconds ≈ 89 is exactly that: the seat was installed one second in and never moved — the seat fix in #1562 worked, and the receipt could not see it.

What blows the frame is ~5 KiB of ranking provenance per candidate, not result bodies, so limit: 20 → 3 in 7bdc33d was a guess at how many candidates fit and landed short.

Changes

  1. test(tracedecay)daemon_suite/git_watch_test.rs had already hit this wall and solved it properly, by paging the stored original back through tracedecay_retrieve. That resolver moves to the shared tests/common/ surface and both suites read every tool answer through it, so page size stops being load-bearing. Both suites' duplicate tool / tool_payload helpers go with it (−113/+16 lines).
  2. test(transport) — both waits paced at 25 ms instead of yield_now. The spin re-entered tracedecay_status thousands of times a second, and each call runs the generation census ready-probe, a scheduler freshness read, and branch diagnostics — 1–3 Git opens, 2 blocking-pool offloads, 2–6 registry mutex acquisitions — on the runtime running the reconcile it waits for.
  3. perf(code-index)GitMetadataFingerprintV1::capture re-derived the git-dir and common-dir through a fresh gix::open on every sample, against a doc comment calling that cost fixed and cheap. runtime-core already owns a revalidating topology memo for the same question. Capture 75.2 µs → 10.0 µs; this is the tier-1 ladder on every query admission, and search runs 2–3 captures per call.
  4. perf(daemon) — the production composition's publication wait re-ran that probe every 10 ms for up to 20 s, leaving a pending arrival the worker yields its graph prepare to. The registry already publishes the edge it wants (subscribe_serving_generation_changes), so the wait parks on it, with a 100 ms floor for the terminal answers that install no seat.

Measured

graph_rebuild_status_test::background_refresh_and_reopen_report_only_servable_generations, perf profile, 4 cores:

outcome fails at
baseline 93.4 s FAIL status-polling deadline, line 176
+ untruncated read 101.6 s FAIL harness reopen budget, line 296
+ paced / event-driven waits 95.9 s FAIL harness reopen budget, line 296
same, harness budget scaffolded wide 164.7 s → 155.3 s PASS

Fingerprint, one-ref fixture, 2000 warm iterations: capture 75.2 µs → 10.0 µs; gix::open 72.7 µs; topology memo 2.0 µs.

Publication waits with the event-driven wait: 0.45 s initial, 50.1 s reopen (from 55.1 s), 14.5 s unchanged reopen (from 15.4 s).

No timeout was raised. The 90 s RECEIPT_TIMEOUT and the harness's 20 s budget are untouched; the wide budget above was investigation scaffolding and is not in the diff.

What this does not fix

With the waits corrected, the journey's next binding constraint is the publication cost itself, and it is not a polling problem:

  • 50 s to republish after adding one file to a 769-file / 98 304-symbol tree, against a 20 s budget. State at timeout: Indexing / PartialRefreshInProgress, clone_index: "the sealed lexical artifact is still building" — progressing, just whole-corpus.
  • 14.5 s for a completely unchanged reopen, where recovery exists specifically to avoid replaying what already serves. Close enough to 20 s to be a flake source on a loaded runner.

That belongs to the rebuild-cost lane and the fix is not a wider budget. Until it lands this test still fails — but at a truthful, self-describing point rather than a 90 s deadline on an already-current generation.

Also found, not changed here

  • The same status-then-search spin exists in daemon_suite/git_watch_test.rs:238 and mcp_suite/.../graph_readiness.rs:42; both compare code_generation across two separate tool calls.
  • tracedecay_status computes the generation census unconditionally, including for admission_only, and build_branch_diagnostics resolves the live branch through a Git open plus a branch-meta read and several path stats on every call — none of it gated by the include_* flags.
  • serving_generation_after_activation_failure (added in b80dd58, fix(code-index): seat text through retryable graph activation #1562) is dead outside cfg(test) and warns on every build.

Verification

  • daemon_suite::git_watch_test — 5 passed, confirming the shared resolver on the suite that owned it.
  • code_index_scheduler::identity::tests — 5 passed, including both fingerprint change-detection tests and the in-place loose-ref rewrite.
  • code_index_scheduler::* — 8 failures, all pre-existing on fix(code-index): seat text through retryable graph activation #1562: 7 of the 8 fail identically with this branch's identity.rs reverted to b80dd58, and the eighth (concurrent_query_admissions_claim_one_pending_wake_before_worker_coalescing) is flaky on base at 3/8 versus 1/5 here.
  • cargo fmt, cargo check --tests, commitlint clean.
Open in Web Open in Cursor 

cursoragent and others added 4 commits September 17, 2026 23:11
A retryable graph activation used to erase the prepared serving
candidate, and an unfinished clone-fingerprint successor withheld the
same seat after exact and lexical owners were ready. Keep the candidate
in both cases so search can move off the predecessor while graph retries.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
The graph-rebuild receipt timed out for 90s against a generation that was
already current: status reported `current` on the expected revision with
the advertised generation matching what search served, and the seat never
moved for the whole wait.

`limit: 3` still rendered 18084 characters against the 15000-character
response frame, so MCP replaced the body with a retrieval handle and moved
`results` and `code_generation` inside `preview`. Every predicate read them
as absent and the wait spun to its deadline. One candidate carries several
KiB of ranking provenance, so the page has to be smaller than a guess at
how many results fit.

Ask for one result, and refuse a truncated envelope outright rather than
reading it as a warming generation, so the next frame overflow reports
itself instead of presenting as a deadline. Pace both waits as well: each
`tracedecay_status` call runs the census ready-probe, a freshness read, and
branch diagnostics, and a `yield_now` spin re-entered that path thousands of
times a second on the runtime running the reconcile it waits for.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
`GitMetadataFingerprintV1::capture` is the tier-1 staleness signal sampled
on every query admission, and its own contract calls that cost fixed and
cheap. It was neither: resolving the git-dir and common-dir through a fresh
`gix::open` cost 72.7us of the 75.2us per capture, and runtime-core already
owns a revalidating topology memo that answers the same question in 2.0us.
Search runs two to three captures per call.

Measured on a one-ref fixture repository, perf profile, 2000 warm
iterations: capture 75.2us -> 10.0us.

The memo is asked only for a checkout carrying `<root>/.git`, which is both
where an open at exactly this root resolves through and where a discovery
started at this root stops, so it returns the same two paths. A bare
control directory or a path that is not a checkout root still opens
directly, because discovery would walk past it to an ancestor whose git
metadata does not describe this project. The memo canonicalizes both paths;
the fingerprint samples file metadata and contents, so its value and its
persisted signature are unchanged.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
The production composition's publication wait re-ran its readiness probe
every 10ms for up to 20 seconds. That probe canonicalizes the root, takes
the scheduler registry's mounted mutex several times, offloads a
Git-metadata freshness capture to the blocking pool, and emits a decline
event — spent on the same cores as the reconcile it waits for, and leaving
a pending arrival the worker yields its graph prepare to.

The registry already publishes the edge this wants: the serving watch
signals every seat install and every source revalidation that keeps an
unchanged generation seated. Drive the wait from it, keeping a 100ms floor
for the terminal answers that install no seat — a route that has not
mounted yet, and a verified source that publishes no generation at all.

Measured on the graph-rebuild transport journey, perf profile: publication
waits 55.1s -> 50.1s and 15.4s -> 14.5s, suite 164.7s -> 155.3s.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: de836c1

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Two suites reached the same wall — an MCP answer over the response frame
arrives as `{"truncated": true, "handle": …, "preview": …}`, where `preview`
is a string and `results` / `code_generation` are absent at the top level —
and only one of them handled it. The graph-rebuild receipt read that
envelope as a warming generation and spent its whole 90s deadline on a
generation that was already current; `limit: 20 -> 3` was a guess at how
many candidates fit, and 3 still rendered 18084 characters against 15000.

Lift the daemon suite's retrieve-paging resolver into the shared test
surface and read every tool answer through it, so the page size stops being
load-bearing: it is a property of how much ranking provenance a candidate
carries, not something a journey should track. Both suites' duplicate
`tool` / `tool_payload` helpers go with it.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Performance Comparison mastercursor/pace-code-index-readiness-waits-b707

Total Elapsed Time: 6.82s → 7.17s (+5.2%)
CPU Baseline: 69.99µs → 69.79µs (-0.3%)
Benchmark ID: index-bench-timing

timing - Execution duration of functions.

+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| Function                                 | Calls                      | Avg                              | P95                              | Total                            | % Total                      |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| tracedecay-index-bench                   | 1 → 1 (+0.0%)              | 6.82s → 7.17s (+5.1%)            | 6.82s → 7.18s (+5.3%)            | 6.82s → 7.17s (+5.1%)            | 100.00% → 100.00% (+0.0%)    |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| query.artifact.batch.sqlite              | 5 → 5 (+0.0%)              | 380.53ms → 394.76ms (+3.7%)      | 425.72ms → 457.70ms (+7.5%)      | 1.90s → 1.97s (+3.7%)            | 27.89% → 27.51% (-1.4%)      |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| code_index.workers.install               | 30 → 30 (+0.0%)            | 57.46ms → 64.23ms (+11.8%)       | 158.86ms → 206.31ms (+29.9%) ⚠️  | 1.72s → 1.93s (+12.2%)           | 25.26% → 26.86% (+6.3%)      |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| domain.canonical.sha256                  | 72891 → 72891 (+0.0%)      | 24.73µs → 25.37µs (+2.6%)        | 80.25µs → 80.19µs (-0.1%)        | 1.80s → 1.85s (+2.8%)            | 26.42% → 25.77% (-2.5%)      |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| query.artifact.finalization.advance_wake | 28 → 28 (+0.0%)            | 46.74ms → 51.61ms (+10.4%)       | 362.28ms → 386.66ms (+6.7%)      | 1.31s → 1.45s (+10.7%)           | 19.18% → 20.14% (+5.0%)      |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| query.artifact.batch.postings            | 5 → 5 (+0.0%)              | 224.73ms → 228.57ms (+1.7%)      | 252.58ms → 257.43ms (+1.9%)      | 1.12s → 1.14s (+1.8%)            | 16.47% → 15.93% (-3.3%)      |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| code_index.extract.parser_artifact       | 276 → 276 (+0.0%)          | 3.01ms → 3.27ms (+8.6%)          | 4.88ms → 5.11ms (+4.7%)          | 829.46ms → 902.93ms (+8.9%)      | 12.16% → 12.59% (+3.5%)      |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| code_index.build.and_publish             | 2 → 2 (+0.0%)              | 415.90ms → 437.43ms (+5.2%)      | 767.03ms → 808.98ms (+5.5%)      | 831.81ms → 874.85ms (+5.2%)      | 12.19% → 12.19% (+0.0%)      |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| query.artifact.batch.parallel_prepare    | 5 → 5 (+0.0%)              | 142.30ms → 172.55ms (+21.3%) ⚠️  | 158.99ms → 206.31ms (+29.8%) ⚠️  | 711.51ms → 862.74ms (+21.3%) ⚠️  | 10.43% → 12.02% (+15.2%)     |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| domain.canonical.json_bytes              | 206681 → 206681 (+0.0%)    | 3.13µs → 3.74µs (+19.5%)         | 3.33µs → 3.35µs (+0.6%)          | 647.10ms → 773.94ms (+19.6%)     | 9.48% → 10.79% (+13.8%)      |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| query.artifact.batch.postings.ngram_rows | 5 → 5 (+0.0%)              | 127.69ms → 130.68ms (+2.3%)      | 142.61ms → 147.32ms (+3.3%)      | 638.43ms → 653.38ms (+2.3%)      | 9.36% → 9.11% (-2.7%)        |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| code_index.restore.file_admit            | 260 → 260 (+0.0%)          | 2.35ms → 2.36ms (+0.4%)          | 3.86ms → 3.99ms (+3.4%)          | 611.25ms → 614.65ms (+0.6%)      | 8.96% → 8.57% (-4.4%)        |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| code_index.build.materialize_full        | 1 → 1 (+0.0%)              | 519.45ms → 562.05ms (+8.2%)      | 519.57ms → 562.56ms (+8.3%)      | 519.45ms → 562.05ms (+8.2%)      | 7.61% → 7.83% (+2.9%)        |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| code_index.collect.materialize_full      | 1 → 1 (+0.0%)              | 513.66ms → 556.40ms (+8.3%)      | 513.80ms → 556.79ms (+8.4%)      | 513.66ms → 556.40ms (+8.3%)      | 7.53% → 7.76% (+3.1%)        |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+
| query.artifact.batch.commit              | 5 → 5 (+0.0%)              | 88.55ms → 98.72ms (+11.5%)       | 103.02ms → 139.98ms (+35.9%) ⚠️  | 442.74ms → 493.59ms (+11.5%)     | 6.49% → 6.88% (+6.0%)        |
+------------------------------------------+----------------------------+----------------------------------+----------------------------------+----------------------------------+------------------------------+

Generated with hotpath-rs

ScriptedAlchemy added a commit that referenced this pull request Sep 20, 2026
perf: port measured git-metadata and clone-census improvements from #1577/#1580
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

#1882 ports e4290b05e8 (git metadata through retained topology, 127.9 µs to 27.7 µs per capture). 7da32a15ae, the event-driven wait in wait_for_production_composition_code_index, is not on master and stays owed here; its only measurement is the 165 s graph-rebuild journey suite. The shared seat fix is #1877.

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