2f4f92a804
ci / rust (push) Has been cancelled
Final increment of the 1 Gbps data-plane rework — the recv counterpart of the sendmmsg work. The client recv path did one recvfrom + one Vec allocation per packet (and the pump's 300µs idle sleep could let packets pile up at line rate). - Transport gains recv_batch(&mut [Vec<u8>], &mut [usize]) -> count; default is a single scalar recv into out[0] (loopback + non-Linux). - UdpTransport overrides it on Linux with recvmmsg (MSG_DONTWAIT) draining up to N datagrams per syscall into the caller's reused buffers — no per-packet alloc. - Session::poll_frame owns a lazily-allocated recv ring (RECV_BATCH=32) and consumes it one packet at a time across calls, refilling with one recvmmsg when drained. Encapsulated: the punktfunk-client-rs + NativeClient pumps are unchanged, and draining a batch per syscall means the 300µs sleep no longer underdrains. Added UdpTransport::local_addr (used by the test, generally handy). ~125k → ~4k recv syscalls/sec at line rate, zero per-packet recv allocation. Verified: new recv_batch_drains_over_loopback test (50 datagrams drained intact via recvmmsg) + the existing loopback round-trip now runs through the batched poll_frame; full suite (35 + round-trip + 6) + clippy + fmt green. Decode-in-place (kill the per-packet open_from_wire alloc) is a separate later optimization. With A (sendmmsg) + B (paced send) + C (recvmmsg), the native data plane is batched + paced end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
2.8 KiB
Rust
59 lines
2.8 KiB
Rust
//! Pluggable packet I/O. The hot path calls [`Transport::send`] / [`Transport::recv`]
|
|
//! directly — no async runtime is involved.
|
|
|
|
mod loopback;
|
|
mod udp;
|
|
|
|
pub use loopback::{loopback_pair, LoopbackTransport};
|
|
pub use udp::UdpTransport;
|
|
|
|
/// A datagram transport. `recv` is non-blocking: it returns `Ok(None)` when no packet
|
|
/// is currently available, so the caller (decode/present thread) never blocks here.
|
|
pub trait Transport: Send + Sync {
|
|
/// Send one packet. `Ok(true)` = handed to the kernel; `Ok(false)` = dropped locally because
|
|
/// the send buffer was momentarily full (WouldBlock) — a non-fatal loss the FEC/keyframe path
|
|
/// recovers, surfaced so the caller can count it (`packets_send_dropped`) instead of it being
|
|
/// invisible. `Err` = a real send failure.
|
|
fn send(&self, packet: &[u8]) -> std::io::Result<bool>;
|
|
|
|
/// Send a whole frame's packets in as few syscalls as possible, returning how many were
|
|
/// handed to the kernel (the caller counts `packets.len() - sent` as send-buffer drops). This
|
|
/// is the 1 Gbps+ lever: the [`UdpTransport`](super::UdpTransport) override uses `sendmmsg`
|
|
/// (~64 packets/syscall) instead of one `send` each — at ~125k pkt/s that is the difference
|
|
/// between ~2k and ~125k syscalls/sec. The default is the scalar `send` loop (correct for the
|
|
/// loopback transport and non-Linux builds). On a full send buffer it stops early and reports
|
|
/// the partial count rather than blocking — same lossy, FEC-protected contract as `send`.
|
|
fn send_batch(&self, packets: &[&[u8]]) -> std::io::Result<usize> {
|
|
let mut sent = 0;
|
|
for p in packets {
|
|
if self.send(p)? {
|
|
sent += 1;
|
|
}
|
|
}
|
|
Ok(sent)
|
|
}
|
|
|
|
fn recv(&self) -> std::io::Result<Option<Vec<u8>>>;
|
|
|
|
/// Receive up to `out.len()` datagrams in as few syscalls as possible, writing each into its
|
|
/// `out[i]` buffer (sized ≥ a max datagram) and its byte count into `lens[i]`; returns how many
|
|
/// arrived (`0` = none available; non-blocking). The recv counterpart of [`send_batch`]: the
|
|
/// [`UdpTransport`](super::UdpTransport) override uses `recvmmsg` into a caller-owned, reused
|
|
/// buffer ring — no per-packet allocation or syscall at line rate. The default does a single
|
|
/// scalar [`recv`](Self::recv) into `out[0]` (correct for the loopback transport + non-Linux).
|
|
fn recv_batch(&self, out: &mut [Vec<u8>], lens: &mut [usize]) -> std::io::Result<usize> {
|
|
if out.is_empty() {
|
|
return Ok(0);
|
|
}
|
|
match self.recv()? {
|
|
Some(pkt) => {
|
|
let n = pkt.len().min(out[0].len());
|
|
out[0][..n].copy_from_slice(&pkt[..n]);
|
|
lens[0] = n;
|
|
Ok(1)
|
|
}
|
|
None => Ok(0),
|
|
}
|
|
}
|
|
}
|