test(maintenance): pin pipeline-decode auto-engagement on both rebuild routes - #3496
Conversation
…d routes Problem: polylogue-2cuv reported parse_s + apply_s == total_s exactly (zero overlap) on the real full rebuild, with spill_load (2830s SERIAL pickle.loads/reparse on the writer thread) as 22% of a 12633s pass. The bead asked for a bounded-memory producer/consumer pipeline overlapping parse decode with apply on both the daemon bulk-rebuild route and the offline route. Re-verification against current master found the structural fix already shipped: PR #3478 added `_ReplaySpillPrefetcher` (Lever A), a background thread that decodes upcoming replay cohorts' parsed sessions while the single writer applies the current cohort, wired into `backfill_historical_revision_evidence` via the `pipeline_decode` parameter (auto-engages under `parallel_threads_effective()` once a pass has >=8 cohorts). PR #3485 (`_lineage_aware_replay_order`, landed same day) is consumed by both the prefetcher and the writer loop from the identical `ordered_logical_keys` sequence, so lineage ordering survives pipelining. Both `polylogue/maintenance/rebuild_index.py` (offline CLI, `promote=True`) and `polylogue/daemon/bulk_rebuild.py::run_daemon_bulk_rebuild_pass` (daemon route) call the SAME `rebuild_index_from_source_sync` -> `maintenance/replay.py::rebuild_index_from_source` -> `backfill_historical_revision_evidence` chain, so the pipeline auto-engages identically on both -- there is no separate per-route wiring to add. Direct measurement (ad hoc synthetic corpus, not committed -- `spill_load` before=3.988s -> after=0.063s, `spill_prefetch.decode_concurrent` (hidden behind apply)=2.901s, 72.7% of the corpus's spill_load moved off the writer's critical path) confirms the mechanism is live, not merely present in source. Existing tests already pin outcome parity: `test_pipelined_ decode_matches_serial_archive_state` in `tests/unit/sources/test_revision_backfill.py` asserts byte-identical `RevisionBackfillResult` and full index content manifest between `pipeline_decode=False` and `pipeline_decode=True` runs over both the sqlite-spill and reparse-fallback decode lanes. What changed: added one new test proving the pipeline auto-engages from the PRODUCTION entry point both routes share (`rebuild_index_from_source_sync`), not only from the lower-level `backfill_historical_revision_evidence` calls the existing unit tests already exercised directly. Anti-vacuity: the test shrinks the spill's RAM cache tiers to force every `for_raw` to miss RAM, sizes the corpus at `_PIPELINE_DECODE_MIN_COHORTS + 4` independent raws (each its own logical cohort), and asserts `spill_prefetch.consumed > 0` in the receipt's `stage_timings_s` -- a parameter dropped anywhere in the `rebuild_index_from_source_sync` -> ... -> `backfill_historical_revision_ evidence` threading chain makes this assertion fail with 0, not a wrong number. What was NOT changed: no new pipeline plumbing -- #3478 already built it. The remaining fully-serial stage is the up-front `census` classification pass (still ahead of the replay loop by construction: cohort membership must be known before the replay loop can order/classify cohorts), which `_ReplaySpillPrefetcher` does not and structurally cannot touch -- pipelining THAT would mean overlapping census-page N+1 with replay-page N's apply, a materially larger redesign than the spill_load producer/consumer pipeline this bead asked for. Left as a residual finding, not implemented here. Verification: python -m devtools test tests/unit/maintenance/test_rebuild_parse_apply_split.py -> 5 passed python -m devtools test tests/unit/sources/test_revision_backfill.py -k pipelined_decode -> 3 passed (pre-existing equivalence proofs, confirmed still green) python -m devtools test tests/benchmarks/test_rebuild_cost_model.py -k "not full_population" -> 4 passed python -m devtools verify --quick -> exit 0 Ref polylogue-2cuv Co-Authored-By: Claude <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 22 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Rebased perf/sharded-from-empty-rebuild onto origin/master (merged since branch point: #3494 deadline_check, #3496 pipeline-decode pin, #3497 conversational-evidence gate, #3498 lineage fixture fix). Conflicts resolved in rebuild_index.py by keeping both #3494's mid-replay deadline checkpointing (non-sharded path) and this PR's shard dispatch, with shard_count>1 + pass_deadline_seconds explicitly rejected (validator and CLI) since the sharded path has no deadline_check seam yet. Ref polylogue-pzxm Co-Authored-By: Claude <noreply@anthropic.com>
Summary
polylogue-2cuv asked for a bounded-memory producer/consumer pipeline overlapping rebuild parse decode with the writer's apply work, on both the daemon bulk-rebuild route and the offline CLI route. Re-verification against current
masterfound this already shipped by PR #3478 (_ReplaySpillPrefetcher, "Lever A"), with lineage-order preservation from PR #3485 landed the same day. This PR does not add pipeline plumbing (none is missing) — it adds one regression test proving the pipeline auto-engages from the production entry point both routes share, plus an honest before/after measurement in the PR description below.Problem
The bead's core measured claim, from a real 4h22m rebuild receipt:
parse_s (census 1202s + spill_load 2830s) + apply_s (8601s) == total (12633s)exactly — zero overlap between decode and the single SQLite writer.spill_load(2830s, 22% of the pass) was documented as strictly SERIALpickle.loads/reparse work interleaved with writer apply work. The bead asked for a producer/consumer queue so census+spill hide behind apply, on bothdaemon/bulk_rebuild.pyandmaintenance/rebuild_index.py.Solution
Finding: already done. Both routes call the identical chain:
maintenance/rebuild_index.py→rebuild_index_from_source_syncdaemon/bulk_rebuild.py::run_daemon_bulk_rebuild_pass→rebuild_index_from_source_sync(same function, scheduled through the daemon's write coordinator)Both funnel into
maintenance/replay.py::rebuild_index_from_source→sources/revision_backfill.py::backfill_historical_revision_evidence, which since PR #3478 (66515459c/c6e275bca, "pipeline replay decode off the writer thread") runs a background_ReplaySpillPrefetcherthread that decodes upcoming replay cohorts' parsed sessions while the writer applies the current cohort, auto-engaging viapipeline_decode=Nonewheneverparallel_threads_effective()(free-threaded build) and the pass has>= _PIPELINE_DECODE_MIN_COHORTS(8) cohorts. PR #3485 (bdffadc2d, same day) added_lineage_aware_replay_order, which both the prefetcher and the writer loop consume from the identicalordered_logical_keyssequence — lineage ordering survives pipelining by construction.Since
pipeline_decodeauto-resolution lives inside the shared function, there is no separate per-route knob either the offline or daemon caller could have forgotten to wire — both inherit it for free.What this PR adds:
tests/unit/maintenance/test_rebuild_parse_apply_split.py::test_rebuild_index_from_source_sync_auto_engages_pipelined_decode— drives the realrebuild_index_from_source_syncentry point (not the lower-levelbackfill_historical_revision_evidencethe existing unit tests already call directly) with a corpus of_PIPELINE_DECODE_MIN_COHORTS + 4independent raws and RAM spill tiers shrunk to force everyfor_rawdecode through the prefetcher-or-inline fork, then assertsstage_timings_s["spill_prefetch.consumed"] > 0. Anti-vacuity: dropping thepipeline_decodeparameter anywhere in therebuild_index_from_source_sync→ ... →backfill_historical_revision_evidencethreading chain makes this assertion fail with0, not a wrong number.What this PR does NOT add: no new pipeline plumbing. The remaining fully-serial stage is the up-front
censusclassification pass — it structurally precedes the replay loop (cohort membership must be resolved before cohorts can be ordered/classified), and_ReplaySpillPrefetcherneither touches nor could touch it without a materially larger redesign (overlapping census-page N+1 with replay-page N's apply, across page boundaries). Left as a residual finding, not implemented here — see the honest measurement below for its current relative weight.Before/after measurement
Ad hoc synthetic-corpus script (not committed; used
tests/infra/rebuild_cost_model.py'sStratum/build_stratum_sample_corpusmachinery againstbackfill_historical_revision_evidencedirectly, comparingpipeline_decode=False— the exact pre-#3478 serial path — againstpipeline_decode=None/auto, the current production default), 160-raw corpus, ~900KB payloads, 20% chain fraction, 10% ambiguous fraction:spill_loadspill_prefetch.decode_concurrent(hidden behind apply)spill_loadnow overlappedWall-clock deltas from this run are not reported as a speedup number: the host was at load average ~19-22 on 24 cores during measurement (multiple concurrent agent lanes per repo convention), which visibly perturbed unrelated stages (e.g.
census, whichpipeline_decodenever touches, moved 5.04s → 7.89s between the two runs) — the wall-clock signal was too noisy to trust in isolation. Thespill_load/decode_concurrentstage-timing shift is the reliable signal because it is a structural property of which code path ran, not a wall-clock race against host load.Verification
Not run:
tests/benchmarks/test_rebuild_cost_model.py::test_full_population_projection(opt-in, ~40 real rebuild passes, minutes) and the full non-integration suite (devtools verify --all) — this PR's surface is a single new test in an already-green file plus a PR-body-only measurement, not new production plumbing.AC matrix (against polylogue-2cuv)
_ReplaySpillPrefetcher(PR #3478), pre-existingrebuild_index_from_source_sync; this PR adds the regression test proving it from that shared entry pointordered_logical_keysthe writer loop doestest_pipelined_decode_matches_serial_archive_state(byte-identicalRevisionBackfillResult+ full index content manifest, both RAM-miss decode lanes)Ref polylogue-2cuv