io_uring busy-poll Phase B: UringDriver (status + remaining work) #7

Closed
opened 2026-07-01 13:09:26 +00:00 by vxfemboy · 0 comments
vxfemboy commented 2026-07-01 13:09:26 +00:00 (Migrated from github.com)

Tracks the remaining half of the unified io_uring busy-poll data-loop milestone. Spec + plan (both phases) are on main:

  • Spec: docs/superpowers/specs/2026-06-30-io-uring-busy-poll-design.md
  • Plan: docs/superpowers/plans/2026-06-30-io-uring-busy-poll.md

Where we are — Phase A is DONE and merged (#6, 0e373fa)

Phase A delivered the single-thread lock-removal win without any io_uring:

  • DataPlane (bin/yipd/src/dataplane.rs) — all packet logic (seal/FEC/frame; deframe/decode/open; control-handler; ARQ; feedback tick) as a mutex-free, I/O-free struct, unit-tested end to end (round-trip, control/ARQ, forged-packet rejection).
  • PollDriver (crates/yip-io/src/poll.rs) — a single-threaded epoll loop over the UDP+TUN fds driving a Dispatch trait (on_udp/on_tun/tickDispatchOut); drop-tolerant on transient UDP send errors; unsafe confined to yip-io with SAFETY comments.
  • tunnel.rs::run(): ~637 → ~93 lines, no threads / Arc / Mutex / .join(). yipd stays #![forbid(unsafe_code)]. yip-device gained TunTap::as_raw_fd() + set_nonblocking().
  • Measured (release, netns, no netem): tunnel RTT ~0.51 → ~0.36 ms (lock/handoff removal — the latency north-star); clean-link single-stream throughput holds within variance (~419 vs ~457 Mbit/s). All three netns tests pass single-threaded, locally + on CI.

What Phase B needs (plan tasks 5–8)

  1. UringDriver (crates/yip-io/src/uring.rs) — ONE io_uring servicing both the UDP and TUN fds. Provided-buffer ring (RING_BUFS = 256 × MAX_WIRE_DATAGRAM = 512 KiB, < 1 MiB for the dev-box RLIMIT_MEMLOCK). Multishot/pooled RECV on UDP + READ on TUN; user_data demux; busy-poll loop → Dispatch → submit sends → re-provide recv buffers. Loopback unit tests (skip-on-Err under memlock).
  2. GSO egress + bounded in-flight send table — coalesce a packet's same-size FEC symbols into one UDP_SEGMENT send; send buffers live in a bounded slot table keyed by user_data until their CQE arrives (the buffer-lifetime unsafe core); sendmmsg fallback if GSO rejected.
  3. Startup driver selectionuring_available() probe → UringDriver else PollDriver; YIP_FORCE_POLL=1 forces the fallback. Run the netns suite on both drivers.
  4. Bench + CI + docs — latency + throughput, io_uring vs poll; add a YIP_FORCE_POLL=1 netns run to CI; record deltas.

Constraints / risks

  • unsafe only in yip-io; yipd stays #![forbid(unsafe_code)]; SAFETY comment per block. The hand-managed buffer lifetimes (provide/borrow/re-provide recvs; bounded in-flight send table) are the top risk.
  • No wire change — the netns suite (ping / ping-under-loss / arq-integrity) gates both drivers.
  • Buffer ring < 1 MiB (memlock). io_uring basic ring works on the dev box (kernel 6.18); registered buffers charge memlock, so keep the ring small.
  • Single-core by design — Phase B is a latency + GSO win, NOT multi-core throughput scaling (that's a separate deferred milestone, see the throughput-sharding issue).
  • If the buffer discipline proves too error-prone, Phase A (PollDriver) already ships and the daemon works on it — Phase B can iterate without blocking the merged lock-removal win.

Resume

Fresh session → superpowers:subagent-driven-development pointed at plan tasks 5–8, off a new branch from main. Progress ledger scaffold: .superpowers/sdd/progress.md.

Tracks the remaining half of the unified io_uring busy-poll data-loop milestone. Spec + plan (both phases) are on `main`: - Spec: `docs/superpowers/specs/2026-06-30-io-uring-busy-poll-design.md` - Plan: `docs/superpowers/plans/2026-06-30-io-uring-busy-poll.md` ## Where we are — Phase A is DONE and merged (#6, `0e373fa`) Phase A delivered the single-thread lock-removal win *without any io_uring*: - **`DataPlane`** (`bin/yipd/src/dataplane.rs`) — all packet logic (seal/FEC/frame; deframe/decode/open; control-handler; ARQ; feedback `tick`) as a **mutex-free, I/O-free** struct, unit-tested end to end (round-trip, control/ARQ, forged-packet rejection). - **`PollDriver`** (`crates/yip-io/src/poll.rs`) — a single-threaded `epoll` loop over the UDP+TUN fds driving a `Dispatch` trait (`on_udp`/`on_tun`/`tick` → `DispatchOut`); drop-tolerant on transient UDP send errors; `unsafe` confined to yip-io with SAFETY comments. - `tunnel.rs::run()`: **~637 → ~93 lines**, no threads / `Arc` / `Mutex` / `.join()`. yipd stays `#![forbid(unsafe_code)]`. yip-device gained `TunTap::as_raw_fd()` + `set_nonblocking()`. - **Measured (release, netns, no netem):** tunnel RTT **~0.51 → ~0.36 ms** (lock/handoff removal — the latency north-star); clean-link single-stream throughput holds within variance (~419 vs ~457 Mbit/s). All three netns tests pass single-threaded, locally + on CI. ## What Phase B needs (plan tasks 5–8) 5. **`UringDriver`** (`crates/yip-io/src/uring.rs`) — ONE `io_uring` servicing both the UDP and TUN fds. Provided-buffer ring (`RING_BUFS = 256 × MAX_WIRE_DATAGRAM = 512 KiB`, **< 1 MiB** for the dev-box `RLIMIT_MEMLOCK`). Multishot/pooled `RECV` on UDP + `READ` on TUN; `user_data` demux; busy-poll loop → `Dispatch` → submit sends → re-provide recv buffers. Loopback unit tests (skip-on-Err under memlock). 6. **GSO egress + bounded in-flight send table** — coalesce a packet's same-size FEC symbols into one `UDP_SEGMENT` send; send buffers live in a bounded slot table keyed by `user_data` until their CQE arrives (the buffer-lifetime `unsafe` core); sendmmsg fallback if GSO rejected. 7. **Startup driver selection** — `uring_available()` probe → `UringDriver` else `PollDriver`; `YIP_FORCE_POLL=1` forces the fallback. Run the netns suite on **both** drivers. 8. **Bench + CI + docs** — latency + throughput, io_uring vs poll; add a `YIP_FORCE_POLL=1` netns run to CI; record deltas. ## Constraints / risks - **`unsafe` only in yip-io**; yipd stays `#![forbid(unsafe_code)]`; SAFETY comment per block. The hand-managed buffer lifetimes (provide/borrow/re-provide recvs; bounded in-flight send table) are the top risk. - **No wire change** — the netns suite (ping / ping-under-loss / arq-integrity) gates both drivers. - **Buffer ring < 1 MiB** (memlock). io_uring basic ring works on the dev box (kernel 6.18); registered buffers charge memlock, so keep the ring small. - **Single-core by design** — Phase B is a latency + GSO win, NOT multi-core throughput scaling (that's a separate deferred milestone, see the throughput-sharding issue). - If the buffer discipline proves too error-prone, Phase A (PollDriver) already ships and the daemon works on it — Phase B can iterate without blocking the merged lock-removal win. ## Resume Fresh session → `superpowers:subagent-driven-development` pointed at plan tasks 5–8, off a new branch from `main`. Progress ledger scaffold: `.superpowers/sdd/progress.md`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
femboy/yip#7
No description provided.