Skip to content

perf(uring): remove avoidable copies on egress, streams, and datagram receive - #5843

Merged
kixelated merged 6 commits into
mainfrom
quest/m2/perf/uring-copies
Oct 17, 2026
Merged

kixelated merged 6 commits into
mainfrom
quest/m2/perf/uring-copies

Conversation

@kixelated

@kixelated kixelated commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Completes quest/m2/perf/uring-copies.md (deleted here, along with its references).

Summary

The four copies the quest listed, all in moq-uring:

  • Egress: moq-quic encrypts each GSO train straight into the TX pool's buffer. The per-connection scratch Vec and its copy are gone. To make that work, udp::TxBuf now owns the slot's Vec<u8> while it is checked out.
  • Stream send: poll_write_buf hands moq-quic the chunk through write_chunks, so a Bytes source is kept by reference instead of copied by write(&[u8]). poll_write goes through the same path.
  • Stream receive: poll_read_chunk returns moq-quic's Bytes, or a zero-copy split of the read-ahead backlog, instead of the trait default's 8 KiB BytesMut plus a copy. web::RecvStream forwards it.
  • Datagram receive: one BytesMut per completion, split per GRO segment, instead of one allocation per datagram. The last (or only) segment is taken whole: splitting a single datagram made the allocation shared and cost ~7% on no-gro in an early run.

API and wire impact

No wire change.

  • moq_quic::SendStream::write_limit() -> usize (new): how many bytes a write would accept right now. This is a QUIC API addition the quest assumed wasn't needed. See decision 1.
  • moq_uring::udp::TxBuf now derefs to Vec<u8>, empty on acquire with tx_buffer_len capacity, instead of a full-capacity [u8]. Fill it with extend_from_slice / resize.
  • moq_uring::udp::Transmit::len is removed: the buffer's length is what gets sent, so the two can't disagree. A buffer grown past tx_buffer_len is refused with InvalidInput, so the configured per-buffer ceiling still holds.
  • moq_uring::udp::Packet::{payload_mut, segments} are removed. Their only caller was the old datagram demux.

Measurements

All numbers come from a loaded shared host (load average 19 to 30 on 32 cores, many concurrent agents). Base and new runs were interleaved. Each one is session_lite (a full moq-lite publisher/subscriber pair on one worker, 1 MiB per iteration) built from an origin/main export and from this branch.

Heap allocations per MiB delivered (deterministic: counting global allocator, measured section only; scratch patch, not committed):

ablation allocs/MiB base → new bytes allocated/MiB base → new
all-on 3677 → 459 (-88%) 9.40 MB → 2.29 MB (-76%)
no-gso 4522 → 2849 (-37%) 9.53 MB → 2.36 MB (-75%)
no-gro 4521 → 2848 (-37%) 9.52 MB → 2.35 MB (-75%)
oneshot 3734 → 511 (-86%) 9.43 MB → 2.31 MB (-76%)
all-off 7588 → 5491 (-28%) 12.97 MB → 2.76 MB (-79%)

Throughput per CPU-second of the worker thread (CLOCK_THREAD_CPUTIME_ID, both peers on that thread):

  • all-on: +12% to +17% median across two runs (8 and 12 interleaved pairs). New was faster in 6/8 and 8/12 pairs.
  • no-gso, no-gro, oneshot, all-off: within noise. A base-vs-base control run spread from -4% to +19% on this host.

Wall-clock Criterion runs swung 180 to 420 MiB/s for the same binary, so I don't quote them. echo_quic (tokio peer on another thread) was noisier still. Re-run on a quiet box before reading anything into the small-mode deltas.

Tests

  • credit::chunked_writes_stop_at_the_window (new, moq-uring integration):
    • a chunked write of a Bytes larger than the peer's stream window takes exactly the window and leaves the rest in the caller's buffer;
    • moq-quic holds the caller's allocation rather than a copy. This check fails on main ("moq-quic copied the chunk");
    • chunked reads return the same bytes, both direct and from the poll_closed read-ahead backlog.
  • moq-quic stream_flow_control / conn_flow_control assert write_limit matches what write accepts, and is zero once blocked or reset.
  • just check passes (4835 workspace tests plus the rest).

Decisions

Settled by the author with the recommended options:

  1. Keep moq_quic::SendStream::write_limit() (option a). write_chunks alone can't honor the trait's partial-write contract: once a chunk is taken from a generic Buf, the part moq-quic refuses can't be put back. The alternatives were to keep copying on stream send, or to add a Bytes-specific write to moq-net's transport trait. The name is open to the maintainer at review (capacity() and writable() were the alternatives).
  2. Keep udp::Packet::{payload_mut, segments} removed. They lost their only caller, the crate is experimental, and restoring them is trivial if a raw-UDP consumer wants them.

Suggested follow-ups

  • Make the allocation count a regression guard: a moq-uring allocation test or bench that reports allocs/MiB for a QUIC stream transfer, so a reintroduced copy shows up in CI. Criterion wall-clock can't see it on shared hosts.
  • RecvStream's read-ahead backlog still copies (extend_from_slice). A VecDeque<Bytes> would drop that copy too.
  • The remaining ~2.3 MB/MiB is mostly the receive-pool → BytesMut copy, which is quest/m2/quic-io-boundary.md's Endpoint::handle candidate.

🤖 Generated with Claude Code

(written by Claude Opus 5.5)

kixelated and others added 2 commits October 16, 2026 19:24
… receive

- Egress: moq-quic encrypts each GSO train straight into the TX pool's
  buffer instead of a scratch Vec that was then copied. `udp::TxBuf` now
  derefs to the slot's `Vec<u8>` (empty on acquire) and `udp::Transmit`
  drops `len`: the buffer's length is what goes out.
- Stream send: `poll_write_buf` hands moq-quic the chunk via `write_chunks`,
  so a `Bytes` source is kept by reference. A new
  `moq_quic::SendStream::write_limit` sizes the chunk first, since bytes
  taken from a `Buf` cannot be put back.
- Stream receive: `poll_read_chunk` returns moq-quic's `Bytes` (or a split of
  the read-ahead backlog) instead of the trait default's 8 KiB copy.
- Datagram receive: one `BytesMut` per completion, split per GRO segment,
  instead of one allocation per datagram. `udp::Packet::{payload_mut,
  segments}` lose their only caller and go.

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

Copy link
Copy Markdown
Collaborator Author

Quest outcome: all four copies are removed (egress, stream send, stream receive, datagram receive). Left as a draft because stream send needed a small moq-quic API addition (SendStream::write_limit) that the quest expected to avoid. Decision 1 in the description has the options; I recommend keeping it.

Checks run: just check (all pass), moq-uring tests with MOQ_STRICT=1 (95/95), and the new zero-copy assertion confirmed failing on main. Not run: just test interop --all. There's no wire, moq-ffi, or gateway change.

Perf came from a loaded shared host. Allocations per MiB are deterministic (-88% on all-on). CPU-time throughput is +12 to 17% on all-on and within noise for the other ablations.

(written by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 17, 2026 04:22
@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-17T06:49:21.327683Z b275b70 New commits
ℹ️ 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 37 seconds.

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: 12142394-d9fd-4b20-8ac4-e7da9aabf7b4
📥 Commits

Reviewing files that changed from the base of the PR and between 9720f46 and e80e53d.

📒 Files selected for processing: 16
  • quest/m2/perf/README.md
  • quest/m2/perf/uring-copies.md
  • quest/m2/quic-io-boundary.md
  • rs/moq-quic/src/connection/streams/mod.rs
  • rs/moq-quic/src/tests/mod.rs
  • rs/moq-uring/benches/worker_turns.rs
  • rs/moq-uring/src/quic/driver/connection.rs
  • rs/moq-uring/src/quic/driver/endpoint.rs
  • rs/moq-uring/src/quic/driver/stream.rs
  • rs/moq-uring/src/quic/web.rs
  • rs/moq-uring/src/udp.rs
  • rs/moq-uring/src/worker.rs
  • rs/moq-uring/tests/allocations.rs
  • rs/moq-uring/tests/integration/credit.rs
  • rs/moq-uring/tests/integration/ecn.rs
  • rs/moq-uring/tests/integration/teardown.rs
  • 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c91ec99a55

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-uring/src/udp.rs Outdated
// `UDP_SEGMENT` is a u16, so an oversized segment would silently
// truncate into a tiny stride and explode the implied segment count.
if len == 0 || len > self.cap || segment == 0 || segment > usize::from(u16::MAX) {
if len == 0 || segment == 0 || segment > usize::from(u16::MAX) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the configured transmit-buffer ceiling

When a raw UDP caller grows the newly exposed Vec beyond Config::tx_buffer_len, this validation accepts it because the previous len > capacity check was removed. The oversized allocation is then retained in the pool after completion, so up to tx_buffers_max buffers can permanently exceed the documented per-buffer memory ceiling, and sends can exceed the configured maximum GSO-train size. Keep the buffer bounded or reject lengths above tx_buffer_len.

AGENTS.md reference: AGENTS.md:L63-L63

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Fixed in the latest commit: send refuses a buffer grown past tx_buffer_len with InvalidInput again, and buffer_grown_past_the_pool_is_rejected covers it.

(written by Claude Opus 5.5)

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 440072f392

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-uring/src/udp.rs

impl std::ops::Deref for TxBuf {
type Target = [u8];
type Target = Vec<u8>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve pool capacity across mutable Vec access

When a caller uses a safe capacity-reducing operation such as shrink_to_fit, *tx = Vec::new(), or mem::take(&mut *tx) and drops the checkout, release_tx stores that undersized Vec back in the slot while poll_acquire only clears it. If the socket is then passed to quic::Endpoint, Driver::flush_one treats the recycled slot as unable to hold a 1200-byte segment and fails the connection. Avoid exposing capacity-changing operations or restore tx_buffer_len capacity when recycling or acquiring the slot.

AGENTS.md reference: rs/AGENTS.md:L42-L43

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Fixed in the latest commit: poll_acquire hands out every checkout at exactly tx_buffer_len capacity, replacing a slot Vec its last holder shrank or regrew. a_resized_checkout_comes_back_pool_sized covers both directions.

(written by Claude Opus 5.5)

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e49790ebf5

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +274 to +276
let segment = match payload.len() > stride {
true => payload.split_to(stride),
false => std::mem::take(&mut payload),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject zero GRO strides before splitting

When UDP_GRO metadata reports a zero stride, which on_recv explicitly anticipates in udp.rs, payload.len() > stride remains true while split_to(0) returns an empty segment without consuming payload. The demux task then spins forever on the worker thread, preventing every connection on that worker from making progress. Reject a zero stride while decoding the completion or normalize it before entering this loop.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This is a real edge: the old chunks_mut(0) would have panicked, and the new loop would spin. Fixed at the source in the latest commit: RecvMeta::parse treats a zero UDP_GRO size as no stride, so a packet falls back to its payload length, and only an empty datagram can carry a zero stride (the demux loop never runs for it). a_zero_gro_size_is_no_stride covers the parse.

(written by Claude Opus 5.5)

kixelated and others added 2 commits October 16, 2026 23:46
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

Ready for review. Both decisions are settled as recommended and recorded in the description: keep SendStream::write_limit() (name still open at review), and keep Packet::{payload_mut, segments} removed.

Codex review raised three findings, all fixed with tests:

  • a send buffer grown past tx_buffer_len is refused again;
  • every TX checkout comes back at the pool's size, even if its last holder shrank or regrew the Vec;
  • a zero UDP_GRO size parses as no stride. The old code would have panicked here and the new demux loop would have spun.

Codex's latest pass found nothing (👍). CodeRabbit was rate-limited and didn't review.

CI: Check, Test, and io_uring pass. TS validation fails with an mp2 audio hole (24 to 96 ms). It isn't a required check, and the same failure shows up on other branches (15 of the last 40 TS runs). This PR touches none of its trigger paths or the tokio pipeline it runs, and a rerun passed once.

(written by Claude Opus 5.5)

@kixelated

kixelated commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Ready to merge, but not yet enqueued: a local permission check blocked the agent from enqueueing, so a maintainer needs to run gh pr merge 5843. Summary for the paper trail:

  • Changes: moq-uring stops copying on egress (encrypts straight into the TX pool), stream send (write_chunks keeps Bytes by reference), stream receive (poll_read_chunk returns moq-quic's Bytes), and datagram receive (one copy per completion, split per GRO segment). Allocations per MiB fall 88% in the default all-on mode, and bytes allocated fall about 75% in every mode.
  • Decisions (author's recommended course): keep moq_quic::SendStream::write_limit(), with the name open to the maintainer; keep udp::Packet::{payload_mut, segments} removed.
  • Review: Codex's three findings are all fixed with tests and replied to. Its latest pass had no findings (👍). The final push is rustfmt only.
  • CI: Check and Test (the required checks) and io_uring pass. TS validation is red from the known fix(mux): put a live export's budget on its first SUBSCRIBE #5854 mp2 issue, which isn't this change.

(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 68392a9 Oct 17, 2026
7 of 8 checks passed
@kixelated
kixelated deleted the quest/m2/perf/uring-copies branch October 17, 2026 08:17
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