internal/planning/library-benchmarking.md
We use max rate benchmarking - each route is tested at maximum throughput to measure its capacity.
RATE=max - Tests maximum throughputCONNECTIONS=10 - Concurrent connectionsDURATION=30s - Test duration per routePros:
Cons:
Pros:
Cons:
Different routes have vastly different capacities:
/empty - ~1500 RPSA fixed rate low enough for all routes would under-utilize fast routes. A per-route fixed rate config would be painful to maintain and would break comparisons when capacity changes.
For library benchmarking in CI, we accept some noise and focus on detecting significant regressions (>15-20%).
Options to improve accuracy if needed:
The benchmark workflow currently treats main-regression alerts as warnings because single-run Bencher alerts on GitHub-hosted runners have been dominated by environmental noise. The goal is to make a fired alert much more likely to represent a real regression before restoring a hard gate, where CI fails the job instead of posting a warning.
Standing instructions while this plan is in effect:
## Main Gate Re-Enablement Plan section (this heading and every
subsection through the end of the file, including the trailing Issue 3169 link) in a follow-up commit; the executed
plan then lives only in git history.The Bencher reporting baseline fix from PR 3148 landed on
2026-04-23. Do not re-enable the hard gate until at least 30 successful Benchmark Workflow runs on main have built
fresh history. Count only completed benchmark jobs triggered after that merge; exclude pre-merge runs, branch runs,
reruns of any kind (manual workflow reruns or automatic GitHub retries), and docs-only pushes skipped by
script/ci-changes-detector. Record each counted run ID and timestamp in
Issue 3169.
The current Bencher invocation lives in .github/workflows/benchmark.yml inside the run_bencher function:
BOUNDARY=0.95MAX_SAMPLE=64--err causes Bencher regression alerts to return a non-zero exit code--threshold-test t_test and --threshold-max-sample-size $MAX_SAMPLErps uses --threshold-lower-boundary $BOUNDARY and --threshold-upper-boundary _ (which disables the upper bound)
because higher RPS is better; a regression is a drop below the lower boundp50_latency, p90_latency, p99_latency, and failed_pct use --threshold-lower-boundary _ (which disables the
lower bound) and --threshold-upper-boundary $BOUNDARY because lower latency and failure rate are better; a regression
is a rise above the upper boundrun_bencher captures Bencher's non-zero exit as BENCHER_EXIT_CODE; the
Warn if Bencher detected regression on main step emits ::warning:: for regression alerts instead of exitingperformance-regression and links to the
regression runFail on non-regression Bencher error on main, so only regression
alerts are soft while the gate is in warning mode$BENCHER_EXIT_CODE after the false-positive
target is met, not removing --errNote: The values above are a snapshot of the workflow at the time of writing and capture only the tuning-relevant flags; operational flags such as
--quietand--format htmlare intentionally omitted because they do not affect threshold behavior. Verify against.github/workflows/benchmark.yml(run_bencherfunction) before tuning because the workflow is the source of truth.
Keep the gate in warning mode while gathering the new baseline.
Compare adjacent qualifying main runs by shared (benchmark, measure) alert pairs:
https://bencher.dev/perf/react-on-rails-t8a9ncxo; otherwise use the overlap-comparison method
tracked in Issue 3169.script/ci-changes-detector does not classify as docs-only.A and B, compute |A intersect B| / |A union B|. For
example, two shared pairs across 10 total unique alert pairs gives 2 / 10 = 20%.0.20): Overlap below this means runner noise is still dominating; keep collecting runs.0.40): Proceed to step 3 only after the full 30-run baseline window exists and overlap is
at least 0.40 for 3 consecutive adjacent qualifying-run pairs. The gap between 0.20 and 0.40 avoids flip-flopping
between noise and signal states; the thresholds were chosen empirically from the alert-overlap evidence in Issue 3169
and should be revisited if the alert distribution changes significantly.Prefer threshold changes that require stronger evidence before failure:
0.95 toward 0.99 using the boundary tuning cadence below--threshold-max-sample-size $MAX_SAMPLE aligned with the available history; add a minimum-sample rule only
if Bencher supports that flag for the configured threshold type(benchmark, measure) pair alert on at least 2 consecutive
runs before filing or failing (restated as Acceptance Criterion 2 below — the same gate, viewed from the tuning side)Boundary tuning cadence. Widen the boundary in fixed 0.01 increments, dwelling at each setting long enough to
judge whether the widening is still reducing noise:
0.95) counts as the dwell for 0.95, so do not collect a separate
10-run dwell there; the first advance/lock decision uses overlap from the most recent 5 adjacent pairs within that
window. Do not re-collect a 30-run window at any widened boundary — each boundary past 0.95 gets the standard
10-run dwell. Boundary changes alone never trigger a baseline reset (see the Baseline reset exception above).0.40 for 3 consecutive pairs), so entering this cadence does not guarantee an immediate Lock
at 0.95.0.40, raise the boundary by 0.01 and return to step 1 of this cadence at
the new value: noise is still dominating, so requiring a wider boundary should help.0.40, stop widening and keep the current boundary (subject to the
noisy-alert rate sanity check in the Interaction section below); recurring alerts now dominate the set, and
widening further risks masking real regressions. Proceed to the Acceptance Criteria.0.99 and mean overlap is still below 0.40 after the dwell, do not widen
further; escalate to step 4 (larger or dedicated runners). Noise that survives the widest boundary is a
runner-environment problem, not a threshold problem.0.99 ceiling, then escalate to step 4 once there). Record the small sample in
Issue 3169 either way.Record each boundary value, its dwell run IDs, and the resulting overlap in
Issue 3169. End to end this is at most 5 boundary values
(0.95 through 0.99).
Interaction with the false-positive target and real regressions during tuning:
0.40 but the noisy-alert rate is still
far worse than the target (for example, more than 5 noisy alerts per 20 runs — a rough "clearly dominating"
threshold, not a precisely calibrated bound like the 0.40 overlap signal), a few chronic flakes are probably
dominating the overlap rather than real recurring regressions, so keep widening (up to the 0.99 ceiling in step 5
above) and record the exception in Issue 3169. If the boundary is already at 0.99 when this override applies, there
is nowhere left to widen: record the exception in Issue 3169 and escalate to step 4 (larger or dedicated runners)
rather than locking on chronically noisy alerts.0.95. This re-entry dwell is always the
standard 10-run dwell — even when 0.95 itself was the locked boundary, re-entry does not require a new 30-run
baseline window.If shared-runner noise remains high, move benchmark jobs to larger GitHub-hosted runners or dedicated runners before restoring the hard gate.
BOUNDARY=0.95 with the
t-test masks smaller deltas; recalibrate this floor downward if the gate later moves to dedicated runners or boundary
widening reduces the noise floor. If no alert fires, re-tune before proceeding.(benchmark, measure) pair alerting on at least 2 consecutive runs before filing or
failing; a single noisy run does not trigger the gate. This is a manual gate: Bencher still alerts on the first run,
and the requirement is that a reviewer confirms recurrence in Issue 3169 before acting on it. (This is the same gate
stated in Tuning Sequence step 3; restated here because it is also a pre-condition for the 5-run clean-run count.)Benchmark Workflow runs complete with no
Bencher regression alert; that means BENCHER_HAS_ALERT stays 0 with the current code (a value of 1 is what
triggers the warning step and the regression-issue update). A run qualifies when the
triggering push modifies at least one file that script/ci-changes-detector does
not classify as docs-only. Track the running count in Issue 3169.Benchmark Workflow runs whose triggering commits do not intentionally change
benchmark performance. Criterion 5 below defines who tracks this rate after re-enabling and the review cadence that
triggers reverting to warning mode if the target is breached.(benchmark, measure) pair in the next qualifying run and has no matching
performance-sensitive code change. The 1-in-20 window is rolling: count the most recent 20 such qualifying runs (the
same cohort defined in criterion 4), and exclude intentional-perf-change commits from both the numerator (noisy
failures) and the denominator (the 20-run total) rather than counting them as either real or noisy.
Review the running rate after every 5 gate-triggering runs or at least monthly, whichever comes first. If the gate later
exceeds the 1-in-20 noisy-failure rate on main, revert it to warning mode and re-tune thresholds from the existing
baseline window and overlap data before trying to re-enable it again.See Issue 3169 for the tracking discussion and historical alert-overlap evidence. The boundary tuning cadence in Tuning Sequence step 3 was specified in response to Issue 3260.