Skip to content

ci(nanonbt-bench): bench pull requests against main and comment - #12

Merged
topi-banana merged 11 commits into
mainfrom
ci/bench-pr-result-comment
Sep 30, 2026
Merged

topi-banana merged 11 commits into
mainfrom
ci/bench-pr-result-comment

Conversation

@topi-banana

@topi-banana topi-banana commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

ci.yml builds and lints the comparison bench but never runs it: criterion reported wall time, and timing noise between jobs on a shared runner would drown the numbers. This pull request adds a Bench workflow that runs the suite on every pull request and comments its results against main.

The bench now measures with iai-callgrind, which counts instructions under callgrind. The count does not depend on the machine or the run, so one pass per side compares as exact numbers: no noise threshold, no repeated passes, no mirrored order. Earlier revisions of this branch still tried to make criterion's wall-clock numbers comparable — two parallel jobs in opposite orders, an agreement rule, setarch -R — and could not: the same comparison, run twice on the same two revisions, called 32 entries improved and 43 regressed in one run where the other called 63 and 42, and the same head binary differed by up to 42% between the two runs. Criterion's confidence interval is a within-run one and cannot see either. Those jobs, their artifacts and the setarch wrappers are gone.

The workflow is one compare job. It checks the base commit out with the pull request's crates/nanonbt-bench laid over it — the base may not carry the suite yet, and both revisions should be measured by the same entries — benches it, then checks the head out and benches it again. Both passes run under the one valgrind and the one iai-callgrind-runner the job installs, the runner at the version the bench's lock file pins. --save-summary=json leaves one summary per entry below target/iai; target/iai is cleared before the first pass, and the base tree is moved aside before the head pass writes its own. A pull request from a fork, or one Dependabot opened, runs with a read-only token: the comment step is skipped and its numbers stay in the job log.

Every entry is now one #[library_benchmark] named <kind>_<target>_<id>, so the summarizer can rebuild the report id from the function name and the id its #[bench] attribute carries — parse_nanonbt_serde_small is parse/nanonbt-serde/small. The document bytes and the write setup are hoisted out of the measured function: iai-callgrind evaluates args and setup once, outside the callgrind toggle, so neither is counted. --cache-sim=no skips the cache simulation the report never reads while keeping the instruction count. A value that borrows from its input goes through write_bench_leaked!, which leaks the bytes so the borrow can outlive setup; empty_group! stands in for a target whose feature is off, so main! always names the same four groups.

examples/bench-summary.rs reads the two trees of summary.json — the first profile's total Ir, keyed by benchmark id — and renders the comment under the bench header. It leads with the counts — improved, regressed and unchanged, plus new and removed when there are any — then the changes themselves, green for fewer instructions and red for more, each with both instruction counts and the percentage, then a collapsed table of every entry grouped by parse, write and skip. Equal work counts equal instructions, so there is no threshold and no unstable bucket. The pumpkin entries are the exception and are listed for reference only: their compounds are std hash maps, serialization walks them in the order the process's random seed builds, and that moved their counts by up to nine percent across thirty runs of the same binary.

bench.yml is its own workflow and does not feed ci's aggregate; the bench job in ci keeps compiling and linting the crate, and the numbers live here.

Changes

  • bench(nanonbt-bench): count instructions with iai-callgrind (20f5368)
    • .github/workflows/bench.yml: the single compare job — base with the pull request's bench, then head, both under callgrind — and the iai-callgrind-runner install, --save-summary=json, the summary example and the sticky comment
    • .github/workflows/ci.yml: the bench job's comment now points at bench.yml
    • crates/nanonbt-bench/Cargo.toml, Cargo.lock: iai-callgrind replaces criterion, serde_json serves the example, and [profile.bench] debug = true gives callgrind the symbols it matches names by
    • crates/nanonbt-bench/benches/compare/macros.rs: parse_bench!, write_bench!, write_bench_leaked! and empty_group!
    • crates/nanonbt-bench/benches/compare/main.rs, documents.rs, targets/*: every entry is one #[library_benchmark], with documents and write setup hoisted out of the measurement
    • crates/nanonbt-bench/examples/bench-summary.rs: reads the two iai-callgrind summary trees, pairs the entries by id and renders the comment

Testing

  • the Bench workflow on this pull request: its head changes no library source, so every comparable entry must count the same instructions; the run reports 0 improved, 0 regressed and 173 unchanged of the 173 entries compared, with the 29 pumpkin entries listed for reference only
  • the ci workflow on the same head, including the bench crate's cargo fmt --all --check and cargo clippy --all-targets --locked -- -D warnings
  • thirty runs of the same binary for the pumpkin caveat: their counts moved by up to nine percent between runs

@topi-banana topi-banana self-assigned this Sep 27, 2026
@topi-banana
topi-banana marked this pull request as ready for review September 27, 2026 07:12
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Benchmark results

ebc0cded (this pull request) against b99053bf (main). Workflow run.

Every counting platform compares its own base and pull request; a count is exact on the platform that produced it, but counts from different architectures are different instruction sets and are not comparable with each other. pumpkin entries are listed for reference only: their compounds are std hash maps, so their counts move between runs with the seed the map is built under.

Platforms

Platform Harness Entries 🟢 🔴 ⚪ Other
linux-x86_64 callgrind instructions 202 0 0 173 29 reference
linux-aarch64 callgrind instructions 202 0 0 173 29 reference
wasm32-wasip1 wasmi fuel 144 0 0 115 29 reference
windows-x64 build & run 202 run — — — —
windows-arm64 build & run 202 run — — — —
macos-x64 build & run 202 run — — — —
macos-arm64 build & run 202 run — — — —

linux-x86_64

Callgrind instruction counts: exact within this platform, so the two sides compare without a threshold; lower is better.

0 improved · 0 regressed · 173 unchanged of 173 entries compared — 29 reference.

All 202 entries

parse

Benchmark PR Change
parse/fastnbt-borrow/byte-array 466 ⚪
parse/fastnbt-borrow/int-array 525 ⚪
parse/fastnbt-borrow/long-array 521 ⚪
parse/fastnbt/byte-array 1001827 ⚪
parse/fastnbt/chunk 333701 ⚪
parse/fastnbt/int-array 4842117 ⚪
parse/fastnbt/long-array 5177898 ⚪
parse/fastnbt/long-names 81766 ⚪
parse/fastnbt/player 154381 ⚪
parse/fastnbt/short-list 28029675 ⚪
parse/fastnbt/short-names 78158 ⚪
parse/fastnbt/small 2979 ⚪
parse/nanonbt-borrow-signed/byte-array 362 ⚪
parse/nanonbt-borrow-signed/int-array 366 ⚪
parse/nanonbt-borrow-signed/long-array 366 ⚪
parse/nanonbt-borrow-signed/short-list 362 ⚪
parse/nanonbt-borrow/byte-array 362 ⚪
parse/nanonbt-borrow/chunk 171421 ⚪
parse/nanonbt-borrow/int-array 366 ⚪
parse/nanonbt-borrow/long-array 366 ⚪
parse/nanonbt-borrow/player 167917 ⚪
parse/nanonbt-borrow/short-list 362 ⚪
parse/nanonbt-borrow/small 2165 ⚪
parse/nanonbt-derive-signed/byte-array 500957 ⚪
parse/nanonbt-derive-signed/int-array 1438412 ⚪
parse/nanonbt-derive-signed/long-array 3375912 ⚪
parse/nanonbt-derive-signed/short-list 9029529 ⚪
parse/nanonbt-derive/byte-array 500932 ⚪
parse/nanonbt-derive/chunk 269811 ⚪
parse/nanonbt-derive/int-array 1438403 ⚪
parse/nanonbt-derive/long-array 3375903 ⚪
parse/nanonbt-derive/long-names 47671 ⚪
parse/nanonbt-derive/player 177186 ⚪
parse/nanonbt-derive/short-list 9029522 ⚪
parse/nanonbt-derive/short-names 11508 ⚪
parse/nanonbt-derive/small 2367 ⚪
parse/nanonbt-serde/chunk 300363 ⚪
parse/nanonbt-serde/long-names 117888 ⚪
parse/nanonbt-serde/player 237345 ⚪
parse/nanonbt-serde/short-names 80782 ⚪
parse/nanonbt-serde/small 3953 ⚪
parse/pumpkin/byte-array 1002673 not compared
parse/pumpkin/chunk 728065 not compared
parse/pumpkin/int-array 1880265 not compared
parse/pumpkin/long-array 2570251 not compared
parse/pumpkin/long-names 141549 not compared
parse/pumpkin/player 520528 not compared
parse/pumpkin/short-list 79422793 not compared
parse/pumpkin/short-names 99199 not compared
parse/pumpkin/small 12894 not compared
parse/simdnbt-borrow/byte-array 923 ⚪
parse/simdnbt-borrow/chunk 160454 ⚪
parse/simdnbt-borrow/int-array 1565272 ⚪
parse/simdnbt-borrow/long-array 3629016 ⚪
parse/simdnbt-borrow/long-names 119259 ⚪
parse/simdnbt-borrow/player 129964 ⚪
parse/simdnbt-borrow/short-list 1423400 ⚪
parse/simdnbt-borrow/short-names 114305 ⚪
parse/simdnbt-borrow/small 3118 ⚪
parse/simdnbt-owned/byte-array 501398 ⚪
parse/simdnbt-owned/chunk 386593 ⚪
parse/simdnbt-owned/int-array 1517847 ⚪
parse/simdnbt-owned/long-array 3534053 ⚪
parse/simdnbt-owned/long-names 116717 ⚪
parse/simdnbt-owned/player 261048 ⚪
parse/simdnbt-owned/short-list 2424104 ⚪
parse/simdnbt-owned/short-names 104521 ⚪
parse/simdnbt-owned/small 4852 ⚪

skip

Benchmark PR Change
skip/fastnbt/byte-array 718 ⚪
skip/fastnbt/byte-list 3800714 ⚪
skip/fastnbt/compound-list 11300714 ⚪
skip/fastnbt/double-list 3800714 ⚪
skip/fastnbt/float-list 3800714 ⚪
skip/fastnbt/int-array 719 ⚪
skip/fastnbt/int-list 3800714 ⚪
skip/fastnbt/long-array 718 ⚪
skip/fastnbt/long-list 3800714 ⚪
skip/fastnbt/short-list 3800714 ⚪
skip/fastnbt/string-list 4700714 ⚪
skip/nanonbt-derive/byte-array 562 ⚪
skip/nanonbt-derive/byte-list 585 ⚪
skip/nanonbt-derive/compound-list 11200597 ⚪
skip/nanonbt-derive/double-list 586 ⚪
skip/nanonbt-derive/float-list 586 ⚪
skip/nanonbt-derive/int-array 562 ⚪
skip/nanonbt-derive/int-list 586 ⚪
skip/nanonbt-derive/long-array 563 ⚪
skip/nanonbt-derive/long-list 586 ⚪
skip/nanonbt-derive/short-list 585 ⚪
skip/nanonbt-derive/string-list 4500597 ⚪
skip/nanonbt-serde/byte-array 911 ⚪
skip/nanonbt-serde/byte-list 934 ⚪
skip/nanonbt-serde/compound-list 11200946 ⚪
skip/nanonbt-serde/double-list 935 ⚪
skip/nanonbt-serde/float-list 935 ⚪
skip/nanonbt-serde/int-array 911 ⚪
skip/nanonbt-serde/int-list 935 ⚪
skip/nanonbt-serde/long-array 912 ⚪
skip/nanonbt-serde/long-list 935 ⚪
skip/nanonbt-serde/short-list 934 ⚪
skip/nanonbt-serde/string-list 4500946 ⚪
skip/pumpkin/byte-array 103203 not compared
skip/pumpkin/byte-list 14504198 not compared
skip/pumpkin/compound-list 169297262 not compared
skip/pumpkin/double-list 15563473 not compared
skip/pumpkin/float-list 15333639 not compared
skip/pumpkin/int-array 690357 not compared
skip/pumpkin/int-list 15333639 not compared
skip/pumpkin/long-array 1140349 not compared
skip/pumpkin/long-list 15563473 not compared
skip/pumpkin/short-list 15103798 not compared
skip/pumpkin/string-list 84420962 not compared
skip/simdnbt-borrow/byte-array 1275 ⚪
skip/simdnbt-borrow/byte-list 1314 ⚪
skip/simdnbt-borrow/compound-list 12327509 ⚪
skip/simdnbt-borrow/double-list 1012 ⚪
skip/simdnbt-borrow/float-list 1013 ⚪
skip/simdnbt-borrow/int-array 973 ⚪
skip/simdnbt-borrow/int-list 1013 ⚪
skip/simdnbt-borrow/long-array 974 ⚪
skip/simdnbt-borrow/long-list 1013 ⚪
skip/simdnbt-borrow/short-list 1013 ⚪
skip/simdnbt-borrow/string-list 5304899 ⚪
skip/simdnbt-owned/byte-array 102074 ⚪
skip/simdnbt-owned/byte-list 102190 ⚪
skip/simdnbt-owned/compound-list 90284306 ⚪
skip/simdnbt-owned/double-list 1439330 ⚪
skip/simdnbt-owned/float-list 670584 ⚪
skip/simdnbt-owned/int-array 670504 ⚪
skip/simdnbt-owned/int-list 670584 ⚪
skip/simdnbt-owned/long-array 1439247 ⚪
skip/simdnbt-owned/long-list 1439330 ⚪
skip/simdnbt-owned/short-list 286218 ⚪
skip/simdnbt-owned/string-list 44148872 ⚪

write

Benchmark PR Change
write/fastnbt-borrow/byte-array 1003512 ⚪
write/fastnbt-borrow/int-array 443463 ⚪
write/fastnbt-borrow/long-array 883596 ⚪
write/fastnbt/byte-array 613737 ⚪
write/fastnbt/chunk 379177 ⚪
write/fastnbt/int-array 1881540 ⚪
write/fastnbt/long-array 4259173 ⚪
write/fastnbt/long-names 79287 ⚪
write/fastnbt/player 303911 ⚪
write/fastnbt/short-list 10005788 ⚪
write/fastnbt/short-names 42349 ⚪
write/fastnbt/small 6975 ⚪
write/nanonbt-borrow-signed/byte-array 1001563 ⚪
write/nanonbt-borrow-signed/int-array 441660 ⚪
write/nanonbt-borrow-signed/long-array 881654 ⚪
write/nanonbt-borrow-signed/short-list 2001602 ⚪
write/nanonbt-borrow/byte-array 1001563 ⚪
write/nanonbt-borrow/chunk 79788 ⚪
write/nanonbt-borrow/int-array 441660 ⚪
write/nanonbt-borrow/long-array 881654 ⚪
write/nanonbt-borrow/player 94554 ⚪
write/nanonbt-borrow/short-list 2001602 ⚪
write/nanonbt-borrow/small 1822 ⚪
write/nanonbt-derive-signed/byte-array 502041 ⚪
write/nanonbt-derive-signed/int-array 6506011 ⚪
write/nanonbt-derive-signed/long-array 6737593 ⚪
write/nanonbt-derive-signed/short-list 6504516 ⚪
write/nanonbt-derive/byte-array 502096 ⚪
write/nanonbt-derive/chunk 130313 ⚪
write/nanonbt-derive/int-array 6005883 ⚪
write/nanonbt-derive/long-array 6237584 ⚪
write/nanonbt-derive/long-names 7228 ⚪
write/nanonbt-derive/player 100957 ⚪
write/nanonbt-derive/short-list 6504522 ⚪
write/nanonbt-derive/short-names 7171 ⚪
write/nanonbt-derive/small 2099 ⚪
write/nanonbt-serde/chunk 277222 ⚪
write/nanonbt-serde/long-names 57564 ⚪
write/nanonbt-serde/player 208681 ⚪
write/nanonbt-serde/short-names 30253 ⚪
write/nanonbt-serde/small 5111 ⚪
write/pumpkin/byte-array 1003849 not compared
write/pumpkin/chunk 652891 not compared
write/pumpkin/int-array 6727874 not compared
write/pumpkin/long-array 7951683 not compared
write/pumpkin/long-names 147692 not compared
write/pumpkin/player 522912 not compared
write/pumpkin/short-list 45772895 not compared
write/pumpkin/short-names 87361 not compared
write/pumpkin/small 10968 not compared
write/simdnbt-borrow/byte-array 1005501 ⚪
write/simdnbt-borrow/chunk 445626 ⚪
write/simdnbt-borrow/int-array 2102721 ⚪
write/simdnbt-borrow/long-array 5426853 ⚪
write/simdnbt-borrow/long-names 57581 ⚪
write/simdnbt-borrow/player 327339 ⚪
write/simdnbt-borrow/short-list 3428906 ⚪
write/simdnbt-borrow/short-names 46244 ⚪
write/simdnbt-borrow/small 7878 ⚪
write/simdnbt-owned/byte-array 1005597 ⚪
write/simdnbt-owned/chunk 443927 ⚪
write/simdnbt-owned/int-array 2102721 ⚪
write/simdnbt-owned/long-array 5426853 ⚪
write/simdnbt-owned/long-names 57693 ⚪
write/simdnbt-owned/player 359634 ⚪
write/simdnbt-owned/short-list 3428922 ⚪
write/simdnbt-owned/short-names 46708 ⚪
write/simdnbt-owned/small 8168 ⚪

linux-aarch64

Callgrind instruction counts: exact within this platform, so the two sides compare without a threshold; lower is better.

0 improved · 0 regressed · 173 unchanged of 173 entries compared — 29 reference.

All 202 entries

parse

Benchmark PR Change
parse/fastnbt-borrow/byte-array 472 ⚪
parse/fastnbt-borrow/int-array 507 ⚪
parse/fastnbt-borrow/long-array 509 ⚪
parse/fastnbt/byte-array 126909 ⚪
parse/fastnbt/chunk 324296 ⚪
parse/fastnbt/int-array 4887897 ⚪
parse/fastnbt/long-array 5269473 ⚪
parse/fastnbt/long-names 130831 ⚪
parse/fastnbt/player 144224 ⚪
parse/fastnbt/short-list 26571086 ⚪
parse/fastnbt/short-names 73130 ⚪
parse/fastnbt/small 2708 ⚪
parse/nanonbt-borrow-signed/byte-array 345 ⚪
parse/nanonbt-borrow-signed/int-array 348 ⚪
parse/nanonbt-borrow-signed/long-array 348 ⚪
parse/nanonbt-borrow-signed/short-list 343 ⚪
parse/nanonbt-borrow/byte-array 345 ⚪
parse/nanonbt-borrow/chunk 150909 ⚪
parse/nanonbt-borrow/int-array 348 ⚪
parse/nanonbt-borrow/long-array 348 ⚪
parse/nanonbt-borrow/player 146284 ⚪
parse/nanonbt-borrow/short-list 343 ⚪
parse/nanonbt-borrow/small 1738 ⚪
parse/nanonbt-derive-signed/byte-array 63474 ⚪
parse/nanonbt-derive-signed/int-array 500943 ⚪
parse/nanonbt-derive-signed/long-array 1000943 ⚪
parse/nanonbt-derive-signed/short-list 8070978 ⚪
parse/nanonbt-derive/byte-array 63449 ⚪
parse/nanonbt-derive/chunk 239923 ⚪
parse/nanonbt-derive/int-array 500928 ⚪
parse/nanonbt-derive/long-array 1000928 ⚪
parse/nanonbt-derive/long-names 42550 ⚪
parse/nanonbt-derive/player 157659 ⚪
parse/nanonbt-derive/short-list 8070961 ⚪
parse/nanonbt-derive/short-names 9009 ⚪
parse/nanonbt-derive/small 1936 ⚪
parse/nanonbt-serde/chunk 262625 ⚪
parse/nanonbt-serde/long-names 162022 ⚪
parse/nanonbt-serde/player 211611 ⚪
parse/nanonbt-serde/short-names 75065 ⚪
parse/nanonbt-serde/small 3349 ⚪
parse/pumpkin/byte-array 127838 not compared
parse/pumpkin/chunk 701858 not compared
parse/pumpkin/int-array 940311 not compared
parse/pumpkin/long-array 1877849 not compared
parse/pumpkin/long-names 125054 not compared
parse/pumpkin/player 502367 not compared
parse/pumpkin/short-list 77120271 not compared
parse/pumpkin/short-names 90452 not compared
parse/pumpkin/small 12052 not compared
parse/simdnbt-borrow/byte-array 981 ⚪
parse/simdnbt-borrow/chunk 133567 ⚪
parse/simdnbt-borrow/int-array 626614 ⚪
parse/simdnbt-borrow/long-array 1251611 ⚪
parse/simdnbt-borrow/long-names 164161 ⚪
parse/simdnbt-borrow/player 121702 ⚪
parse/simdnbt-borrow/short-list 282911 ⚪
parse/simdnbt-borrow/short-names 104233 ⚪
parse/simdnbt-borrow/small 2990 ⚪
parse/simdnbt-owned/byte-array 63451 ⚪
parse/simdnbt-owned/chunk 374501 ⚪
parse/simdnbt-owned/int-array 626626 ⚪
parse/simdnbt-owned/long-array 1250986 ⚪
parse/simdnbt-owned/long-names 167595 ⚪
parse/simdnbt-owned/player 272616 ⚪
parse/simdnbt-owned/short-list 282300 ⚪
parse/simdnbt-owned/short-names 104706 ⚪
parse/simdnbt-owned/small 5022 ⚪

skip

Benchmark PR Change
skip/fastnbt/byte-array 677 ⚪
skip/fastnbt/byte-list 3900665 ⚪
skip/fastnbt/compound-list 10500665 ⚪
skip/fastnbt/double-list 4000665 ⚪
skip/fastnbt/float-list 4000665 ⚪
skip/fastnbt/int-array 679 ⚪
skip/fastnbt/int-list 4000665 ⚪
skip/fastnbt/long-array 678 ⚪
skip/fastnbt/long-list 4000665 ⚪
skip/fastnbt/short-list 3900665 ⚪
skip/fastnbt/string-list 4500665 ⚪
skip/nanonbt-derive/byte-array 505 ⚪
skip/nanonbt-derive/byte-list 520 ⚪
skip/nanonbt-derive/compound-list 11000539 ⚪
skip/nanonbt-derive/double-list 525 ⚪
skip/nanonbt-derive/float-list 523 ⚪
skip/nanonbt-derive/int-array 504 ⚪
skip/nanonbt-derive/int-list 525 ⚪
skip/nanonbt-derive/long-array 505 ⚪
skip/nanonbt-derive/long-list 521 ⚪
skip/nanonbt-derive/short-list 523 ⚪
skip/nanonbt-derive/string-list 4300539 ⚪
skip/nanonbt-serde/byte-array 793 ⚪
skip/nanonbt-serde/byte-list 809 ⚪
skip/nanonbt-serde/compound-list 11000828 ⚪
skip/nanonbt-serde/double-list 814 ⚪
skip/nanonbt-serde/float-list 812 ⚪
skip/nanonbt-serde/int-array 793 ⚪
skip/nanonbt-serde/int-list 814 ⚪
skip/nanonbt-serde/long-array 793 ⚪
skip/nanonbt-serde/long-list 810 ⚪
skip/nanonbt-serde/short-list 812 ⚪
skip/nanonbt-serde/string-list 4400828 ⚪
skip/pumpkin/byte-array 15598 not compared
skip/pumpkin/byte-list 14104236 not compared
skip/pumpkin/compound-list 162658052 not compared
skip/pumpkin/double-list 14562163 not compared
skip/pumpkin/float-list 14533005 not compared
skip/pumpkin/int-array 140321 not compared
skip/pumpkin/int-list 14633005 not compared
skip/pumpkin/long-array 277821 not compared
skip/pumpkin/long-list 14662163 not compared
skip/pumpkin/short-list 14603867 not compared
skip/pumpkin/string-list 81865293 not compared
skip/simdnbt-borrow/byte-array 1293 ⚪
skip/simdnbt-borrow/byte-list 1332 ⚪
skip/simdnbt-borrow/compound-list 10769087 ⚪
skip/simdnbt-borrow/double-list 1069 ⚪
skip/simdnbt-borrow/float-list 1069 ⚪
skip/simdnbt-borrow/int-array 1030 ⚪
skip/simdnbt-borrow/int-list 1068 ⚪
skip/simdnbt-borrow/long-array 1030 ⚪
skip/simdnbt-borrow/long-list 1068 ⚪
skip/simdnbt-borrow/short-list 1068 ⚪
skip/simdnbt-borrow/string-list 5004960 ⚪
skip/simdnbt-owned/byte-array 13837 ⚪
skip/simdnbt-owned/byte-list 13923 ⚪
skip/simdnbt-owned/compound-list 63051926 ⚪
skip/simdnbt-owned/double-list 251209 ⚪
skip/simdnbt-owned/float-list 126212 ⚪
skip/simdnbt-owned/int-array 126158 ⚪
skip/simdnbt-owned/int-list 126212 ⚪
skip/simdnbt-owned/long-array 251156 ⚪
skip/simdnbt-owned/long-list 251209 ⚪
skip/simdnbt-owned/short-list 57470 ⚪
skip/simdnbt-owned/string-list 31706370 ⚪

write

Benchmark PR Change
write/fastnbt-borrow/byte-array 65217 ⚪
write/fastnbt-borrow/int-array 252631 ⚪
write/fastnbt-borrow/long-array 502780 ⚪
write/fastnbt/byte-array 160260 ⚪
write/fastnbt/chunk 287119 ⚪
write/fastnbt/int-array 1003930 ⚪
write/fastnbt/long-array 2004079 ⚪
write/fastnbt/long-names 78223 ⚪
write/fastnbt/player 282394 ⚪
write/fastnbt/short-list 8505916 ⚪
write/fastnbt/short-names 41569 ⚪
write/fastnbt/small 6712 ⚪
write/nanonbt-borrow-signed/byte-array 126104 ⚪
write/nanonbt-borrow-signed/int-array 501116 ⚪
write/nanonbt-borrow-signed/long-array 1001116 ⚪
write/nanonbt-borrow-signed/short-list 287 ⚪
write/nanonbt-borrow/byte-array 126104 ⚪
write/nanonbt-borrow/chunk 8336 ⚪
write/nanonbt-borrow/int-array 501116 ⚪
write/nanonbt-borrow/long-array 1001116 ⚪
write/nanonbt-borrow/player 103941 ⚪
write/nanonbt-borrow/short-list 287 ⚪
write/nanonbt-borrow/small 289 ⚪
write/nanonbt-derive-signed/byte-array 64081 ⚪
write/nanonbt-derive-signed/int-array 6506243 ⚪
write/nanonbt-derive-signed/long-array 6769185 ⚪
write/nanonbt-derive-signed/short-list 6504704 ⚪
write/nanonbt-derive/byte-array 64756 ⚪
write/nanonbt-derive/chunk 134279 ⚪
write/nanonbt-derive/int-array 6006132 ⚪
write/nanonbt-derive/long-array 6269173 ⚪
write/nanonbt-derive/long-names 7622 ⚪
write/nanonbt-derive/player 111311 ⚪
write/nanonbt-derive/short-list 6504710 ⚪
write/nanonbt-derive/short-names 7551 ⚪
write/nanonbt-derive/small 2203 ⚪
write/nanonbt-serde/chunk 251660 ⚪
write/nanonbt-serde/long-names 58765 ⚪
write/nanonbt-serde/player 213308 ⚪
write/nanonbt-serde/short-names 31737 ⚪
write/nanonbt-serde/small 5155 ⚪
write/pumpkin/byte-array 129029 not compared
write/pumpkin/chunk 608444 not compared
write/pumpkin/int-array 250223 not compared
write/pumpkin/long-array 500223 not compared
write/pumpkin/long-names 1009 not compared
write/pumpkin/player 224369 not compared
write/pumpkin/short-list 42044096 not compared
write/pumpkin/short-names 773 not compared
write/pumpkin/small 9840 not compared
write/simdnbt-borrow/byte-array 130979 ⚪
write/simdnbt-borrow/chunk 457398 ⚪
write/simdnbt-borrow/int-array 1201839 ⚪
write/simdnbt-borrow/long-array 2613501 ⚪
write/simdnbt-borrow/long-names 60797 ⚪
write/simdnbt-borrow/player 354473 ⚪
write/simdnbt-borrow/short-list 538800 ⚪
write/simdnbt-borrow/short-names 51101 ⚪
write/simdnbt-borrow/small 8626 ⚪
write/simdnbt-owned/byte-array 130979 ⚪
write/simdnbt-owned/chunk 456853 ⚪
write/simdnbt-owned/int-array 1201839 ⚪
write/simdnbt-owned/long-array 2613501 ⚪
write/simdnbt-owned/long-names 60908 ⚪
write/simdnbt-owned/player 382038 ⚪
write/simdnbt-owned/short-list 538800 ⚪
write/simdnbt-owned/short-names 51476 ⚪
write/simdnbt-owned/small 8932 ⚪

wasm32-wasip1

Wasmi fuel: the wasm instructions each entry runs, exact within this platform, so the two sides compare without a threshold; lower is better.

0 improved · 0 regressed · 115 unchanged of 115 entries compared — 29 reference.

All 144 entries

parse

Benchmark PR Change
parse/fastnbt-borrow/byte-array 32773 ⚪
parse/fastnbt-borrow/int-array 34157 ⚪
parse/fastnbt-borrow/long-array 34087 ⚪
parse/fastnbt/byte-array 30642 ⚪
parse/fastnbt/chunk 1378742 ⚪
parse/fastnbt/int-array 10103813 ⚪
parse/fastnbt/long-array 17684660 ⚪
parse/fastnbt/long-names 542593 ⚪
parse/fastnbt/player 1029436 ⚪
parse/fastnbt/short-list 212021245 ⚪
parse/fastnbt/short-names 390873 ⚪
parse/fastnbt/small 134379 ⚪
parse/nanonbt-borrow-signed/byte-array 2011 ⚪
parse/nanonbt-borrow-signed/int-array 2023 ⚪
parse/nanonbt-borrow-signed/long-array 2023 ⚪
parse/nanonbt-borrow-signed/short-list 2031 ⚪
parse/nanonbt-borrow/byte-array 8318 ⚪
parse/nanonbt-borrow/chunk 448692 ⚪
parse/nanonbt-borrow/int-array 8610 ⚪
parse/nanonbt-borrow/long-array 8610 ⚪
parse/nanonbt-borrow/player 660513 ⚪
parse/nanonbt-borrow/short-list 8618 ⚪
parse/nanonbt-borrow/small 24238 ⚪
parse/nanonbt-derive-signed/byte-array 17487 ⚪
parse/nanonbt-derive-signed/int-array 5509741 ⚪
parse/nanonbt-derive-signed/long-array 13009741 ⚪
parse/nanonbt-derive-signed/short-list 18017254 ⚪
parse/nanonbt-derive/byte-array 20076 ⚪
parse/nanonbt-derive/chunk 750425 ⚪
parse/nanonbt-derive/int-array 5513917 ⚪
parse/nanonbt-derive/long-array 13016045 ⚪
parse/nanonbt-derive/long-names 182240 ⚪
parse/nanonbt-derive/player 746103 ⚪
parse/nanonbt-derive/short-list 18521368 ⚪
parse/nanonbt-derive/short-names 127547 ⚪
parse/nanonbt-derive/small 33330 ⚪
parse/nanonbt-serde/chunk 1344603 ⚪
parse/nanonbt-serde/long-names 568370 ⚪
parse/nanonbt-serde/player 1298038 ⚪
parse/nanonbt-serde/short-names 372746 ⚪
parse/nanonbt-serde/small 145512 ⚪
parse/pumpkin/byte-array 25907 not compared
parse/pumpkin/chunk 2012536 not compared
parse/pumpkin/int-array 5135598 not compared
parse/pumpkin/long-array 12260598 not compared
parse/pumpkin/long-names 353635 not compared
parse/pumpkin/player 1548855 not compared
parse/pumpkin/short-list 395690384 not compared
parse/pumpkin/short-names 266592 not compared
parse/pumpkin/small 140343 not compared

skip

Benchmark PR Change
skip/fastnbt/byte-array 5665 ⚪
skip/fastnbt/byte-list 24622734 ⚪
skip/fastnbt/compound-list 65303309 ⚪
skip/fastnbt/double-list 26803309 ⚪
skip/fastnbt/float-list 25203309 ⚪
skip/fastnbt/int-array 3334 ⚪
skip/fastnbt/int-list 25203309 ⚪
skip/fastnbt/long-array 3334 ⚪
skip/fastnbt/long-list 26803309 ⚪
skip/fastnbt/short-list 24903309 ⚪
skip/fastnbt/string-list 27303309 ⚪
skip/nanonbt-derive/byte-array 2764 ⚪
skip/nanonbt-derive/byte-list 24255 ⚪
skip/nanonbt-derive/compound-list 64402810 ⚪
skip/nanonbt-derive/double-list 2821 ⚪
skip/nanonbt-derive/float-list 2821 ⚪
skip/nanonbt-derive/int-array 2764 ⚪
skip/nanonbt-derive/int-list 5180 ⚪
skip/nanonbt-derive/long-array 2764 ⚪
skip/nanonbt-derive/long-list 5180 ⚪
skip/nanonbt-derive/short-list 4901 ⚪
skip/nanonbt-derive/string-list 28405085 ⚪
skip/nanonbt-serde/byte-array 4183 ⚪
skip/nanonbt-serde/byte-list 25527 ⚪
skip/nanonbt-serde/compound-list 65204229 ⚪
skip/nanonbt-serde/double-list 4240 ⚪
skip/nanonbt-serde/float-list 4240 ⚪
skip/nanonbt-serde/int-array 4183 ⚪
skip/nanonbt-serde/int-list 6599 ⚪
skip/nanonbt-serde/long-array 4183 ⚪
skip/nanonbt-serde/long-list 6599 ⚪
skip/nanonbt-serde/short-list 6320 ⚪
skip/nanonbt-serde/string-list 28806504 ⚪
skip/pumpkin/byte-array 12000 not compared
skip/pumpkin/byte-list 77754828 not compared
skip/pumpkin/compound-list 493156421 not compared
skip/pumpkin/double-list 78298827 not compared
skip/pumpkin/float-list 78330294 not compared
skip/pumpkin/int-array 1029230 not compared
skip/pumpkin/int-list 78330294 not compared
skip/pumpkin/long-array 2447980 not compared
skip/pumpkin/long-list 78298827 not compared
skip/pumpkin/short-list 78346401 not compared
skip/pumpkin/string-list 227303302 not compared

write

Benchmark PR Change
write/fastnbt-borrow/byte-array 40134 ⚪
write/fastnbt-borrow/int-array 58956 ⚪
write/fastnbt-borrow/long-array 94442 ⚪
write/fastnbt/byte-array 2396994 ⚪
write/fastnbt/chunk 949896 ⚪
write/fastnbt/int-array 5545615 ⚪
write/fastnbt/long-array 13076894 ⚪
write/fastnbt/long-names 201612 ⚪
write/fastnbt/player 880016 ⚪
write/fastnbt/short-list 10521008 ⚪
write/fastnbt/short-names 148354 ⚪
write/fastnbt/small 77763 ⚪
write/nanonbt-borrow-signed/byte-array 16941 ⚪
write/nanonbt-borrow-signed/int-array 40394 ⚪
write/nanonbt-borrow-signed/long-array 71644 ⚪
write/nanonbt-borrow-signed/short-list 25184 ⚪
write/nanonbt-borrow/byte-array 16927 ⚪
write/nanonbt-borrow/chunk 118183 ⚪
write/nanonbt-borrow/int-array 40401 ⚪
write/nanonbt-borrow/long-array 71644 ⚪
write/nanonbt-borrow/player 222285 ⚪
write/nanonbt-borrow/short-list 25191 ⚪
write/nanonbt-borrow/small 14137 ⚪
write/nanonbt-derive-signed/byte-array 16709 ⚪
write/nanonbt-derive-signed/int-array 17511735 ⚪
write/nanonbt-derive-signed/long-array 25011996 ⚪
write/nanonbt-derive-signed/short-list 16513891 ⚪
write/nanonbt-derive/byte-array 13315 ⚪
write/nanonbt-derive/chunk 218550 ⚪
write/nanonbt-derive/int-array 18009416 ⚪
write/nanonbt-derive/long-array 25509677 ⚪
write/nanonbt-derive/long-names 13219 ⚪
write/nanonbt-derive/player 198734 ⚪
write/nanonbt-derive/short-list 17009172 ⚪
write/nanonbt-derive/short-names 12897 ⚪
write/nanonbt-derive/small 5383 ⚪
write/nanonbt-serde/chunk 572118 ⚪
write/nanonbt-serde/long-names 162992 ⚪
write/nanonbt-serde/player 672720 ⚪
write/nanonbt-serde/short-names 127022 ⚪
write/nanonbt-serde/small 56883 ⚪
write/pumpkin/byte-array 27583 not compared
write/pumpkin/chunk 1378830 not compared
write/pumpkin/int-array 11547427 not compared
write/pumpkin/long-array 19078953 not compared
write/pumpkin/long-names 265497 not compared
write/pumpkin/player 1156058 not compared
write/pumpkin/short-list 225892785 not compared
write/pumpkin/short-names 190211 not compared
write/pumpkin/small 78337 not compared

iai-callgrind counts instructions under valgrind, so the same code counts the same instructions on every machine and every run; wasmi fuel is exact the same way, and the comparison needs no noise threshold and no repeated passes.

The `Bench` workflow needs to say what a pull request changed, not just
that the bench ran. The new `bench-summary` example walks criterion's
output tree and reads each entry's `new` estimates, the saved `main`
baseline and the `change` between them into a Markdown summary: the
counts, the changes that cleared criterion's noise threshold, and every
entry under a collapsed table.

An entry counts as improved or regressed by the same rule criterion's
own report uses, the 95% confidence interval of the mean change clearing
the threshold on one side; the saved files carry no p-value, so the
interval alone decides.

The tool is an example rather than a `src/bin` target: a bin target
trips Cargo's unused-dependencies lint for the optional comparison
dependencies, which only the bench targets use.
The bench is numbers; without a baseline they mean nothing across
runners. The new workflow benches the base commit with `--save-baseline
main` and the pull request with `--baseline-lenient main`, back to back
on one runner, so the percentages describe one machine at one moment.
The base pass's `new/` numbers are cleared before the second pass, which
leaves each side in its own directory and keeps an entry the pull
request removed visible to the summary; lenient comparison keeps an
entry it added from failing the pass instead of joining the baseline.

The summary the `bench-summary` example renders is posted as a sticky
comment. A pull request from a fork, or one Dependabot opened, runs with
a read-only token, so that step is skipped and its numbers stay in the
log. The job sits outside the required `ci` workflow: a shared runner is
noisy, and two full passes are not a check to gate on.
The two passes run in different processes, and randomization laid their
input buffers out differently, which moves an entry by tens of percent
where its walk is sensitive to the placement. The first run showed it:
the pull request side of `skip/fastnbt/double-list` measured 55% slower
while `skip/fastnbt/int-list`, benchmarked in the same minutes, matched
the base pass's iteration count exactly, and the same document moved
0-3% on three other targets. The two revisions differ by a doc comment,
so none of it was the code.

`setarch -R` disables the randomization: both passes place the same
buffers the same way, and the percentages describe the code. Noise from
the shared host between the passes remains; the comment keeps its
caveat.
One pair of passes could not tell a change in the code from a change in
the host. The same comparison was run twice on the same two revisions
and one run called 32 entries improved and 43 regressed where the other
called 63 and 42; the head binary itself differed by up to 42% between
the two runs. Criterion's confidence interval is a within-run one, so no
amount of it can rule that out.

The tool now reads the `base` and `head` estimates of two jobs — one
benching the base first, one benching the pull request first — and calls
an entry improved or regressed only when both jobs clear criterion's ±1%
noise threshold in the same direction, at the less extreme of the two
percentages. A pair that disagrees is called unstable instead, and both
jobs' numbers are always shown side by side. The mirrored order is what
gives the agreement its force: anything that follows the pass order
moves the two jobs' changes in opposite directions, while only a change
in the code moves both the same way.

The tool no longer reads criterion's `change` directories, only the
named baselines, so the workflow no longer needs `--baseline-lenient`.
The single pair of back-to-back passes was still at the mercy of the
host: the passes are different processes minutes apart, and the same
code moved by tens of percent between runs of the same comparison. Both
sides are now benched in each of two parallel jobs, in opposite orders:
`forward` benches the base commit first, `backward` the pull request.
Every number a job reports is still set against another pass on the same
runner; only the comparison crosses runners, and the summary keeps it
only where the two agree.

Each job passes `--save-baseline base` then `--save-baseline head`, so
the passes no longer compare and the tool reads the two named
baselines. That drops the step clearing the base pass's `new/` numbers
and the lenient comparison. Each job uploads its criterion tree; a third
job downloads both, runs the summary and posts the comment, so the
`pull-requests: write` permission moves to it alone.
Criterion measures wall time, which moves with the machine and the run.
The workflow tried to cancel that with two mirrored passes and a
within-run confidence interval; neither sees the host itself, and the
same comparison still disagreed with itself by tens of percent.

iai-callgrind counts instructions under callgrind instead. The count
does not depend on the machine or the run, so one pass per side compares
as exact numbers, and the two jobs and their artifacts go away with it.
The `compare` suite is now one `#[library_benchmark]` per entry, named
`<kind>_<target>_<id>`, with the document bytes and the write setup
hoisted out of the measured function; `--cache-sim=no` keeps the
instruction count while skipping the cache simulation the report never
reads.

The workflow benches both revisions in one job: it checks the base
commit out with the pull request's bench crate laid over it, runs the
suite with `--save-summary=json`, moves the summaries aside, benches the
head and hands both trees to the rewritten `bench-summary` example. Its
comment pairs each entry by id and leads with improved, regressed and
unchanged; `pumpkin` entries are listed for reference only, because
their compounds are `std` hash maps whose serialization order follows
the process's random seed, which moved their counts by up to nine
percent across thirty runs of the same binary.

The runner comes from `taiki-e/install-action` at the version the
bench's lock file pins and valgrind from apt; both sides run under that
same pair, and the runner disables ASLR itself, so the `setarch`
wrappers go away too.
@topi-banana
topi-banana force-pushed the ci/bench-pr-result-comment branch from 8486bdb to 20f5368 Compare September 28, 2026 13:59
Callgrind only runs where valgrind does, Linux x86_64 and aarch64. The
same four target files now also compile into the crate library, against
a runtime registry in place of the iai macros, so the two harnesses that
cover the other platforms can walk the same entries: the wasmi fuel
runner on wasm32 and the smoke runner on Windows and macOS. Both
backends name every entry `<kind>_<target>_<id>`, and the shared
`report::display_id` turns those names into the report id a comparison
pairs by.
Run callgrind on Linux x86_64 and aarch64, wasmi fuel on wasm32, and a
smoke run of every entry on Windows and macOS, then render all of them
into one sticky comment with a section per platform and exact counts
within each. A platform that failed to produce results reads as `did not
run` instead of blocking the others, and the tables drop to a compact
form if the comment would outgrow GitHub's size limit. RISC-V gets its
own dispatch-only workflow, since it has no hosted runner and needs QEMU
and valgrind 3.25 or newer.
The smoke platforms run every entry but count nothing, so the intro's
"every platform counts its own base and pull request" overstated what
Windows and macOS do.
@topi-banana
topi-banana force-pushed the ci/bench-pr-result-comment branch from a488dec to a21b19a Compare September 29, 2026 08:57
`--locked` makes cargo fail when the lock does not match the manifests,
but the base commit's manifest decides its own version and dependencies,
which a pull request may change; a version bump or a dependency change
made the base bench, and with it the whole platform, fail before it ran.
A stale lock re-resolves only as far as the base library needs and keeps
every other pin, the comparison targets above all, and the pull request's
checkout restores the lock before the head runs, which stay `--locked`.
@topi-banana
topi-banana merged commit bb263fb into main Sep 30, 2026
23 checks passed
@topi-banana
topi-banana deleted the ci/bench-pr-result-comment branch September 30, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant