perf(pad): prefetch rebase revisions in one Promise.all (move #7756 cliff)
Summary
Dive run 25949421120 found the real ceiling — 350-400 authors on a 4-vCPU GitHub runner, with apply_mean ballooning from ~10 ms at step 200 to ~50 ms at step 400. That's much higher than the synchronous apply work; the difference is event-loop yield latency.
Both hot loops in PadMessageHandler do one await pad.getRevision(r) per intermediate revision in serial:
- `handleUserChanges` rebase: `while (r < pad.getHeadRevisionNumber()) { ... await pad.getRevision(r) ... }`
- `updatePadClients` catch-up: `while (sessioninfo.rev < pad.getHeadRevisionNumber()) { ... await pad.getRevision(r) ... }`
Under steady-state CPU pressure each await is a yield, and the continuation queues behind whatever other work is pending. At cliff load (7+ cores of work on 4 vCPU) each yield costs ~6 ms wall-clock. A commit that's 5 revs behind pays that 5x. apply_mean = 30-50 ms even though the underlying ueberdb cache lookup is microseconds.
Change
Both loops snapshot headRev once and prefetch the full range in a single Promise.all:
const rebaseTargetHead = pad.getHeadRevisionNumber();
const rebaseRange: number[] = [];
for (let i = baseRev + 1; i <= rebaseTargetHead; i++) rebaseRange.push(i);
const rebaseRevs = rebaseRange.length === 0
? []
: await Promise.all(rebaseRange.map((i) => pad.getRevision(i)));
The actual rebase / fan-out iteration is now synchronous over the prefetched array. N event-loop yields collapse to 1.
In `updatePadClients` the prefetch covers the SUPERSET range (laggiest recipient's next rev → headRev) so all per-socket loops read from the local `revCache`. The existing fallback `await pad.getRevision(r)` stays as defensive code.
Why this is correctness-positive too
The existing assertion `assert([r, r + 1].includes(newRev))` after the rebase loop was racy if another writer landed between the serial awaits — `pad.getHeadRevisionNumber()` could jump mid-loop. Snapshotting head once makes `r === rebaseTargetHead` deterministic at the end of the loop.
Tests
- 3/3 in `src/tests/backend-new/specs/rebase-prefetch.test.ts` — pin the helper's parallel-fetch shape and result-ordering invariants.
- Existing 600+ backend-new tests still pass.
Test plan
- CI green
- Score against dive workflow with `core_ref=feat/prefetch-rebase-revs` at `authors=100..500`. Expect apply_mean at the cliff to drop noticeably (today's 40-50 ms → ideally ~10-15 ms), which should push the cliff out from step 400.
- If the cliff moves, push deeper.
🤖 Generated with Claude Code