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