1use serde::{Deserialize, Serialize};
2
3use crate::error::{FootprintOverflow, MemoryError, MemoryResult};
4
5#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
7pub struct MemoryDecision {
8 pub label: String,
10 pub fixed_bytes: u64,
12 pub bytes_per_event: u64,
14 pub chunk_events: usize,
16 pub estimated_peak_bytes: u64,
18 pub actual_high_water_bytes: Option<u64>,
20 pub strategy: String,
22}
23
24#[doc(hidden)]
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct MemoryFootprint {
28 pub fixed_bytes: u64,
30 pub bytes_per_event: u64,
32}
33
34impl MemoryFootprint {
35 pub const fn new(fixed_bytes: u64, bytes_per_event: u64) -> Self {
37 Self {
38 fixed_bytes,
39 bytes_per_event,
40 }
41 }
42 pub const fn fixed(fixed_bytes: u64) -> Self {
44 Self::new(fixed_bytes, 0)
45 }
46 pub const fn per_event(bytes_per_event: u64) -> Self {
48 Self::new(0, bytes_per_event)
49 }
50 pub fn from_usize(fixed_bytes: usize, bytes_per_event: usize) -> Self {
52 Self::from_usize_checked(fixed_bytes, bytes_per_event)
53 .unwrap_or(Self::new(u64::MAX, u64::MAX))
54 }
55 pub fn from_usize_checked(
63 fixed_bytes: usize,
64 bytes_per_event: usize,
65 ) -> Result<Self, FootprintOverflow> {
66 Ok(Self::new(
67 checked_u64(fixed_bytes)?,
68 checked_u64(bytes_per_event)?,
69 ))
70 }
71 pub const fn checked_add(self, other: Self) -> Result<Self, FootprintOverflow> {
78 let fixed_bytes = match self.fixed_bytes.checked_add(other.fixed_bytes) {
79 Some(value) => value,
80 None => return Err(FootprintOverflow::Addition),
81 };
82 let bytes_per_event = match self.bytes_per_event.checked_add(other.bytes_per_event) {
83 Some(value) => value,
84 None => return Err(FootprintOverflow::Addition),
85 };
86 Ok(Self::new(fixed_bytes, bytes_per_event))
87 }
88 pub const fn checked_scale(self, factor: u64) -> Result<Self, FootprintOverflow> {
96 let fixed_bytes = match self.fixed_bytes.checked_mul(factor) {
97 Some(value) => value,
98 None => return Err(FootprintOverflow::Multiplication),
99 };
100 let bytes_per_event = match self.bytes_per_event.checked_mul(factor) {
101 Some(value) => value,
102 None => return Err(FootprintOverflow::Multiplication),
103 };
104 Ok(Self::new(fixed_bytes, bytes_per_event))
105 }
106 pub fn checked_scale_usize(self, factor: usize) -> Result<Self, FootprintOverflow> {
113 self.checked_scale(checked_u64(factor)?)
114 }
115 pub fn checked_peak_bytes(self, events: usize) -> Result<u64, FootprintOverflow> {
122 let events = checked_u64(events)?;
123 let event_bytes = self
124 .bytes_per_event
125 .checked_mul(events)
126 .ok_or(FootprintOverflow::Multiplication)?;
127 self.fixed_bytes
128 .checked_add(event_bytes)
129 .ok_or(FootprintOverflow::Addition)
130 }
131 pub fn peak_bytes(self, events: usize) -> u64 {
133 self.checked_peak_bytes(events).unwrap_or(u64::MAX)
134 }
135 fn normalized(mut self) -> Self {
136 self.bytes_per_event = self.bytes_per_event.max(1);
137 self
138 }
139}
140
141#[doc(hidden)]
143#[derive(Clone, Debug, PartialEq, Eq)]
144pub struct MemoryFitRequest {
145 pub label: String,
147 pub footprint: MemoryFootprint,
149 pub available_bytes: u64,
151 pub event_limit: usize,
153 pub strategy: String,
155}
156
157impl MemoryDecision {
158 pub fn fit(
165 label: impl Into<String>,
166 fixed_bytes: u64,
167 bytes_per_event: u64,
168 available_bytes: u64,
169 event_limit: usize,
170 strategy: impl Into<String>,
171 ) -> MemoryResult<Self> {
172 MemoryFitRequest {
173 label: label.into(),
174 footprint: MemoryFootprint::new(fixed_bytes, bytes_per_event),
175 available_bytes,
176 event_limit,
177 strategy: strategy.into(),
178 }
179 .evaluate()
180 }
181}
182
183impl MemoryFitRequest {
184 pub fn evaluate(mut self) -> MemoryResult<MemoryDecision> {
191 self.footprint = self.footprint.normalized();
192 if self.event_limit == 0 {
193 return Ok(self.decision(0));
194 }
195 let event_capacity = self
196 .available_bytes
197 .saturating_sub(self.footprint.fixed_bytes);
198 let events =
199 saturating_usize(event_capacity / self.footprint.bytes_per_event).min(self.event_limit);
200 if events == 0 {
201 return Err(MemoryError::BudgetExceeded {
202 resource: self.label,
203 requested: self
204 .footprint
205 .fixed_bytes
206 .saturating_add(self.footprint.bytes_per_event),
207 remaining: self.available_bytes,
208 });
209 }
210 Ok(self.decision(events))
211 }
212
213 pub fn evaluate_resident(self, chunk_events: usize) -> MemoryResult<MemoryDecision> {
220 let peak = self.footprint.peak_bytes(self.event_limit);
221 if peak > self.available_bytes {
222 return Err(MemoryError::BudgetExceeded {
223 resource: self.label,
224 requested: peak,
225 remaining: self.available_bytes,
226 });
227 }
228 Ok(self.decision_with_peak(chunk_events, peak))
229 }
230
231 fn decision(self, events: usize) -> MemoryDecision {
232 let peak = self.footprint.peak_bytes(events);
233 self.decision_with_peak(events, peak)
234 }
235
236 fn decision_with_peak(self, chunk_events: usize, peak: u64) -> MemoryDecision {
237 MemoryDecision {
238 label: self.label,
239 fixed_bytes: self.footprint.fixed_bytes,
240 bytes_per_event: self.footprint.bytes_per_event,
241 chunk_events,
242 estimated_peak_bytes: peak,
243 actual_high_water_bytes: None,
244 strategy: self.strategy,
245 }
246 }
247}
248
249fn saturating_usize(value: u64) -> usize {
250 usize::try_from(value).unwrap_or(usize::MAX)
251}
252fn checked_u64(value: usize) -> Result<u64, FootprintOverflow> {
253 u64::try_from(value).map_err(|_| FootprintOverflow::Conversion)
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn fits_the_largest_safe_chunk() {
262 let decision = MemoryDecision::fit("test", 100, 8, 1_000, 1_000, "streaming").unwrap();
263 assert_eq!(decision.chunk_events, 112);
264 assert_eq!(decision.estimated_peak_bytes, 996);
265 }
266
267 #[test]
268 fn defines_zero_and_capacity_boundary_policy() {
269 let zero = MemoryDecision::fit("zero", 101, 0, 100, 0, "empty").unwrap();
270 assert_eq!(
271 (
272 zero.bytes_per_event,
273 zero.chunk_events,
274 zero.estimated_peak_bytes
275 ),
276 (1, 0, 101)
277 );
278 let normalized = MemoryDecision::fit("normalized", 0, 0, 100, 200, "streaming").unwrap();
279 assert_eq!(
280 (
281 normalized.bytes_per_event,
282 normalized.chunk_events,
283 normalized.estimated_peak_bytes
284 ),
285 (1, 100, 100)
286 );
287 assert!(MemoryDecision::fit("full", 100, 1, 100, 1, "streaming").is_err());
288 assert!(MemoryDecision::fit("full", 101, 1, 100, 1, "streaming").is_err());
289 }
290
291 #[test]
292 fn saturates_at_integer_boundaries() {
293 let decision =
294 MemoryDecision::fit("maximum", 0, 1, u64::MAX, usize::MAX, "resident").unwrap();
295 assert_eq!(
296 decision.chunk_events,
297 usize::try_from(u64::MAX).unwrap_or(usize::MAX)
298 );
299 assert_eq!(decision.estimated_peak_bytes, u64::MAX);
300 assert_eq!(
301 MemoryFootprint::new(u64::MAX, u64::MAX).peak_bytes(usize::MAX),
302 u64::MAX
303 );
304 assert_eq!(
305 MemoryDecision::fit("overflow", u64::MAX, 1, u64::MAX, 1, "streaming"),
306 Err(MemoryError::BudgetExceeded {
307 resource: "overflow".into(),
308 requested: u64::MAX,
309 remaining: u64::MAX,
310 })
311 );
312 }
313
314 #[test]
315 fn named_and_resident_requests_share_policy() {
316 let request = MemoryFitRequest {
317 label: "named".into(),
318 footprint: MemoryFootprint::from_usize(100, 8),
319 available_bytes: 1_000,
320 event_limit: 100,
321 strategy: "resident".into(),
322 };
323 assert_eq!(request.clone().evaluate().unwrap().chunk_events, 100);
324 let decision = request.evaluate_resident(25).unwrap();
325 assert_eq!(
326 (decision.chunk_events, decision.estimated_peak_bytes),
327 (25, 900)
328 );
329 }
330
331 #[test]
332 fn resident_requests_check_the_full_footprint() {
333 let request = MemoryFitRequest {
334 label: "resident".into(),
335 footprint: MemoryFootprint::new(100, 8),
336 available_bytes: 899,
337 event_limit: 100,
338 strategy: "resident".into(),
339 };
340 assert_eq!(
341 request.evaluate_resident(25),
342 Err(MemoryError::BudgetExceeded {
343 resource: "resident".into(),
344 requested: 900,
345 remaining: 899,
346 })
347 );
348
349 let zero_per_event = MemoryFitRequest {
350 label: "resident".into(),
351 footprint: MemoryFootprint::new(100, 0),
352 available_bytes: 100,
353 event_limit: 100,
354 strategy: "resident".into(),
355 }
356 .evaluate_resident(25)
357 .unwrap();
358 assert_eq!(
359 (
360 zero_per_event.bytes_per_event,
361 zero_per_event.estimated_peak_bytes
362 ),
363 (0, 100)
364 );
365 }
366}