perf(core): packetize straight into the wire pool — zero-alloc host send path
Stage B of the zero-copy host packetize path (networking-audit deferred plan §1): Packetizer::packetize_each yields (header, shard) pairs in exact wire order; Session::seal_frame writes seq(8) ‖ header(40) ‖ shard ‖ tag scratch directly into the pooled wire buffer and seals [8..] in place. The per-packet intermediate Vec (header ++ body) and its extra memcpy are gone — with Stage A, every data byte is now copied once (frame → wire) instead of three times, and the ~2 transient allocs/packet on the send thread are zero after pool warmup (~180k allocs/s at 1 Gbps rates). packetize() stays as a thin wrapper over packetize_each — the reference implementation used by tests and the loss harness. - wire-equivalence test: pooled path vs wrapper path byte-identical across multi-block/partial-tail/exact-multiple/empty frames, fec 0%/50%, both schemes, crypto on/off - loss-harness sweep: recovery rates identical to the pre-item-1 baseline - bench pipeline (end-to-end incl. client half) vs pre-item-1 baseline, stages A+B cumulative: gf16/64K -3.6%, gf16/1M -3.2%; gf8 cases are Cauchy-math-bound and unchanged within noise - cargo ndk check (arm64-v8a) green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -135,7 +135,9 @@ impl Packetizer {
|
||||
self.fec.fec_percent
|
||||
}
|
||||
|
||||
/// Packetize one access unit into wire packets (header + shard payload each).
|
||||
/// Packetize one access unit into owned wire packets (header ++ shard payload each).
|
||||
/// Thin wrapper over [`packetize_each`](Self::packetize_each) — the allocation-free
|
||||
/// streaming path's reference implementation (tests and the loss harness use this).
|
||||
pub fn packetize(
|
||||
&mut self,
|
||||
frame: &[u8],
|
||||
@@ -143,6 +145,31 @@ impl Packetizer {
|
||||
user_flags: u32,
|
||||
coder: &dyn ErasureCoder,
|
||||
) -> Result<Vec<Vec<u8>>> {
|
||||
let mut packets = Vec::new();
|
||||
self.packetize_each(frame, pts_ns, user_flags, coder, |hdr, body| {
|
||||
let mut pkt = Vec::with_capacity(HEADER_LEN + body.len());
|
||||
pkt.extend_from_slice(hdr.as_bytes());
|
||||
pkt.extend_from_slice(body);
|
||||
packets.push(pkt);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(packets)
|
||||
}
|
||||
|
||||
/// Packetize one access unit, yielding each packet to `emit` as a `(header, shard bytes)`
|
||||
/// pair — in exact wire order, which is also the order the session's nonce counter
|
||||
/// advances. No per-packet allocation happens here, so the caller can write header and
|
||||
/// shard straight into a pooled wire buffer and seal in place
|
||||
/// ([`Session::seal_frame`](crate::session::Session::seal_frame)). An `emit` error aborts
|
||||
/// the frame mid-way (packet numbering has already advanced — callers treat it as fatal).
|
||||
pub fn packetize_each(
|
||||
&mut self,
|
||||
frame: &[u8],
|
||||
pts_ns: u64,
|
||||
user_flags: u32,
|
||||
coder: &dyn ErasureCoder,
|
||||
mut emit: impl FnMut(&PacketHeader, &[u8]) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let payload = self.shard_payload;
|
||||
let frame_index = self.next_frame_index;
|
||||
self.next_frame_index = self.next_frame_index.wrapping_add(1);
|
||||
@@ -183,7 +210,6 @@ impl Packetizer {
|
||||
}
|
||||
};
|
||||
|
||||
let mut packets = Vec::new();
|
||||
for b in 0..block_count {
|
||||
let first = b * max_block;
|
||||
let last = ((b + 1) * max_block).min(total_data);
|
||||
@@ -234,14 +260,10 @@ impl Packetizer {
|
||||
fec_scheme: coder.scheme() as u8,
|
||||
flags,
|
||||
};
|
||||
|
||||
let mut pkt = Vec::with_capacity(HEADER_LEN + body.len());
|
||||
pkt.extend_from_slice(hdr.as_bytes());
|
||||
pkt.extend_from_slice(body);
|
||||
packets.push(pkt);
|
||||
emit(&hdr, body)?;
|
||||
}
|
||||
}
|
||||
Ok(packets)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user