Skip to content

bench(quic): Criterion sweeps of streams, receive, routing, and wire - #5812

Merged
kixelated merged 6 commits into
mainfrom
quest/m1/quic/coverage/bench
Oct 17, 2026
Merged

kixelated merged 6 commits into
mainfrom
quest/m1/quic/coverage/bench

Conversation

@kixelated

@kixelated kixelated commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Closes the quest/m1/quic/coverage/bench.md quest: the crate-level benchmarks for moq-quic, so the open rewrites (#5711 scheduler, #5735 receive memory, #5674 peer limits) can run just bench <base> against a fixed suite.

What lands

Four Criterion targets under rs/moq-quic/benches/. Each case sweeps two axes, so a cost that grows with the table rather than the touched path shows up as a slope.

group axes measures
send_priority streams {1,16,256,1024} x priorities {1,8,64} open, write, finish, schedule, packetize, receive, and ACK fresh uni streams
send_write_size streams {1,64} x bytes per write {64,1024,16384} the same with 64 KiB per stream, split into writes
receive_loss streams {1,16,256} x packets per stream {2,16} server-side reassembly with every 8th client datagram dropped
receive_reset streams {16,256,1024} x bytes before reset {0,16384} RESET_STREAM fan-out on both ends
receive_stop streams {16,256,1024} x bytes {1100,16384} STOP_SENDING fan-out, through the sender's reset
endpoint_route connections {16,256,4096} x CIDs per connection {2,5} Endpoint::handle dispatch of 1024 short header datagrams
wire_encode DATAGRAM frames per packet {1,16} x packets {64,1024} poll_transmit packet building
wire_decode frames {1,16} x packets {64,1024} forged packet ingest and frame decode
wire_ack packets in flight {256,2048} x ACK ranges {1,16,128} ACK processing and loss detection

The harness (benches/common/mod.rs) drives a sans-IO Pair through the public Endpoint/Connection API only, so the suite compiles on both sides of a rewrite. It is bench-local and does not touch src/tests.

  • Packets are unprotected (ported from quinn's perf noprotection.rs) and the congestion window is unlimited, so a case measures protocol CPU, not AES or a controller model.
  • Time is virtual and only jumps to the next timer when no datagram moved.
  • Endpoint RNG seeds are fixed, so packet number skips and key update timing repeat on every run and on both sides of a comparison.
  • Cases are timed in thread CPU time (a custom Criterion Measurement over cpu-time), so a host that deschedules the benchmark does not inflate it.
  • The wire cases forge 1-RTT packets directly and decode server packet numbers per RFC 9000 A.3, timing only one side with iter_custom.

sh/gh/nightly.sh adds -p moq-quic --bench streams --bench receive --bench endpoint --bench wire to the existing cargo bench ... -- --test smoke, so every case runs once nightly. just bench picks the targets up automatically.

Public API and wire impact

None. Dev-only: new [[bench]] targets, plus criterion and rustls (std, ring) dev-dependencies. No exported items or visibility changed.

Merged-first comparison for #5688 and #5690

Both merged before the suite existed, so the suite was copied onto each side: b302891 vs d0c4931 for #5688 (shard), and d0c4931 vs 38436f2 for #5690 (reliable reset). Each side was built in its own target dir and run pinned to one P-core (taskset -c 3), with --warm-up-time 1 --measurement-time 2 --sample-size 30.

Result: neither merge shows a change outside this host's A/A noise. The A/A columns are identical binaries run back to back. The host was at load average 25 on 24 cores, with other builds running. Small cases repeat within about 5%. Cases with large working sets (1024 streams, 64 x 1 KiB writes, 4096 connections) swing 20-130%, and sometimes jump between two modes. The sharpest outlier, #5690 wire_encode/frames_16/packets_1024 at -43%, did not reproduce when run alone: d0c4931 measured 527-536 µs and 38436f2 measured 512-561 µs.

Median change per case (parentheses: Criterion saw no significant change)
case A/A 1 A/A 2 A/A 3 #5688 #5690
endpoint_route/connections_16/cids_2 +56% -11% +9% -8% (+0%)
endpoint_route/connections_16/cids_5 (+8%) -7% +11% -20% -10%
endpoint_route/connections_256/cids_2 +60% -18% (+1%) -20% -19%
endpoint_route/connections_256/cids_5 (-8%) (+0%) +18% (+4%) (+14%)
endpoint_route/connections_4096/cids_2 +22% +24% +18% -7% -66%
endpoint_route/connections_4096/cids_5 +131% (-0%) -11% +67% -61%
receive_loss/streams_1/packets_2 (+1%) (+1%) (+0%) (+1%) (+1%)
receive_loss/streams_1/packets_16 +4% (+1%) (-0%) -4% +3%
receive_loss/streams_16/packets_2 (-0%) (+0%) -3% -3% +6%
receive_loss/streams_16/packets_16 +2% (+1%) -5% +5% +4%
receive_loss/streams_256/packets_2 -3% (-1%) -3% (-2%) -5%
receive_loss/streams_256/packets_16 -9% -8% -22% (-2%) -39%
receive_reset/streams_16/bytes_0 -2% -4% -8% (+0%) -7%
receive_reset/streams_16/bytes_16384 (+0%) -6% -4% (+1%) -5%
receive_reset/streams_256/bytes_0 (+1%) -9% -6% -3% +3%
receive_reset/streams_256/bytes_16384 -13% -33% -23% +14% -33%
receive_reset/streams_1024/bytes_0 (+1%) -6% -6% (-2%) +2%
receive_reset/streams_1024/bytes_16384 (+1%) -19% -22% -8% -18%
receive_stop/streams_16/bytes_1100 -2% -6% -3% -2% (+1%)
receive_stop/streams_16/bytes_16384 (-1%) -4% -9% (+1%) (+0%)
receive_stop/streams_256/bytes_1100 -2% (-0%) -7% (-1%) -8%
receive_stop/streams_256/bytes_16384 -22% +6% -34% -10% -14%
receive_stop/streams_1024/bytes_1100 +4% (+1%) -13% (-1%) (-1%)
receive_stop/streams_1024/bytes_16384 (+2%) (-2%) -17% +5% -8%
send_priority/streams_1/priorities_1 -3% -3% -6% (-2%) (-1%)
send_priority/streams_16/priorities_1 (+1%) +2% -6% +2% +7%
send_priority/streams_16/priorities_8 (+1%) (-2%) -9% (-0%) -4%
send_priority/streams_256/priorities_1 +6% +7% -6% +8% -17%
send_priority/streams_256/priorities_8 +3% (+1%) -11% (-4%) -11%
send_priority/streams_256/priorities_64 +3% (+1%) -15% -9% -7%
send_priority/streams_1024/priorities_1 (-5%) +23% -26% +19% -23%
send_priority/streams_1024/priorities_8 +14% (+6%) -38% +23% -8%
send_priority/streams_1024/priorities_64 -13% +35% (-7%) +55% (+3%)
send_write_size/streams_1/bytes_64 (-0%) (+0%) -2% (+0%) -2%
send_write_size/streams_1/bytes_1024 (+1%) +2% +4% (+0%) (-1%)
send_write_size/streams_1/bytes_16384 +2% +5% (-1%) +6% +3%
send_write_size/streams_64/bytes_64 +22% +4% -9% +10% (+2%)
send_write_size/streams_64/bytes_1024 +42% +28% -18% +45% +21%
send_write_size/streams_64/bytes_16384 -11% +32% -10% +46% +15%
wire_encode/frames_1/packets_64 +3% (+0%) -5% -3% +3%
wire_encode/frames_1/packets_1024 (+1%) (-2%) -7% (-1%) (+0%)
wire_encode/frames_16/packets_64 (+1%) (-2%) -6% (+0%) (+1%)
wire_encode/frames_16/packets_1024 +2% -6% -5% (+0%) -43%
wire_decode/frames_1/packets_64 +5% (-0%) (-0%) (-1%) +5%
wire_decode/frames_1/packets_1024 +7% (-3%) +5% -3% +7%
wire_decode/frames_16/packets_64 (+4%) +7% +9% (+0%) +17%
wire_decode/frames_16/packets_1024 (+3%) +5% +12% (-3%) +18%
wire_ack/packets_256/ranges_1 -2% (-2%) -4% +3% +4%
wire_ack/packets_256/ranges_16 (-3%) +3% +6% +4% (+1%)
wire_ack/packets_256/ranges_128 (+0%) -5% -2% (+1%) (+1%)
wire_ack/packets_2048/ranges_1 (+2%) -7% (-2%) +19% -16%
wire_ack/packets_2048/ranges_16 +29% +14% +3% +5% -2%
wire_ack/packets_2048/ranges_128 -3% (+3%) (-2%) +8% (-1%)

A/A 1 is main with this suite. A/A 2 and 3 are the b302891 binary against itself.

What it takes to get a usable signal on this host:

  • Wall time was unusable. A/A runs swung 50-250%, which is why the suite measures thread CPU time.
  • Pin to one core type. The host is a hybrid Intel part (8 P-cores, 16 E-cores). Unpinned, the same case measured 420 µs or 690 µs depending on where it was scheduled.
  • Use one target dir per checkout. With a shared CARGO_TARGET_DIR, cargo did not rebuild when switching checkouts, and every pass silently ran the first binary. That was the cause of an earlier "-18% to -55% routing win for feat(quic)!: shard the relay's QUIC endpoint per core by default #5688", which was not real.

Pitfalls the harness pins

  • Reordering a full flight trips KEY_UPDATE_ERROR: the random key update plus a truncated packet number decodes wrong. Loss uses periodic drops instead.
  • STOP_SENDING does not reset the stream by itself. The harness's app resets on StreamEvent::Stopped, as a real app must.
  • min_mtu is pinned to 1452, so black hole detection cannot shrink packets after forged losses.
  • Iterations end by advancing virtual time past two PTOs. Without that, lost_packets piled up and later iterations got slower.

Checks

just check passes. All 53 cases pass cargo bench -p moq-quic --bench ... -- --test, and sh/gh/nightly.test.py passes.

Follow-ups

  • Run rewrite comparisons on a quiet, pinned host or a dedicated runner. On a shared host, only differences well outside the A/A columns above count.
  • Consider having bench/run.sh pin itself to one core type on hybrid CPUs. It already builds each side in its own target dir.

(Written by Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 5 commits October 16, 2026 17:18
Criterion benchmarks for moq-quic, so each QUIC rewrite can compare
against the code it replaced. A bench-local harness drives a sans-IO
client/server pair through the public Connection/Endpoint API only, with
no-op packet protection, an unlimited congestion window, and virtual
time, so the suite measures protocol CPU and builds on both sides of a
rewrite. Every case sweeps two axes, so a cost that grows with a table
shows as a slope. The nightly Rust smoke runs each case once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Elapsed time on a loaded host swung unrelated cases by 50-250% between
identical runs. Thread CPU time drops descheduling, and fixed endpoint
seeds make packet number skips and key update timing repeat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Quest outcome for quest/m1/quic/coverage/bench.md:

  • Landed: 9 Criterion groups (53 cases) across streams, receive, endpoint, and wire. Each one sweeps two axes, runs through the public API only, and is timed in thread CPU time with fixed seeds. Every case also runs once in the nightly -- --test smoke.
  • Merged-first comparison: neither feat(quic)!: shard the relay's QUIC endpoint per core by default #5688 nor feat(quic)!: reliable reset from a send-stream checkpoint #5690 moves any case outside the A/A noise on this shared host. The tables are in the description.
  • Open decisions:
    • Whether to keep the thread CPU measurement. I recommend keeping it, since wall time swung 50-250% between identical runs here.
    • Whether to teach bench/run.sh to pin to one core type. I recommend it as a separate small PR.
  • Not done: rewrite comparisons on a quiet host. Each rewrite PR should run just bench <base> on a pinned, idle machine.

(Written by Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 17, 2026 00:54
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T00:58:03.394725Z b7c6698 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 31 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: eeed39e7-056d-4137-8851-63777e86cb7c
📥 Commits

Reviewing files that changed from the base of the PR and between b7d7e99 and b7c6698.

⛔ Files ignored due to path filters: 1
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing: 10
  • quest/m1/quic/coverage/README.md
  • quest/m1/quic/coverage/bench.md
  • rs/moq-quic/Cargo.toml
  • rs/moq-quic/benches/common/mod.rs
  • rs/moq-quic/benches/endpoint.rs
  • rs/moq-quic/benches/receive.rs
  • rs/moq-quic/benches/streams.rs
  • rs/moq-quic/benches/wire.rs
  • sh/gh/nightly.sh
  • sh/gh/nightly.test.py
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merging. Summary:

Codex review is clean on b7c6698, and CI passes.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit 3789ee2 Oct 17, 2026
14 checks passed
@kixelated
kixelated deleted the quest/m1/quic/coverage/bench branch October 17, 2026 01:58
kixelated added a commit that referenced this pull request Oct 20, 2026
Richer simulation workloads was the epic's last child; the benchmarks
(#5812), fuzzing (#5779, #5946), characterization tests (#5781), seeded
simulation (#5780), and coverage report (#5772) landed before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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