Paired per-route benchmarks
scripts/bench measures how much throughput each target sustains under
load. scripts/bench-paired answers a
narrower question: on one route, is the target slower or faster than the
reference, by more than the noise? It is the per-route counterpart to
roundhouse-compare: the compare proves that two
servers return the same response for a path, and this times that path on
both.
An average across routes can hide a route that got slower. An emitted app can be several times faster than Rails overall and still regress on one route that takes a residue or fallback path. This tool reports each route separately and does not average them away.
Method
The protocol is adapted from Yuan et al., Where Does the Performance Go? Understanding and Recovering Performance Regressions in C-to-Rust Translation (arXiv:2610.23044). That paper found translation slowdowns varied widely between operations of the same project, for example −0.3% and +62% on two hash functions in one library.
- One route is one operation. A route is a complete request whose
response can be checked. Check it with
roundhouse-compareon the same paths and seeded rows before timing it;bench-pairedonly times. - A sample is per-request cost. One keep-alive connection sends
requests back to back (
wrk -t1 -c1) for--sample-secseconds. The sample is the mean time per request. - Pairs alternate. Each pair is a reference sample and a target sample taken back to back, and the order flips every pair. Drift (thermals, a neighbour, a JIT compiling elsewhere) then lands on both sides equally. Warm-up pairs are taken first and discarded.
- Unstable batches are retaken. A batch is stable when the
coefficient of variation (CV) of its pairwise ratios is at most
--max-cv(3%). An unstable batch is retaken up to--retriestimes. If it never settles, it is reported as unstable rather than dropped. - Outliers are trimmed, then the median is taken. Pairwise ratios outside Tukey's 1.5 × IQR fences are dropped. The route's ratio is the median of the rest, with a seeded bootstrap 95% interval.
The ratio is target time divided by reference time, so a value below 1
means the target is faster. Each route gets a verdict by comparing its
interval with --tolerance (default 3%):
| Verdict | Meaning |
|---|---|
slower |
the whole interval is above 1 + tolerance |
faster |
the whole interval is below 1 − tolerance |
same |
the whole interval is inside 1 ± tolerance |
inconclusive |
none of the above: take more pairs or quieten the machine |
--fail-on-slower exits 1 when any route is slower. An inconclusive
or unstable route does not fail the run. Read the CV column before you
trust a verdict.
Client and loopback costs are added equally to both sides, which pulls every ratio toward 1. That understates a large speedup or slowdown, but never reverses the direction.
Real-blog: Rails against the emitted Ruby app
Seed both servers from the same database, the way scripts/compare and
scripts/bench do. Start each in the configuration you want to compare:
production environment, the same JIT and worker settings, each pinned to
its own core. Then time the compare paths:
bin/rh fixture && bin/rh transpile ruby
sqlite3 fixtures/real-blog/storage/development.sqlite3 \
".backup fixtures/real-blog/storage/production.sqlite3"
mkdir -p build/transpiled-blog-ruby/storage
sqlite3 fixtures/real-blog/storage/development.sqlite3 \
".backup build/transpiled-blog-ruby/storage/development.sqlite3"
(cd fixtures/real-blog && RAILS_ENV=production SECRET_KEY_BASE=bench \
RUBYOPT=--yjit taskset -c 2 bin/rails server -p 4000 -e production) &
(cd build/transpiled-blog-ruby && \
BUNDLE_GEMFILE=../../runtime/spinel/scaffold/Gemfile \
BLOG_DB=storage/development.sqlite3 RAILS_ENV=production PORT=3000 \
RUBYOPT=--yjit taskset -c 4 bundle exec puma -C config/puma.rb config.ru) &
tools/compare/target/release/roundhouse-compare \
--reference http://localhost:4000 --target http://localhost:3000 \
--path /articles --path /articles/1 # the responses must match first
taskset -c 6 scripts/bench-paired \
--reference http://localhost:4000 --target http://localhost:3000 \
--label-reference rails --label-target ruby --out paired.json
With no --path, it times the paths that scripts/compare checks for
the ruby target. taskset is Linux-only; on macOS, leave it out and
expect noisier batches.
Gating a change to the emitter
To check a Roundhouse change for performance regressions, emit the same fixture with the baseline build and with the candidate build. Serve both trees the same way, and time the candidate against the baseline:
scripts/bench-paired --reference http://localhost:3001 \
--target http://localhost:3002 --fail-on-slower --out gate.json
Here the reference is the baseline emit. A route is flagged slower
only when the whole interval clears the tolerance, so noise alone does
not fail the gate.
Re-reading a result
--out writes every sample, the configuration, a scripts/bench-env
snapshot of the machine, and the summary. --analyze FILE re-reads such
a file without measuring again. Pass --tolerance, --max-cv or --seed
to re-judge the same samples with different settings.