Skip to content

test(quic): prove receive memory with JavaScript senders and benchmarks - #5752

Closed
kixelated wants to merge 8 commits into
quest/m1/quic/receive-memoryfrom
quest/m1/quic/receive-memory-coverage
Closed

kixelated wants to merge 8 commits into
quest/m1/quic/receive-memoryfrom
quest/m1/quic/receive-memory-coverage

Conversation

@kixelated

@kixelated kixelated commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

#5735 bounds incomplete messages by QUIC connection credit and recovers by stopping the least useful stream, with Rust-only end-to-end tests. Nothing proved that a JavaScript sender drives either native receiver into exhaustion and that it recovers, and nothing measured what bounded receive memory costs.

Approach

Stacked on #5735. Finishes quest/m1/quic/receive-memory-coverage.md.

  • JavaScript sender. test/receive-credit/sender.ts is a Bun publisher over @moq/web-transport, pinned to the server's certificate, with WebSocket disabled. Each stdin line asks it for one group: a low-priority frame twice the window, a high-priority half-window frame, then a small low group.
  • Test-only WebTransport servers. Each receiver is assembled in its test file with a 256 KiB connection window, far below MIN_RECEIVE_WINDOW: tokio's quic::driver endpoint behind quic::web::Server (rs/moq-tokio/tests/integration/receive_credit.rs), and an io_uring endpoint in web mode (rs/moq-uring/tests/integration/receive_credit.rs, moq-lite only, as the worker serves). The test waits until three quarters of the window holds the low frame, asks for the high frame and requires it to complete, requires the low frame to end in an error rather than complete, and then requires the later low group to arrive. Both are #[ignore] (they need Bun) and run through just test receive-credit, a new interop workflow step with MOQ_STRICT=1, so the io_uring arm fails instead of skipping. The tokio arm covers moq-lite-06 and moq-transport-16.
  • Benchmark. benchmark_receive_windows (moq-tokio, release, nightly next to benchmark_held_reads) delivers 256 MiB per run from a stock server to a raw-driver subscriber with moq-tokio's 128 MiB connection window. It sweeps concurrent streams (1, 16, 256 at 256 KiB frames) and frame sizes (16 KiB, 256 KiB, 4 MiB at 16 streams) on separate axes. Each run compares the driver's finite 1.25 MB stream window against one that follows the connection window. Each track writes a one-frame group once the previous one arrives, so the stream count is the concurrency. It reports peak retained bytes (held + unread, sampled every millisecond), throughput, and p50/p99 write-to-complete latency, each the median of 3 alternating runs. About 17 s locally.

Negative control: with Monitor::poll's eviction disabled, both JavaScript tests time out waiting for the high-priority group.

Local benchmark on a shared, loaded host (loopback, so there is no RTT):

axis streams frame stream window retained MiB MiB/s p50 ms p99 ms
streams 1 256 KiB finite 0.3 423 0.41 1.16
streams 1 256 KiB follow 0.2 322 0.40 26.85
streams 16 256 KiB finite 2.7 719 4.12 17.35
streams 16 256 KiB follow 2.0 716 3.95 16.15
streams 256 256 KiB finite 6.2 682 59.11 273.34
streams 256 256 KiB follow 5.6 686 36.32 314.39
frame 16 16 KiB finite 0.3 493 0.45 1.19
frame 16 16 KiB follow 0.2 490 0.45 1.16
frame 16 256 KiB finite 1.9 718 4.14 17.29
frame 16 256 KiB follow 1.9 725 4.05 14.97
frame 16 4 MiB finite 42.9 631 61.50 346.36
frame 16 4 MiB follow 32.9 650 58.85 243.87

A finite stream window does not bound retained memory. Stream credit returns on reads while the incomplete frame keeps its connection credit, so retained bytes track streams x frame in both modes. This supports #5735's large stream-window default. Loopback hides the extra round trips a finite window adds, so its latency cost is understated here.

Impact

  • No public API change. Both servers are assembled inside test files from existing raw-driver APIs; nothing new can configure a window below MIN_RECEIVE_WINDOW.
  • No wire change.
  • New private Bun workspace @moq/receive-credit-test, a just test receive-credit recipe, an interop workflow step and path triggers, and one nightly benchmark line.

Alternatives

  • Hashing the certificate in Rust to pass serverCertificateHashes: that needs a digest dependency in two test crates. The sender passes the PEM as serverCertificate, and @moq/net computes the same SHA-256 pin.
  • A grid sweep of streams x frame sizes: the quest asks for separate axes so each slope reads on its own, and a grid adds cells without a clearer slope.
  • An open-loop writer for the benchmark: it opens a stream per queued group, so the stream count would no longer be the concurrency.

Follow-ups

  • The benchmark runs on loopback. An RTT-shaped variant (for example through moq-shaper) would show the round trips a finite stream window costs large frames.

🤖 Generated with Claude Code

(Written by Opus 5.5)

kixelated and others added 4 commits October 16, 2026 11:31
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d benchmark it

A Bun publisher over @moq/web-transport fills a test-only WebTransport
server's connection window, on tokio's raw driver and on an io_uring
endpoint, with a frame that can never complete. The receiver stops it, a
high-priority frame completes, and a later low group arrives. The interop
workflow runs it, since normal CI has no Bun.

benchmark_receive_windows sweeps concurrent streams and frame sizes on
separate axes, comparing the driver's finite stream window with one that
follows the connection window, and reports retained bytes, throughput,
and frame completion latency. Nightly runs it next to benchmark_held_reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… into quest/m1/quic/receive-memory-coverage

# Conflicts:
#	quest/m1/quic/README.md
#	quest/m1/quic/receive-memory-coverage.md
#	quest/m1/quic/receive-memory.md
… into quest/m1/quic/receive-memory-coverage

# Conflicts:
#	sh/gh/nightly.sh
… into quest/m1/quic/receive-memory-coverage

# Conflicts:
#	quest/m1/quic/README.md
@kixelated

Copy link
Copy Markdown
Collaborator Author

#5689 merged: moq-net, moq-relay, moq-tokio, and moq-uring now link their integration tests into one binary under tests/integration/. When you rebase, move your edits to rs/moq-tokio/tests/receive_credit.rs and rs/moq-uring/tests/receive_credit.rs (added by #5735, which this PR stacks on) into that crate's tests/integration/ and add a mod line to its main.rs. Moving them in #5735 first and rebasing onto it is simplest.

(Written by Claude Opus 5.5)

… into quest/m1/quic/receive-memory-coverage

# Conflicts:
#	.github/workflows/interop.yml
kixelated added a commit that referenced this pull request Oct 17, 2026
…sender

Ports the coverage from #5752 onto leases, and ranks a group stream by its
subscription priority before its sequence.

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

Copy link
Copy Markdown
Collaborator Author

Closing as folded into #5878, which carries this PR's Bun sender, receive-credit tests and benchmark_receive_windows.

(Written by Claude Opus 5.5)

@kixelated kixelated closed this Oct 20, 2026
@kixelated
kixelated deleted the quest/m1/quic/receive-memory-coverage branch October 22, 2026 06:01
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