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 { self.slots_per_lane } else { slots })
69 }
70
71 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 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 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#[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 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 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 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 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 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}