Skip to main content

orbit_stream/exchange/
chunk.rs

1/// Benchmark-backed default publication target.
2///
3/// The same-host SHM exchange benchmarks show 64 KiB as the useful
4/// general-purpose boundary: it amortizes one descriptor/readiness cycle
5/// without turning a normal payload into one very large credit reservation.
6/// It is deterministic policy about our internal data path, not an
7/// external-network heuristic and not wire geometry.
8pub const DEFAULT_CHUNK_BYTES: usize = 64 * 1024;
9
10/// Physical payload geometry available to one exchange direction.
11///
12/// Runtime policy chooses how many slots one publication should span. The
13/// resulting chunk capacity is always an exact multiple of the physical slot
14/// payload size: `slots * slot_payload_bytes`.
15#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
16pub struct ChunkGeometry {
17    slot_payload_bytes: usize,
18    slots_per_lane: usize
19}
20
21impl ChunkGeometry {
22    pub const fn new(
23        slot_payload_bytes: usize,
24        slots_per_lane: usize
25    ) -> Option<Self> {
26        if slot_payload_bytes == 0 || slots_per_lane == 0 {
27            None
28        } else {
29            Some(Self { slot_payload_bytes, slots_per_lane })
30        }
31    }
32
33    /// Usable payload bytes in one physical slot. Slot metadata lives outside
34    /// this capacity.
35    pub const fn slot_payload_bytes(self) -> usize {
36        self.slot_payload_bytes
37    }
38
39    /// Maximum slots one publication may reserve from this node's lane.
40    pub const fn slots_per_lane(self) -> usize {
41        self.slots_per_lane
42    }
43
44    /// Exact publication capacity for a runtime-selected slot count.
45    pub const fn chunk_bytes(
46        self,
47        slots: usize
48    ) -> Option<usize> {
49        if slots == 0 || slots > self.slots_per_lane {
50            None
51        } else {
52            self.slot_payload_bytes.checked_mul(slots)
53        }
54    }
55
56    /// Convert a byte target into a valid runtime slot count. The target is
57    /// rounded down to whole slots, but never below one slot, and clamped to
58    /// the node lane.
59    pub const fn slots_for_target(
60        self,
61        target_bytes: usize
62    ) -> Option<usize> {
63        if target_bytes == 0 {
64            return None;
65        }
66        let slots = target_bytes / self.slot_payload_bytes;
67        let slots = if slots == 0 { 1 } else { slots };
68        Some(if slots > self.slots_per_lane { self.slots_per_lane } else { slots })
69    }
70
71    /// Effective chunk capacity for a runtime byte target. This is always
72    /// `n * slot_payload_bytes` for a valid lane-bounded `n`.
73    pub const fn chunk_bytes_for_target(self, target_bytes: usize) -> Option<usize> {
74        let slots = match self.slots_for_target(target_bytes) {
75            Some(slots) => slots,
76            None => return None
77        };
78        self.chunk_bytes(slots)
79    }
80
81    /// Plan a known payload using a runtime-selected slot count.
82    pub const fn plan(
83        self,
84        data_bytes: usize,
85        slots_per_chunk: usize
86    ) -> Option<ChunkPlan> {
87        let chunk_bytes = match self.chunk_bytes(slots_per_chunk) {
88            Some(bytes) => bytes,
89            None => return None
90        };
91        ChunkPlan::for_limit(data_bytes, self.slot_payload_bytes, chunk_bytes)
92    }
93
94    /// Plan a known payload from a runtime byte target after aligning it to
95    /// the physical slot payload geometry.
96    pub const fn plan_for_target(
97        self,
98        data_bytes: usize,
99        target_bytes: usize
100    ) -> Option<ChunkPlan> {
101        let slots = match self.slots_for_target(target_bytes) {
102            Some(slots) => slots,
103            None => return None
104        };
105        self.plan(data_bytes, slots)
106    }
107}
108
109/// A request/response-agnostic plan for splitting a known payload into chunks.
110#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
111pub struct ChunkPlan {
112    data_bytes: usize,
113    chunk_bytes: usize,
114    chunk_count: usize,
115    slots_per_chunk: usize,
116    tail_bytes: usize,
117    tail_slots: usize
118}
119
120impl ChunkPlan {
121    /// Plan with the benchmark-backed 64 KiB target.
122    pub const fn new(
123        data_bytes: usize,
124        slot_bytes: usize
125    ) -> Option<Self> {
126        Self::for_limit(data_bytes, slot_bytes, DEFAULT_CHUNK_BYTES)
127    }
128
129    /// Plan with a runtime-selected number of physical slots per chunk.
130    pub const fn for_slots(
131        data_bytes: usize,
132        slot_payload_bytes: usize,
133        slots_per_chunk: usize
134    ) -> Option<Self> {
135        let geometry = match ChunkGeometry::new(slot_payload_bytes, slots_per_chunk) {
136            Some(geometry) => geometry,
137            None => return None
138        };
139        geometry.plan(data_bytes, slots_per_chunk)
140    }
141
142    pub(crate) const fn for_limit(
143        data_bytes: usize,
144        slot_bytes: usize,
145        limit_bytes: usize
146    ) -> Option<Self> {
147        if slot_bytes == 0 || limit_bytes == 0 {
148            return None;
149        }
150        let target = if limit_bytes < slot_bytes { slot_bytes } else { limit_bytes };
151        let chunk_bytes = (target / slot_bytes) * slot_bytes;
152        if data_bytes == 0 {
153            return Some(Self {
154                data_bytes,
155                chunk_bytes,
156                chunk_count: 0,
157                slots_per_chunk: chunk_bytes / slot_bytes,
158                tail_bytes: 0,
159                tail_slots: 0
160            });
161        }
162        let chunk_count = data_bytes.div_ceil(chunk_bytes);
163        let remainder = data_bytes % chunk_bytes;
164        let tail_bytes = if remainder == 0 { chunk_bytes } else { remainder };
165        Some(Self {
166            data_bytes,
167            chunk_bytes,
168            chunk_count,
169            slots_per_chunk: chunk_bytes / slot_bytes,
170            tail_bytes,
171            tail_slots: tail_bytes.div_ceil(slot_bytes)
172        })
173    }
174
175    pub const fn data_bytes(self) -> usize {
176        self.data_bytes
177    }
178
179    /// Maximum bytes in each full publication.
180    pub const fn chunk_bytes(self) -> usize {
181        self.chunk_bytes
182    }
183
184    pub const fn chunk_count(self) -> usize {
185        self.chunk_count
186    }
187
188    pub const fn slots_per_chunk(self) -> usize {
189        self.slots_per_chunk
190    }
191
192    /// Exact bytes in the last publication, or zero for an empty payload.
193    pub const fn tail_bytes(self) -> usize {
194        self.tail_bytes
195    }
196
197    pub const fn tail_slots(self) -> usize {
198        self.tail_slots
199    }
200
201    /// Exact length of chunk `index`, if it exists.
202    pub const fn chunk_len(
203        self,
204        index: usize
205    ) -> Option<usize> {
206        if index >= self.chunk_count {
207            None
208        } else if index + 1 == self.chunk_count {
209            Some(self.tail_bytes)
210        } else {
211            Some(self.chunk_bytes)
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn default_plan_uses_benchmark_chunk_and_exact_tail() {
222        let plan = ChunkPlan::new(150 * 1024, 4 * 1024).expect("plan");
223        assert_eq!(plan.chunk_bytes(), 64 * 1024);
224        assert_eq!(plan.slots_per_chunk(), 16);
225        assert_eq!(plan.chunk_count(), 3);
226        assert_eq!(plan.tail_bytes(), 22 * 1024);
227        assert_eq!(plan.tail_slots(), 6);
228        assert_eq!(plan.chunk_len(0), Some(64 * 1024));
229        assert_eq!(plan.chunk_len(2), Some(22 * 1024));
230        assert_eq!(plan.chunk_len(3), None);
231    }
232
233    #[test]
234    fn a_slot_larger_than_the_default_is_never_split_artificially() {
235        let plan = ChunkPlan::new(1024 * 1024, 128 * 1024).expect("plan");
236        assert_eq!(plan.chunk_bytes(), 128 * 1024);
237        assert_eq!(plan.slots_per_chunk(), 1);
238        assert_eq!(plan.chunk_count(), 8);
239    }
240
241    #[test]
242    fn small_and_empty_payloads_are_well_defined() {
243        let small = ChunkPlan::new(1024, 4 * 1024).expect("plan");
244        assert_eq!(small.chunk_count(), 1);
245        assert_eq!(small.tail_bytes(), 1024);
246        assert_eq!(small.tail_slots(), 1);
247
248        let empty = ChunkPlan::new(0, 4 * 1024).expect("plan");
249        assert_eq!(empty.chunk_count(), 0);
250        assert_eq!(empty.tail_bytes(), 0);
251    }
252
253    #[test]
254    fn arena_plan_clamps_full_chunks_to_one_lane() {
255        let spec = crate::exchange::PayloadArenaSpec::new(200, 4, 4 * 1024);
256        let plan = spec.plan_chunks(1024 * 1024).expect("plan");
257        assert_eq!(plan.chunk_bytes(), 16 * 1024);
258        assert_eq!(plan.slots_per_chunk(), 4);
259        assert_eq!(plan.chunk_count(), 64);
260    }
261
262    #[test]
263    fn runtime_slot_count_defines_exact_chunk_capacity() {
264        let geometry = ChunkGeometry::new(4096, 32).expect("geometry");
265
266        assert_eq!(geometry.slot_payload_bytes(), 4096);
267        assert_eq!(geometry.chunk_bytes(7), Some(7 * 4096));
268        assert_eq!(geometry.chunk_bytes(0), None);
269        assert_eq!(geometry.chunk_bytes(33), None);
270
271        let plan = geometry.plan(70_000, 7).expect("plan");
272        assert_eq!(plan.chunk_bytes(), 7 * 4096);
273        assert_eq!(plan.slots_per_chunk(), 7);
274        assert_eq!(plan.chunk_count(), 3);
275    }
276
277    #[test]
278    fn byte_target_is_aligned_and_clamped_to_runtime_geometry() {
279        let geometry = ChunkGeometry::new(4096, 8).expect("geometry");
280
281        assert_eq!(geometry.slots_for_target(1), Some(1));
282        assert_eq!(geometry.slots_for_target(10_000), Some(2));
283        assert_eq!(geometry.slots_for_target(1024 * 1024), Some(8));
284        assert_eq!(geometry.chunk_bytes_for_target(10_000), Some(8192));
285        assert_eq!(geometry.chunk_bytes_for_target(1024 * 1024), Some(32_768));
286        assert_eq!(geometry.slots_for_target(0), None);
287    }
288}