Skip to content

test(perf): scale the nightly limits by how fast the runner is (#159) - #160

Merged
ibrahimroshdy merged 1 commit into
mainfrom
fix/perf-calibrate
Oct 17, 2026
Merged

ibrahimroshdy merged 1 commit into
mainfrom
fix/perf-calibrate

Conversation

@ibrahimroshdy

Copy link
Copy Markdown
Collaborator

Closes #159.

The nightly perf job failed on slow runners, not slow code. The CPU model the snapshot bench prints matches the result every night: the EPYC 9V45 passed on Oct 10 and 12, and the EPYC 7763 (four nights), 9V74 and Xeon 8573C failed with unchanged code. Now the run times the runner first, and every millisecond limit in test/perf scales with how slow it is.

How it works

  • test/perf/global-setup.ts (a second global setup in vitest.perf.config.ts) times a fixed reference workload once, before any file runs. None of the workload is Kept code (test/perf/calibrate.ts):
    • cpu: pure JS, 150,000 records through JSON, a sort, a Map, normalize. A checksum pins it.
    • db: plain SQL in a scratch database. 100k rows into a temp table, an index, an aggregate, a deep ordered page and a self-join.
    • vector: an exact cosine scan for the 50 nearest of 10,000 768-dim vectors, ten times. This is the shape of semantic search at the fixture's size. Semantic search slowed about 2.5× on the 7763 while most checks slowed 1.5–1.8×, so vector work gets its own part.
    • Each part counts its fastest of 5 runs. Commits and round trips are left out, because their cost depends on the disk and on Docker's networking, not on CPU speed. The whole workload takes about 9 s.
  • The factor is the geometric mean of measured/reference over the three parts, clamped to [1, 3]. Every limit is multiplied by it (under() in test/perf/limits.ts).
  • A failure says, for example, searchSemanticP95Ms: 520 ms against 300 ms × 1.68 (runner speed) = 504 ms. Each file's JSON gets calibration, plus a limits entry per check (measuredMs, limitMs, factor, scaledLimitMs, pass). .tmp/perf/calibration.json holds the raw timings.
  • A real regression still fails. It slows the code under test, but the reference work doesn't run that code, so the factor stays the same.
  • Vehicles' "within 20% of step 4's figures" check stays relative to the recorded figures and is scaled like every other limit. It gets no extra noise floor: 20% of those figures is 18–24 ms, and they were taken on a slower machine than the reference runner.
  • The snapshot bench (rls.bench.ts) is not scaled. Its budgets are §3.1's, and the slowest night used about 28% of them.

The reference values are inferred, not measured on a runner. REFERENCE_MS should be the 9V45's timing, but the workload is new, so nothing has measured it there yet. It is the M1 Pro's timing from a full local perf run, divided by 1.42. That is how much slower the M1 Pro was than the 9V45 at the same fixed work in the same run: the snapshot bench's full snapshot as owner, p50 453.5 ms locally against 327.2 / 311.5 ms on Oct 10 / 12. Re-pin it from the first 9V45 night's calibration.json.

Tested

  • test/perf-calibration.test.ts (normal suite, 11 tests) covers:
    • the factor and limit arithmetic: factor 1 on the reference runner, never below 1, the geometric mean, the cap at 3, refusing bad timings
    • the workload's checksum
    • a 2× regression in search still failing: the Oct 12 figure doubled fails on a factor-1 runner, and the Oct 17 figure (failing today) passes at a 1.68 factor while doubling it fails. The slow runner's calibration is the same with or without the regression.
  • bash scripts/ci-local.sh --only perf ran once locally, on an M1 Pro at load 10–26 from other runs, so its figures are not evidence. Calibration: cpu 273.1 / db 626.9 / vector 401.8 ms. That run used a provisional reference (the M1 Pro's own earlier probe), so its factor was 1.12, and 19 of 20 checks passed. The one failure was the notification count, p95 24.9 ms against 20 × 1.12 = 22.4 ms. Under the reference pinned here, that run's factor would have been 1.42 (limit 28.4 ms).
  • ci-local.sh --only lint,typecheck,drift and check-i18n pass.
  • Full server suite: 3,523 passed, 2 failed, 55 skipped. Both failures are timing tests in files this change doesn't touch, run at load 20–28: things/move.test.ts "200 things in under a second" (1,898 ms; 1,194 ms on a rerun) and files/pdf-text.test.ts (a runaway PDF hit timeout before memory; it passed on a rerun).

The nightly perf job failed on slower GitHub runners with unchanged code. The run now times a
fixed reference workload first: pure JS, plain SQL and an exact vector scan, none of it Kept's
code. Every millisecond limit in test/perf is multiplied by the speed factor, clamped to [1, 3].
The reports and failure messages print the factor and the scaled limit. A regression still
fails, because the reference work doesn't slow down with the code.

The reference values are inferred from a local M1 Pro run and the snapshot bench's M1 Pro-to-
EPYC 9V45 ratio. Re-pin them from a fast night's calibration.json.

Closes #159

Signed-off-by: Ibrahim Roshdy <22573766+ibrahimroshdy@users.noreply.github.com>
@ibrahimroshdy
ibrahimroshdy merged commit fe44c2a into main Oct 17, 2026
21 checks passed
@ibrahimroshdy
ibrahimroshdy deleted the fix/perf-calibrate branch October 17, 2026 15:44
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Perf: scale the nightly limits by how fast the runner is

1 participant