Stop publishing runs whose responses were mostly errors#1020
Open
MDA2AV wants to merge 2 commits into
Open
Conversation
rps counts a 500 exactly like a 200, and nothing between the load generator
and the board ever looked at the status counts. They are parsed by every
tool adapter, written into results/, carried through rebuild_site_data.py
and listed in gen_new_leaderboard_data.py's BASE_FIELDS -- and then read by
nobody. A framework that collapsed under load was ranked on the rate at
which it produced failures.
Twenty-one published rows were not measurements:
php-fpm baseline-h2-1024 294,864 rps 99.2% 5xx
symfony-spawn-franken baseline-h2-256 28,243 rps 72.9% 5xx
symfony-spawn-franken static-h2-1024 52,582 rps 56.5% 5xx
symfony-spawn-franken static-h2-256 63,518 rps 47.3% 5xx
symfony-spawn-franken baseline-h2-1024 97,817 rps 36.5% 5xx
phoenix-bandit upload-{32,256} ~700 rps 33.3% 4xx
rage api-16-1024 9,825 rps 15.2%
rage api-4-256 13,999 rps 8.8%
apache/caddy/pingora pipelined-{512,4096} 0 rps 100.0% 4xx
traefik pipelined-{512,4096} 0 rps 100.0% 5xx
fasthttp crud-4096 0 rps 0 responses
effinitive static-h2-1024 0 rps 0 responses
trillium-tuned static-h2-{256,1024} 0 rps 0 responses
php-fpm is the clearest case: 294k rps of 500s placed it mid-field on
baseline-h2, ahead of frameworks answering every request correctly.
The four zero rows are a different lie. Their logs show why: fasthttp
crashed (a V backtrace in site/static/logs/crud/4096/fasthttp.log, and
14,506,257 reconnects as every connect was refused), while effinitive and
trillium-tuned bound their listeners and then returned nothing at all while
burning 1486-6187% CPU and 4.3-5.1 GiB. Publishing rps=0 states "we
measured zero throughput", which ranks them last on a test that never ran.
Absent is the honest value, and the composite already reads absent as 0.
Two changes plus the purge:
rebuild_site_data.py grows failure_reason() and applies it in
merge_results() to the merged list, not just the incoming batch, so a bad
row already in site/data is dropped on the next rebuild even when its
framework isn't re-run. This matters because site/data is generated: hand
-deleting these rows would have lasted exactly until the next --save, which
would have restored them from the raw files still on the box. The threshold
is 5% non-2xx/3xx, overridable with HTTPARENA_MAX_ERROR_PCT.
benchmark.sh warns at save time when a run fails the same rule. The row and
the container log are still written -- they are the post-mortem evidence --
but a clean-looking --save should not imply a result that will never reach
the board.
Composite impact is small, since these rows were mostly earning little:
rage -24.4 (api-4 was 20.6 of it), phoenix-bandit -9.4, symfony-spawn
-franken -5.6, php-fpm -1.0, against a ~2600 ceiling. The zero rows score 0
either way. The eight pipelined proxy rows change nothing at all: pipelined
is reference-only, and #1017 removes those frameworks outright.
The 1-5% band is deliberately left published -- roadrunner, sark,
vanilla-epoll, swerver and others sit there, and at 16k connections a
handful of failures is plausible rather than diagnostic. Verified by
rebuilding a synthetic results tree through merge_results: 9.1% dropped,
4.8% kept, and a pre-existing failed row purged without being re-run.
scripts/__pycache__/rebuild_site_data.cpython-312.pyc rode in with the previous commit: verifying the new failure_reason() meant importing rebuild_site_data as a module, which writes bytecode next to the source, and nothing in .gitignore covered it. Removes the file and ignores __pycache__/ and *.py[cod] so importing any script in scripts/ can't do this again.
MDA2AV
added a commit
that referenced
this pull request
Jul 24, 2026
Closes #751. Results lived in 52 shared arrays — site/data/<profile>-<conns>.json, each holding every framework — so a saved run rewrote files that every other framework also occupies. They now live one file per framework: site/data/baseline-512.json [ {actix…}, {hyper…}, … ] -> site/data/results/actix.json { framework, results: { "baseline-512": {…} } } 108 files, 1941 rows, same rows verbatim. The conflict is real but narrower than "any two saves": git merges non-adjacent hunks fine, so two frameworks updating existing rows usually merge. It bites when rows are *inserted* — two contributors each adding a framework, which is exactly when a rebase is most annoying. Reproduced both ways against the real data: old layout, actix + hyper update rows merges cleanly old layout, two new frameworks added CONFLICT in baseline-512 and -4096 new layout, the same two new frameworks merges cleanly, both files present Rows are copied verbatim rather than reshaped, which makes the migration provable: regenerating data.js before and after gives a byte-identical file. That is the main guarantee here — the board, the composite and every archived round see exactly what they saw before. Consumers: rebuild_site_data.py writes each framework's file; a run touches only the frameworks it benchmarked (verified: benchmarking actix + hyper modified 2 of 108 files) gen_leaderboard_data loads site/data/results/*.json once and rebuilds the per-profile view, sorted by framework name so the emitted data.js does not churn compare.sh one read of the framework's own file instead of a scan of every profile file archive.sh regroups into the existing round shape, so rounds/*.json keeps its current format benchmark-pr.yml stages only site/data/results/<slug>.json The filename is the display_name lowercased with anything outside [A-Za-z0-9._-] replaced by '-', because two gateway entries are named "aspnet-minimal + nginx" and "+ caddy". .gitignore's `results/` rule was unanchored and silently swallowed the new directory — the first attempt committed the code and none of the data. It is now `/results/`, which is the raw benchmark output it was always meant to cover; the root directory is still ignored. scripts/migrate_results_layout.py is kept rather than deleted: it re-derives the layout from a flat checkout, which is what a branch predating this needs in order to rebase. Note for #1020, which edits rebuild_site_data.py and deletes rows from the flat files: it will need a rebase, and running the migration script after merging main is the mechanical way to do it.
MDA2AV
added a commit
that referenced
this pull request
Jul 24, 2026
Closes #751. Results lived in 52 shared arrays — site/data/<profile>-<conns>.json, each holding every framework — so a saved run rewrote files that every other framework also occupies. They now live one file per framework: site/data/baseline-512.json [ {actix…}, {hyper…}, … ] -> site/data/results/actix.json { framework, results: { "baseline-512": {…} } } 108 files, 1941 rows, same rows verbatim. The conflict is real but narrower than "any two saves": git merges non-adjacent hunks fine, so two frameworks updating existing rows usually merge. It bites when rows are *inserted* — two contributors each adding a framework, which is exactly when a rebase is most annoying. Reproduced both ways against the real data: old layout, actix + hyper update rows merges cleanly old layout, two new frameworks added CONFLICT in baseline-512 and -4096 new layout, the same two new frameworks merges cleanly, both files present Rows are copied verbatim rather than reshaped, which makes the migration provable: regenerating data.js before and after gives a byte-identical file. That is the main guarantee here — the board, the composite and every archived round see exactly what they saw before. Consumers: rebuild_site_data.py writes each framework's file; a run touches only the frameworks it benchmarked (verified: benchmarking actix + hyper modified 2 of 108 files) gen_leaderboard_data loads site/data/results/*.json once and rebuilds the per-profile view, sorted by framework name so the emitted data.js does not churn compare.sh one read of the framework's own file instead of a scan of every profile file archive.sh regroups into the existing round shape, so rounds/*.json keeps its current format benchmark-pr.yml stages only site/data/results/<slug>.json The filename is the display_name lowercased with anything outside [A-Za-z0-9._-] replaced by '-', because two gateway entries are named "aspnet-minimal + nginx" and "+ caddy". .gitignore's `results/` rule was unanchored and silently swallowed the new directory — the first attempt committed the code and none of the data. It is now `/results/`, which is the raw benchmark output it was always meant to cover; the root directory is still ignored. scripts/migrate_results_layout.py is kept rather than deleted: it re-derives the layout from a flat checkout, which is what a branch predating this needs in order to rebase. Note for #1020, which edits rebuild_site_data.py and deletes rows from the flat files: it will need a rebase, and running the migration script after merging main is the mechanical way to do it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
rpscounts a 500 exactly like a 200, and nothing between the load generator and the board ever looked at the status counts. They're parsed by every tool adapter, written intoresults/, carried throughrebuild_site_data.py, and listed ingen_new_leaderboard_data.py'sBASE_FIELDS— then read by nobody. So a framework that collapsed under load was ranked on the rate at which it produced failures.The 21 rows that weren't measurements
php-fpmsymfony-spawn-frankensymfony-spawn-frankensymfony-spawn-frankensymfony-spawn-frankenphoenix-banditragerageapache,caddy,pingoratraefikfasthttpeffinitivetrillium-tunedphp-fpmis the clearest case: 294k rps of 500s placed it mid-field on baseline-h2, ahead of frameworks answering every request correctly.The four zero rows are a different lie, and the logs say why —
fasthttpcrashed (a V backtrace insite/static/logs/crud/4096/fasthttp.log, plus 14,506,257 reconnects as every connect was refused);effinitiveandtrillium-tunedbound their listeners and then returned nothing while burning 1486–6187% CPU and 4.3–5.1 GiB. Publishingrps: 0states "we measured zero throughput", which ranks them last on a test that never ran. Absent is the honest value, and the composite already reads absent as 0.Changes
rebuild_site_data.pygrowsfailure_reason()and applies it inmerge_results()to the merged list, not just the incoming batch — so a bad row already insite/datais dropped on the next rebuild even when its framework isn't re-run. This is load-bearing:site/datais generated, so hand-deleting these rows would have lasted exactly until the next--saverestored them from the raw files still on the box. Threshold is 5% non-2xx/3xx, overridable viaHTTPARENA_MAX_ERROR_PCT.benchmark.shwarns at save time on the same rule. The row and container log are still written — they're the post-mortem evidence — but a clean-looking--saveshouldn't imply a result that will never reach the board.composite-score.md, so a contributor whose row goes missing can find out why.Ranking impact
Small — these rows were mostly earning little:
rage−24.4 (api-4 is 20.6 of it),phoenix-bandit−9.4,symfony-spawn-franken−5.6,php-fpm−1.0, against a ~2600 ceiling. The zero rows score 0 either way. The eightpipelinedproxy rows change nothing: pipelined is reference-only, and #1017 removes those frameworks outright.Deliberately left alone
The 1–5% band stays published —
roadrunner,sark,vanilla-epoll,swerverand others sit there, and at 16k connections a handful of failures is plausible rather than diagnostic. 1969 of 2017 rows had exactly zero errors, so the tail is thin either way.Verification
Rebuilt a synthetic results tree through
merge_results(): 9.1% dropped, 4.8% kept, and a pre-existing failed row purged without being re-run. The shell warning was checked against the same thresholds including the env override. 1996 published rows remain, 0 failing the rule.🤖 Generated with Claude Code