throughput: batched UDP I/O (sendmmsg/recvmmsg) on the poll hot path #54

Merged
vxfemboy merged 5 commits from feat/throughput-batched-io into main 2026-07-11 23:28:35 +00:00
vxfemboy commented 2026-07-11 19:23:01 +00:00 (Migrated from github.com)

What

Batches the UDP send/recv on the default poll data-plane loop with sendmmsg(2)/recvmmsg(2), collapsing per-packet syscalls into per-burst ones. Lever 3 of 3 for single-core 10-Gbit (cheap FEC ✓ #51 → fast AEAD ✓ #52batched I/O).

Why

With FEC (~0.32 µs) and AEAD (~0.63 µs) cheap after #51/#52, per-packet UDP syscalls are the dominant single-core cost — the tx path issued ~2–3 sendtos per packet (one per FEC symbol) plus one recvfrom per datagram on rx. yip-io already had sendmmsg/recvmmsg mechanics but they were unwired and lacked per-datagram addresses.

How

  • crates/yip-io/src/poll.rs — two new addressed batch syscalls (co-located with the existing epoll code): send_mmsg(udp_fd, &[EgressDatagram]) (one sendmmsg, each datagram to its own dst via msg_name) and recv_mmsg(udp_fd, bufs, lens, srcs) (one non-blocking recvmmsg, capturing each src).
  • drain_udp drains the rx burst with recvmmsg; drain_tun accumulates a TUN burst's egress symbols into one send_mmsg via flush_tx (chunked at 64); tick egress batched too. run_poll owns reusable batch buffers (rx bufs Boxed, 128 KiB). The now-dead per-packet send_to_udp was removed. run_poll's public signature is unchanged, so yipd needs no edits.

Opportunistic and latency-neutral — it batches only what epoll already has queued; a single ready packet still does one recv + one send syscall. No GSO — each datagram stays its own independent UDP packet, so two symbols of one FEC object are never lost as a unit (FEC loss-independence preserved).

Results

  • Correctness (netns, real sudo): run-netns-tunnel, run-netns-tunnel-loss (10/10 under 10% netem loss — FEC still recovers with batched sends), and run-arq-integrity (118 retransmits) all PASS. Full cargo test --workspace: 0 failures; yip-io 30/30 (addressed send_mmsg/recv_mmsg unit-tested on loopback sockets).
  • Structural win: tx path ~2–3 sendto/packet → one sendmmsg/burst; rx one recvfrom/datagram → one recvmmsg/burst.

Final whole-branch review (opus): READY WITH FOLLOW-UPS — the mmsg unsafe is memory-safe (copy-into-array-before-taking-pointer, no aliasing/OOB, SAFETY comments accurate), loop termination can't hang/spin, egress is behaviorally equivalent (no dropped path, tx_batch no leak), and yipd stays #![forbid(unsafe_code)] (unsafe confined to yip-io).

Follow-ups (non-blocking)

  • Throughput number not captured this run: crates/yip-bench/tests/run-iperf-compare.sh wedges in this environment (two yipd come up but iperf never completes — a harness/env flake, not a data-plane regression, since every other netns test passes traffic). The measured before/after Gbit figure — the spec's headline metric — is a tracked follow-up (fix/replace that harness).
  • Two doc comments in uring.rs (:857, :1040) still name the deleted send_to_udp.
  • Minor: recv_mmsg maps an unparseable src to 0.0.0.0:0 silently; no explicit 64-cap unit test; flush_tx doc wording.

Design docs

  • Spec: docs/superpowers/specs/2026-07-11-throughput-batched-io-design.md
  • Plan: docs/superpowers/plans/2026-07-11-throughput-batched-io.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01RVP6NnbDMAg1iTsMMTfL86

## What Batches the UDP send/recv on the default poll data-plane loop with `sendmmsg(2)`/`recvmmsg(2)`, collapsing per-packet syscalls into per-burst ones. Lever 3 of 3 for single-core 10-Gbit (cheap FEC ✓ #51 → fast AEAD ✓ #52 → **batched I/O**). ## Why With FEC (~0.32 µs) and AEAD (~0.63 µs) cheap after #51/#52, per-packet UDP **syscalls** are the dominant single-core cost — the tx path issued ~2–3 `sendto`s *per packet* (one per FEC symbol) plus one `recvfrom` per datagram on rx. `yip-io` already had `sendmmsg`/`recvmmsg` mechanics but they were unwired and lacked per-datagram addresses. ## How - **`crates/yip-io/src/poll.rs`** — two new addressed batch syscalls (co-located with the existing epoll code): `send_mmsg(udp_fd, &[EgressDatagram])` (one `sendmmsg`, each datagram to its own `dst` via `msg_name`) and `recv_mmsg(udp_fd, bufs, lens, srcs)` (one non-blocking `recvmmsg`, capturing each `src`). - **`drain_udp`** drains the rx burst with `recvmmsg`; **`drain_tun`** accumulates a TUN burst's egress symbols into one `send_mmsg` via `flush_tx` (chunked at 64); `tick` egress batched too. `run_poll` owns reusable batch buffers (rx bufs `Box`ed, 128 KiB). The now-dead per-packet `send_to_udp` was removed. `run_poll`'s public signature is unchanged, so `yipd` needs no edits. **Opportunistic and latency-neutral** — it batches only what epoll already has queued; a single ready packet still does one recv + one send syscall. **No GSO** — each datagram stays its own independent UDP packet, so two symbols of one FEC object are never lost as a unit (FEC loss-independence preserved). ## Results - **Correctness (netns, real sudo):** `run-netns-tunnel`, **`run-netns-tunnel-loss` (10/10 under 10% netem loss — FEC still recovers with batched sends)**, and `run-arq-integrity` (118 retransmits) all PASS. Full `cargo test --workspace`: 0 failures; `yip-io` 30/30 (addressed `send_mmsg`/`recv_mmsg` unit-tested on loopback sockets). - **Structural win:** tx path ~2–3 `sendto`/packet → **one `sendmmsg`/burst**; rx one `recvfrom`/datagram → one `recvmmsg`/burst. Final whole-branch review (opus): **READY WITH FOLLOW-UPS** — the `mmsg` unsafe is memory-safe (copy-into-array-before-taking-pointer, no aliasing/OOB, SAFETY comments accurate), loop termination can't hang/spin, egress is behaviorally equivalent (no dropped path, `tx_batch` no leak), and `yipd` stays `#![forbid(unsafe_code)]` (unsafe confined to `yip-io`). ## Follow-ups (non-blocking) - **Throughput number not captured this run:** `crates/yip-bench/tests/run-iperf-compare.sh` wedges in this environment (two `yipd` come up but iperf never completes — a harness/env flake, not a data-plane regression, since every other netns test passes traffic). The measured before/after Gbit figure — the spec's headline metric — is a tracked follow-up (fix/replace that harness). - Two doc comments in `uring.rs` (:857, :1040) still name the deleted `send_to_udp`. - Minor: `recv_mmsg` maps an unparseable src to `0.0.0.0:0` silently; no explicit 64-cap unit test; `flush_tx` doc wording. ## Design docs - Spec: `docs/superpowers/specs/2026-07-11-throughput-batched-io-design.md` - Plan: `docs/superpowers/plans/2026-07-11-throughput-batched-io.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01RVP6NnbDMAg1iTsMMTfL86
Sign in to join this conversation.
No description provided.