perf(pad): prefetch rebase revisions in one Promise.all (move #7756 cliff)

Closed
JO
5mo ago
+112-18

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