Skip to main content

subetha_cxc/
interleave.rs

1//! Block interleaving: a sender-side transmit-order permutation that
2//! converts a burst loss into a spread loss the per-block FEC can
3//! recover.
4//!
5//! Without interleaving, the k+r datagrams of one block ship
6//! consecutively, so a burst that drops B consecutive packets removes B
7//! shards from one block - and if B > r, that block is unrecoverable.
8//! With interleave depth D, the datagrams of D blocks ship column-major:
9//! shard 0 of blocks 0..D, then shard 1 of blocks 0..D, and so on. The
10//! datagrams of any one block are then spaced D apart on the wire, so a
11//! burst of up to D consecutive losses removes at most ONE shard from
12//! each block - back inside FEC's r-parity budget.
13//!
14//! This is purely a sender-side reordering. The receiver routes every
15//! datagram by its `(block_id, shard_index)` header
16//! ([`crate::reliable_udp::Decoder`]), so it reassembles correctly
17//! regardless of arrival order and needs no de-interleave logic.
18//!
19//! Cost: the interleaver buffers up to D blocks before emitting the
20//! first datagram, trading D-block latency for burst tolerance. D is a
21//! control-table knob ([`crate::control_table::ControlTable`]); D = 1 is
22//! pass-through with no added latency.
23
24/// Buffers up to `depth` blocks of datagrams and emits them column-major
25/// so each block's shards are spaced `depth` apart on the wire.
26#[derive(Debug)]
27pub struct Interleaver {
28    depth: usize,
29    /// Up to `depth` blocks, each a list of that block's datagrams.
30    pending: Vec<Vec<Vec<u8>>>,
31}
32
33impl Interleaver {
34    /// Create an interleaver of the given depth (clamped to at least 1).
35    pub fn new(depth: usize) -> Self {
36        Self {
37            depth: depth.max(1),
38            pending: Vec::new(),
39        }
40    }
41
42    /// Current interleave depth.
43    pub fn depth(&self) -> usize {
44        self.depth
45    }
46
47    /// Change the interleave depth. Any buffered blocks are flushed
48    /// first (returned to the caller) so the depth change does not
49    /// reorder a partially-staged group.
50    pub fn set_depth(&mut self, depth: usize) -> Vec<Vec<u8>> {
51        let flushed = self.flush();
52        self.depth = depth.max(1);
53        flushed
54    }
55
56    /// Number of blocks currently buffered.
57    pub fn buffered_blocks(&self) -> usize {
58        self.pending.len()
59    }
60
61    /// Stage one block's datagrams. Returns the interleaved datagrams
62    /// ready to transmit: empty until `depth` blocks are buffered, then
63    /// the whole interleaved group. At depth 1 the block is returned
64    /// immediately (pass-through, no added latency).
65    pub fn add_block(&mut self, datagrams: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
66        if datagrams.is_empty() {
67            return Vec::new();
68        }
69        self.pending.push(datagrams);
70        if self.pending.len() >= self.depth {
71            self.emit()
72        } else {
73            Vec::new()
74        }
75    }
76
77    /// Emit whatever blocks are buffered (a short final group),
78    /// interleaved. Empty if nothing is staged.
79    pub fn flush(&mut self) -> Vec<Vec<u8>> {
80        if self.pending.is_empty() {
81            Vec::new()
82        } else {
83            self.emit()
84        }
85    }
86
87    /// Column-major emit of the buffered blocks, then clear.
88    fn emit(&mut self) -> Vec<Vec<u8>> {
89        let mut blocks = std::mem::take(&mut self.pending);
90        let max_len = blocks.iter().map(|b| b.len()).max().unwrap_or(0);
91        let total: usize = blocks.iter().map(|b| b.len()).sum();
92        let mut out = Vec::with_capacity(total);
93        // For each shard column, emit that column's datagram from every block
94        // that has one. Block i's shards land at output positions separated by
95        // the number of blocks, so a burst of <= depth consecutive losses hits
96        // at most one shard per block. The datagram is MOVED out, not cloned:
97        // `blocks` is owned here and dropped on return, so swapping in an empty
98        // Vec transfers ownership with no per-datagram allocation or byte copy.
99        for col in 0..max_len {
100            for block in blocks.iter_mut() {
101                if col < block.len() {
102                    out.push(std::mem::take(&mut block[col]));
103                }
104            }
105        }
106        out
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    /// Build `n` "blocks" of `shards` tiny datagrams each; each datagram
115    /// encodes (block_index, shard_index) in its first two bytes so the
116    /// permutation and burst properties are checkable.
117    fn make_blocks(n: usize, shards: usize) -> Vec<Vec<Vec<u8>>> {
118        (0..n)
119            .map(|b| (0..shards).map(|s| vec![b as u8, s as u8]).collect())
120            .collect()
121    }
122
123    #[test]
124    fn depth_one_is_passthrough() {
125        let mut il = Interleaver::new(1);
126        let blocks = make_blocks(1, 5);
127        let out = il.add_block(blocks[0].clone());
128        assert_eq!(out, blocks[0], "depth 1 emits the block immediately");
129        assert_eq!(il.buffered_blocks(), 0);
130    }
131
132    #[test]
133    fn emits_when_depth_reached_and_is_a_permutation() {
134        let depth = 4;
135        let shards = 6;
136        let blocks = make_blocks(depth, shards);
137        let mut il = Interleaver::new(depth);
138        let mut out = Vec::new();
139        for (i, b) in blocks.iter().enumerate() {
140            let emitted = il.add_block(b.clone());
141            if i < depth - 1 {
142                assert!(emitted.is_empty(), "no emit before depth reached");
143            } else {
144                out = emitted;
145            }
146        }
147        // Output is a permutation of all input datagrams (no loss/dup).
148        let mut got = out.clone();
149        let mut want: Vec<Vec<u8>> = blocks.into_iter().flatten().collect();
150        got.sort();
151        want.sort();
152        assert_eq!(got, want, "interleave is a permutation of the input");
153    }
154
155    /// The core property: with depth D, any window of D consecutive
156    /// emitted datagrams contains at most ONE shard from any block.
157    fn burst_property(depth: usize, shards: usize) {
158        let blocks = make_blocks(depth, shards);
159        let mut il = Interleaver::new(depth);
160        let mut out = Vec::new();
161        for b in &blocks {
162            out.extend(il.add_block(b.clone()));
163        }
164        out.extend(il.flush());
165        assert_eq!(out.len(), depth * shards);
166        // Slide a window of `depth` and count per-block hits.
167        for start in 0..=out.len() - depth {
168            let mut per_block = vec![0u32; depth];
169            for pkt in &out[start..start + depth] {
170                per_block[pkt[0] as usize] += 1;
171            }
172            assert!(
173                per_block.iter().all(|&c| c <= 1),
174                "depth={depth} shards={shards} window@{start}: a block lost >1 shard to a burst of {depth}"
175            );
176        }
177    }
178
179    #[test]
180    fn burst_of_depth_hits_at_most_one_shard_per_block() {
181        burst_property(4, 6);
182        burst_property(8, 10);
183        burst_property(3, 3);
184        burst_property(16, 8);
185    }
186
187    #[test]
188    fn set_depth_flushes_pending() {
189        let mut il = Interleaver::new(4);
190        let blocks = make_blocks(2, 5); // fewer than depth
191        il.add_block(blocks[0].clone());
192        let flushed = il.add_block(blocks[1].clone());
193        assert!(flushed.is_empty(), "2 of 4 staged, nothing emitted yet");
194        let out = il.set_depth(2);
195        assert_eq!(out.len(), 10, "changing depth flushes the 2 staged blocks");
196        assert_eq!(il.depth(), 2);
197        assert_eq!(il.buffered_blocks(), 0);
198    }
199}
200
201/// Gilbert-Elliott burst-loss A/B: interleaving must cut the ARQ load it
202/// would otherwise take to recover bursts, by spreading each burst so the
203/// per-block FEC absorbs it. Both depths deliver exactly (ARQ is the
204/// floor); the metric is how many retransmits each needed.
205#[cfg(test)]
206mod gilbert_elliott {
207    use super::Interleaver;
208    use crate::reliable_udp::{Decoder, Encoder};
209
210    /// Two-state burst channel. Probabilities are per-1000.
211    struct Ge {
212        bad: bool,
213        rng: u64,
214        p_gb: u32, // good -> bad
215        p_bg: u32, // bad -> good (mean burst length = 1000 / p_bg)
216        p_b: u32,  // loss while bad
217        p_g: u32,  // loss while good
218    }
219
220    impl Ge {
221        fn new(seed: u64) -> Self {
222            Self { bad: false, rng: seed | 1, p_gb: 30, p_bg: 200, p_b: 900, p_g: 0 }
223        }
224        fn rand(&mut self) -> u32 {
225            self.rng = self
226                .rng
227                .wrapping_mul(6364136223846793005)
228                .wrapping_add(1442695040888963407);
229            (self.rng >> 33) as u32
230        }
231        /// Advance the state, then decide loss for one datagram.
232        fn drop(&mut self) -> bool {
233            if self.bad {
234                if self.rand() % 1000 < self.p_bg {
235                    self.bad = false;
236                }
237            } else if self.rand() % 1000 < self.p_gb {
238                self.bad = true;
239            }
240            let p = if self.bad { self.p_b } else { self.p_g };
241            self.rand() % 1000 < p
242        }
243    }
244
245    /// Encode `n` items at the given interleave depth, push the
246    /// interleaved stream through the GE channel, and drive ARQ (also
247    /// lossy) to completion. Returns (delivered-exactly, retransmits).
248    fn run(depth: usize, n: u64, seed: u64) -> (bool, usize) {
249        let (k, r) = (8usize, 2usize);
250        let mut enc = Encoder::new(k, r, 8);
251        let mut il = Interleaver::new(depth);
252        let mut dec = Decoder::new();
253
254        // Build the interleaved wire order (all blocks sealed before any
255        // feedback, so parity r stays constant at 2 throughout).
256        let mut wire: Vec<Vec<u8>> = Vec::new();
257        for i in 0..n {
258            let block = enc.push(&i.to_le_bytes());
259            if !block.is_empty() {
260                wire.extend(il.add_block(block));
261            }
262        }
263        let tail = enc.flush();
264        if !tail.is_empty() {
265            wire.extend(il.add_block(tail));
266        }
267        wire.extend(il.flush());
268
269        let mut ge = Ge::new(seed);
270        let mut delivered: Vec<u64> = Vec::new();
271        for pkt in &wire {
272            if ge.drop() {
273                continue;
274            }
275            for it in dec.on_packet(pkt) {
276                delivered.push(u64::from_le_bytes(it.try_into().unwrap()));
277            }
278        }
279
280        // ARQ loop: retransmits also traverse the GE channel.
281        let mut retransmits = 0usize;
282        let mut rounds = 0u32;
283        while (delivered.len() as u64) < n {
284            rounds += 1;
285            assert!(rounds < 20_000, "no convergence at depth {depth}");
286            let fb = dec.feedback(true);
287            for pkt in enc.on_feedback(&fb) {
288                retransmits += 1;
289                if ge.drop() {
290                    continue;
291                }
292                for it in dec.on_packet(&pkt) {
293                    delivered.push(u64::from_le_bytes(it.try_into().unwrap()));
294                }
295            }
296        }
297        let ok = delivered == (0..n).collect::<Vec<_>>();
298        (ok, retransmits)
299    }
300
301    #[test]
302    fn interleaving_cuts_arq_under_bursty_loss() {
303        let n = 240;
304        let seed = 0x00C0_FFEE_1234_5678;
305        let (ok1, rtx1) = run(1, n, seed);
306        let (ok8, rtx8) = run(8, n, seed);
307        assert!(ok1 && ok8, "both deliver exactly via the ARQ floor");
308        assert!(
309            rtx8 < rtx1,
310            "interleaving must cut ARQ under bursts: depth8={rtx8} retransmits vs depth1={rtx1}"
311        );
312    }
313}