1pub const DEFAULT_CHUNK_BYTES: usize = 64 * 1024;
9
10#[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 pub const fn slot_payload_bytes(self) -> usize {
36 self.slot_payload_bytes
37 }
38
39 pub const fn slots_per_lane(self) -> usize {
41 self.slots_per_lane
42 }
43
44 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 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 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 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 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#[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 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 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 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 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 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}