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 {
69            self.slots_per_lane
70        } else {
71            slots
72        })
73    }
74
75    /// Effective chunk capacity for a runtime byte target. This is always
76    /// `n * slot_payload_bytes` for a valid lane-bounded `n`.
77    pub const fn chunk_bytes_for_target(
78        self,
79        target_bytes: usize,
80    ) -> Option<usize> {
81        let slots = match self.slots_for_target(target_bytes) {
82            Some(slots) => slots,
83            None => return None,
84        };
85        self.chunk_bytes(slots)
86    }
87
88    /// Plan a known payload using a runtime-selected slot count.
89    pub const fn plan(
90        self,
91        data_bytes: usize,
92        slots_per_chunk: usize,
93    ) -> Option<ChunkPlan> {
94        let chunk_bytes = match self.chunk_bytes(slots_per_chunk) {
95            Some(bytes) => bytes,
96            None => return None,
97        };
98        ChunkPlan::for_limit(data_bytes, self.slot_payload_bytes, chunk_bytes)
99    }
100
101    /// Plan a known payload from a runtime byte target after aligning it to
102    /// the physical slot payload geometry.
103    pub const fn plan_for_target(
104        self,
105        data_bytes: usize,
106        target_bytes: usize,
107    ) -> Option<ChunkPlan> {
108        let slots = match self.slots_for_target(target_bytes) {
109            Some(slots) => slots,
110            None => return None,
111        };
112        self.plan(data_bytes, slots)
113    }
114}
115
116/// A request/response-agnostic plan for splitting a known payload into chunks.
117#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
118pub struct ChunkPlan {
119    data_bytes: usize,
120    chunk_bytes: usize,
121    chunk_count: usize,
122    slots_per_chunk: usize,
123    tail_bytes: usize,
124    tail_slots: usize,
125}
126
127impl ChunkPlan {
128    /// Plan with the benchmark-backed 64 KiB target.
129    pub const fn new(
130        data_bytes: usize,
131        slot_bytes: usize,
132    ) -> Option<Self> {
133        Self::for_limit(data_bytes, slot_bytes, DEFAULT_CHUNK_BYTES)
134    }
135
136    /// Plan with a runtime-selected number of physical slots per chunk.
137    pub const fn for_slots(
138        data_bytes: usize,
139        slot_payload_bytes: usize,
140        slots_per_chunk: usize,
141    ) -> Option<Self> {
142        let geometry =
143            match ChunkGeometry::new(slot_payload_bytes, slots_per_chunk) {
144                Some(geometry) => geometry,
145                None => return None,
146            };
147        geometry.plan(data_bytes, slots_per_chunk)
148    }
149
150    pub(crate) const fn for_limit(
151        data_bytes: usize,
152        slot_bytes: usize,
153        limit_bytes: usize,
154    ) -> Option<Self> {
155        if slot_bytes == 0 || limit_bytes == 0 {
156            return None;
157        }
158        let target =
159            if limit_bytes < slot_bytes { slot_bytes } else { limit_bytes };
160        let chunk_bytes = (target / slot_bytes) * slot_bytes;
161        if data_bytes == 0 {
162            return Some(Self {
163                data_bytes,
164                chunk_bytes,
165                chunk_count: 0,
166                slots_per_chunk: chunk_bytes / slot_bytes,
167                tail_bytes: 0,
168                tail_slots: 0,
169            });
170        }
171        let chunk_count = data_bytes.div_ceil(chunk_bytes);
172        let remainder = data_bytes % chunk_bytes;
173        let tail_bytes = if remainder == 0 { chunk_bytes } else { remainder };
174        Some(Self {
175            data_bytes,
176            chunk_bytes,
177            chunk_count,
178            slots_per_chunk: chunk_bytes / slot_bytes,
179            tail_bytes,
180            tail_slots: tail_bytes.div_ceil(slot_bytes),
181        })
182    }
183
184    pub const fn data_bytes(self) -> usize {
185        self.data_bytes
186    }
187
188    /// Maximum bytes in each full publication.
189    pub const fn chunk_bytes(self) -> usize {
190        self.chunk_bytes
191    }
192
193    pub const fn chunk_count(self) -> usize {
194        self.chunk_count
195    }
196
197    pub const fn slots_per_chunk(self) -> usize {
198        self.slots_per_chunk
199    }
200
201    /// Exact bytes in the last publication, or zero for an empty payload.
202    pub const fn tail_bytes(self) -> usize {
203        self.tail_bytes
204    }
205
206    pub const fn tail_slots(self) -> usize {
207        self.tail_slots
208    }
209
210    /// Exact length of chunk `index`, if it exists.
211    pub const fn chunk_len(
212        self,
213        index: usize,
214    ) -> Option<usize> {
215        if index >= self.chunk_count {
216            None
217        } else if index + 1 == self.chunk_count {
218            Some(self.tail_bytes)
219        } else {
220            Some(self.chunk_bytes)
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn default_plan_uses_benchmark_chunk_and_exact_tail() {
231        let plan = ChunkPlan::new(150 * 1024, 4 * 1024).expect("plan");
232        assert_eq!(plan.chunk_bytes(), 64 * 1024);
233        assert_eq!(plan.slots_per_chunk(), 16);
234        assert_eq!(plan.chunk_count(), 3);
235        assert_eq!(plan.tail_bytes(), 22 * 1024);
236        assert_eq!(plan.tail_slots(), 6);
237        assert_eq!(plan.chunk_len(0), Some(64 * 1024));
238        assert_eq!(plan.chunk_len(2), Some(22 * 1024));
239        assert_eq!(plan.chunk_len(3), None);
240    }
241
242    #[test]
243    fn a_slot_larger_than_the_default_is_never_split_artificially() {
244        let plan = ChunkPlan::new(1024 * 1024, 128 * 1024).expect("plan");
245        assert_eq!(plan.chunk_bytes(), 128 * 1024);
246        assert_eq!(plan.slots_per_chunk(), 1);
247        assert_eq!(plan.chunk_count(), 8);
248    }
249
250    #[test]
251    fn small_and_empty_payloads_are_well_defined() {
252        let small = ChunkPlan::new(1024, 4 * 1024).expect("plan");
253        assert_eq!(small.chunk_count(), 1);
254        assert_eq!(small.tail_bytes(), 1024);
255        assert_eq!(small.tail_slots(), 1);
256
257        let empty = ChunkPlan::new(0, 4 * 1024).expect("plan");
258        assert_eq!(empty.chunk_count(), 0);
259        assert_eq!(empty.tail_bytes(), 0);
260    }
261
262    #[test]
263    fn arena_plan_clamps_full_chunks_to_one_lane() {
264        let spec = crate::exchange::PayloadArenaSpec::new(200, 4, 4 * 1024);
265        let plan = spec.plan_chunks(1024 * 1024).expect("plan");
266        assert_eq!(plan.chunk_bytes(), 16 * 1024);
267        assert_eq!(plan.slots_per_chunk(), 4);
268        assert_eq!(plan.chunk_count(), 64);
269    }
270
271    #[test]
272    fn runtime_slot_count_defines_exact_chunk_capacity() {
273        let geometry = ChunkGeometry::new(4096, 32).expect("geometry");
274
275        assert_eq!(geometry.slot_payload_bytes(), 4096);
276        assert_eq!(geometry.chunk_bytes(7), Some(7 * 4096));
277        assert_eq!(geometry.chunk_bytes(0), None);
278        assert_eq!(geometry.chunk_bytes(33), None);
279
280        let plan = geometry.plan(70_000, 7).expect("plan");
281        assert_eq!(plan.chunk_bytes(), 7 * 4096);
282        assert_eq!(plan.slots_per_chunk(), 7);
283        assert_eq!(plan.chunk_count(), 3);
284    }
285
286    #[test]
287    fn byte_target_is_aligned_and_clamped_to_runtime_geometry() {
288        let geometry = ChunkGeometry::new(4096, 8).expect("geometry");
289
290        assert_eq!(geometry.slots_for_target(1), Some(1));
291        assert_eq!(geometry.slots_for_target(10_000), Some(2));
292        assert_eq!(geometry.slots_for_target(1024 * 1024), Some(8));
293        assert_eq!(geometry.chunk_bytes_for_target(10_000), Some(8192));
294        assert_eq!(geometry.chunk_bytes_for_target(1024 * 1024), Some(32_768));
295        assert_eq!(geometry.slots_for_target(0), None);
296    }
297}