feat(core): zero-copy pooled reassembly — shards land at their final AU offset

Rewrite the client Reassembler around one whole-frame buffer per frame:
frame_bytes rides in every header and packetize geometry is
deterministic (every non-final block is exactly max_data_per_block data
shards), so a data shard's final AU offset is computable on arrival —
copy it there once, straight from the decrypt ring. New
ErasureCoder::reconstruct_into decodes ONLY the missing shards directly
into the frame buffer's holes (gf16 native; gf8 legacy shim); received
recovery shards ride pooled shard-sized buffers. The completed buffer
IS Frame::data.

Deletes the per-shard to_vec + per-block concat + final AU concat
(~178k allocs and a double copy of every byte per second at 2 Gbps —
the pump wall the 2026-07-14 sweeps measured at 98.9% of an M3 Ultra
core). Reassembly now costs ~0.4 µs/packet in-stream.

The eager buffer changes the hostile-header exposure, so two new
firewalls: derived-geometry validation (a header lying about its
data_shards/block_count vs its own frame_bytes is dropped before it can
scribble across another shard's range) and an in-flight allocation
budget (IN_FLIGHT_BUF_FACTOR × max_frame_bytes) so a window of tiny
first-shards can't commit gigabytes.

Behavior parity pinned by the existing suite (all green unchanged) plus
new end-to-end roundtrips through the real Packetizer (multi-block +
partial tail, loss within budget, reversed delivery, duplicates, empty
frame, unrecoverable block ages out, budget enforcement). loss-harness
recovery curve identical; pipeline bench: gf8/1MB +42%, gf16 neutral
(host-encode dominated). Known pre-existing quirk kept as-is: reversed
delivery reconstructs early (data+recovery ≥ k) and counts late-not-lost
shards into fec_recovered_shards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:08:15 +02:00
parent f2fa7828d6
commit ed0ce5dc6d
4 changed files with 626 additions and 98 deletions
+41 -1
View File
@@ -4,7 +4,9 @@
//! client (unlike Vandermonde RS, whose parity is not interoperable). Hard ceiling: data +
//! recovery ≤ 255 shards/block.
use super::{validate_block_shape, validate_encode_shape, ErasureCoder, FecError};
use super::{
validate_block_shape, validate_encode_shape, validate_into_shape, ErasureCoder, FecError,
};
use crate::config::FecScheme;
use fec_rs::ReedSolomon;
@@ -56,6 +58,44 @@ impl ErasureCoder for Gf8Coder {
.map_err(|_| FecError::Backend("gf8 reconstruct"))?;
collect_originals(received, data_count)
}
fn reconstruct_into(
&self,
recovery_count: usize,
data: &mut [&mut [u8]],
have: &[bool],
recovery: &[(usize, &[u8])],
) -> Result<(), FecError> {
validate_into_shape(data, have, recovery, recovery_count)?;
if have.iter().all(|h| *h) {
return Ok(());
}
// Legacy-scheme shim: fec-rs reconstructs through owned `Option<Vec<u8>>` slots, so copy
// the present shards into that shape and the recovered ones back out. Only P1/gf8
// sessions on loss pay this — the hot gf16 path decodes straight into the caller's slots.
let data_count = data.len();
let mut received: Vec<Option<Vec<u8>>> = Vec::with_capacity(data_count + recovery_count);
for (s, h) in data.iter().zip(have) {
received.push(h.then(|| s.to_vec()));
}
received.resize(data_count + recovery_count, None);
for &(j, bytes) in recovery {
received[data_count + j] = Some(bytes.to_vec());
}
let rs = ReedSolomon::new(data_count, recovery_count)
.map_err(|_| FecError::Config("invalid GF(2^8) shard counts"))?;
rs.reconstruct_data(&mut received)
.map_err(|_| FecError::Backend("gf8 reconstruct"))?;
for (i, h) in have.iter().enumerate() {
if !*h {
let shard = received[i]
.as_ref()
.ok_or(FecError::Backend("reconstruction left an original missing"))?;
data[i].copy_from_slice(shard);
}
}
Ok(())
}
}
fn collect_originals(