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(
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 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 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#[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 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 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 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 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 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}