1use crate::memory_info::MemoryInfo;
38use oxicuda_driver::error::{CudaError, CudaResult};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub enum PressureLevel {
50 Nominal,
52 Warning,
54 Critical,
56}
57
58impl PressureLevel {
59 #[inline]
61 #[must_use]
62 pub fn is_critical(self) -> bool {
63 matches!(self, Self::Critical)
64 }
65
66 #[inline]
68 #[must_use]
69 pub fn is_elevated(self) -> bool {
70 self >= Self::Warning
71 }
72}
73
74impl std::fmt::Display for PressureLevel {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 Self::Nominal => write!(f, "nominal"),
78 Self::Warning => write!(f, "warning"),
79 Self::Critical => write!(f, "critical"),
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq)]
90pub struct PressureSample {
91 pub level: PressureLevel,
93 pub used_fraction: f64,
95 pub previous: PressureLevel,
97}
98
99impl PressureSample {
100 #[inline]
103 #[must_use]
104 pub fn escalated(&self) -> bool {
105 self.level > self.previous
106 }
107
108 #[inline]
111 #[must_use]
112 pub fn de_escalated(&self) -> bool {
113 self.level < self.previous
114 }
115}
116
117type EvictionHook = Box<dyn FnMut() -> usize + Send>;
124
125type TransitionHook = Box<dyn FnMut(PressureSample) + Send>;
127
128pub struct MemoryPressureMonitor {
136 warning_fraction: f64,
137 critical_fraction: f64,
138 current: PressureLevel,
139 total_evicted: usize,
141 eviction_count: u64,
143 eviction_hook: Option<EvictionHook>,
144 transition_hook: Option<TransitionHook>,
145}
146
147impl std::fmt::Debug for MemoryPressureMonitor {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_struct("MemoryPressureMonitor")
150 .field("warning_fraction", &self.warning_fraction)
151 .field("critical_fraction", &self.critical_fraction)
152 .field("current", &self.current)
153 .field("total_evicted", &self.total_evicted)
154 .field("eviction_count", &self.eviction_count)
155 .field("has_eviction_hook", &self.eviction_hook.is_some())
156 .field("has_transition_hook", &self.transition_hook.is_some())
157 .finish()
158 }
159}
160
161impl MemoryPressureMonitor {
162 pub fn new(warning_fraction: f64, critical_fraction: f64) -> CudaResult<Self> {
173 let valid = |x: f64| x.is_finite() && x > 0.0 && x <= 1.0;
174 if !valid(warning_fraction)
175 || !valid(critical_fraction)
176 || warning_fraction >= critical_fraction
177 {
178 return Err(CudaError::InvalidValue);
179 }
180 Ok(Self {
181 warning_fraction,
182 critical_fraction,
183 current: PressureLevel::Nominal,
184 total_evicted: 0,
185 eviction_count: 0,
186 eviction_hook: None,
187 transition_hook: None,
188 })
189 }
190
191 #[must_use]
197 pub fn on_eviction<F>(mut self, hook: F) -> Self
198 where
199 F: FnMut() -> usize + Send + 'static,
200 {
201 self.eviction_hook = Some(Box::new(hook));
202 self
203 }
204
205 #[must_use]
210 pub fn on_transition<F>(mut self, hook: F) -> Self
211 where
212 F: FnMut(PressureSample) + Send + 'static,
213 {
214 self.transition_hook = Some(Box::new(hook));
215 self
216 }
217
218 #[inline]
220 #[must_use]
221 pub fn warning_fraction(&self) -> f64 {
222 self.warning_fraction
223 }
224
225 #[inline]
227 #[must_use]
228 pub fn critical_fraction(&self) -> f64 {
229 self.critical_fraction
230 }
231
232 #[inline]
234 #[must_use]
235 pub fn level(&self) -> PressureLevel {
236 self.current
237 }
238
239 #[inline]
241 #[must_use]
242 pub fn total_evicted(&self) -> usize {
243 self.total_evicted
244 }
245
246 #[inline]
248 #[must_use]
249 pub fn eviction_count(&self) -> u64 {
250 self.eviction_count
251 }
252
253 #[must_use]
259 pub fn classify(&self, used_fraction: f64) -> PressureLevel {
260 if used_fraction >= self.critical_fraction {
261 PressureLevel::Critical
262 } else if used_fraction >= self.warning_fraction {
263 PressureLevel::Warning
264 } else {
265 PressureLevel::Nominal
266 }
267 }
268
269 pub fn observe(&mut self, info: MemoryInfo) -> PressureLevel {
276 let used_fraction = info.usage_fraction();
277 let previous = self.current;
278 let level = self.classify(used_fraction);
279 self.current = level;
280
281 if level != previous {
282 let sample = PressureSample {
283 level,
284 used_fraction,
285 previous,
286 };
287 if let Some(hook) = self.transition_hook.as_mut() {
288 hook(sample);
289 }
290 if level == PressureLevel::Critical && previous != PressureLevel::Critical {
292 if let Some(hook) = self.eviction_hook.as_mut() {
293 let reclaimed = hook();
294 self.total_evicted = self.total_evicted.saturating_add(reclaimed);
295 self.eviction_count = self.eviction_count.saturating_add(1);
296 }
297 }
298 }
299 level
300 }
301
302 pub fn poll(&mut self) -> CudaResult<PressureLevel> {
309 let info = crate::memory_info::memory_info()?;
310 Ok(self.observe(info))
311 }
312
313 pub fn reset(&mut self) {
316 self.current = PressureLevel::Nominal;
317 self.total_evicted = 0;
318 self.eviction_count = 0;
319 }
320}
321
322pub fn validate_thresholds(warning_fraction: f64, critical_fraction: f64) -> CudaResult<()> {
334 MemoryPressureMonitor::new(warning_fraction, critical_fraction).map(|_| ())
335}
336
337#[cfg(test)]
342mod tests {
343 use super::*;
344 use std::sync::Arc;
345 use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
346
347 fn info(free: usize, total: usize) -> MemoryInfo {
348 MemoryInfo { free, total }
349 }
350
351 #[test]
352 fn pressure_level_ordering() {
353 assert!(PressureLevel::Nominal < PressureLevel::Warning);
354 assert!(PressureLevel::Warning < PressureLevel::Critical);
355 assert!(PressureLevel::Critical.is_critical());
356 assert!(!PressureLevel::Warning.is_critical());
357 assert!(PressureLevel::Warning.is_elevated());
358 assert!(!PressureLevel::Nominal.is_elevated());
359 assert!(PressureLevel::Critical.is_elevated());
360 }
361
362 #[test]
363 fn pressure_level_display() {
364 assert_eq!(format!("{}", PressureLevel::Nominal), "nominal");
365 assert_eq!(format!("{}", PressureLevel::Warning), "warning");
366 assert_eq!(format!("{}", PressureLevel::Critical), "critical");
367 }
368
369 #[test]
370 fn new_rejects_bad_thresholds() {
371 assert!(MemoryPressureMonitor::new(0.9, 0.9).is_err());
373 assert!(MemoryPressureMonitor::new(0.95, 0.8).is_err());
374 assert!(MemoryPressureMonitor::new(0.0, 0.9).is_err());
376 assert!(MemoryPressureMonitor::new(0.8, 1.5).is_err());
377 assert!(MemoryPressureMonitor::new(f64::NAN, 0.9).is_err());
379 }
380
381 #[test]
382 fn new_accepts_valid_thresholds() {
383 let m = MemoryPressureMonitor::new(0.8, 0.95).expect("ok");
384 assert!((m.warning_fraction() - 0.8).abs() < 1e-12);
385 assert!((m.critical_fraction() - 0.95).abs() < 1e-12);
386 assert_eq!(m.level(), PressureLevel::Nominal);
387 }
388
389 #[test]
390 fn validate_thresholds_helper() {
391 assert!(validate_thresholds(0.7, 0.9).is_ok());
392 assert!(validate_thresholds(0.9, 0.7).is_err());
393 }
394
395 #[test]
396 fn classify_boundaries() {
397 let m = MemoryPressureMonitor::new(0.80, 0.95).expect("ok");
398 assert_eq!(m.classify(0.50), PressureLevel::Nominal);
400 assert_eq!(m.classify(0.799), PressureLevel::Nominal);
401 assert_eq!(m.classify(0.80), PressureLevel::Warning);
403 assert_eq!(m.classify(0.90), PressureLevel::Warning);
404 assert_eq!(m.classify(0.949), PressureLevel::Warning);
406 assert_eq!(m.classify(0.95), PressureLevel::Critical);
408 assert_eq!(m.classify(1.00), PressureLevel::Critical);
409 }
410
411 #[test]
412 fn observe_transitions_through_levels() {
413 let mut m = MemoryPressureMonitor::new(0.80, 0.95).expect("ok");
414 assert_eq!(m.observe(info(50, 100)), PressureLevel::Nominal);
416 assert_eq!(m.observe(info(15, 100)), PressureLevel::Warning);
418 assert_eq!(m.observe(info(3, 100)), PressureLevel::Critical);
420 assert_eq!(m.level(), PressureLevel::Critical);
421 assert_eq!(m.observe(info(60, 100)), PressureLevel::Nominal);
423 }
424
425 #[test]
426 fn eviction_hook_fires_on_entering_critical() {
427 let evicted = Arc::new(AtomicUsize::new(0));
428 let calls = Arc::new(AtomicU64::new(0));
429 let e2 = Arc::clone(&evicted);
430 let c2 = Arc::clone(&calls);
431 let mut m = MemoryPressureMonitor::new(0.80, 0.95)
432 .expect("ok")
433 .on_eviction(move || {
434 e2.fetch_add(1, Ordering::SeqCst);
435 c2.fetch_add(1, Ordering::SeqCst);
436 4096 });
438
439 m.observe(info(50, 100));
441 assert_eq!(calls.load(Ordering::SeqCst), 0);
442
443 m.observe(info(2, 100));
445 assert_eq!(calls.load(Ordering::SeqCst), 1);
446 assert_eq!(m.total_evicted(), 4096);
447 assert_eq!(m.eviction_count(), 1);
448
449 m.observe(info(1, 100));
451 assert_eq!(calls.load(Ordering::SeqCst), 1);
452 assert_eq!(m.total_evicted(), 4096);
453
454 m.observe(info(80, 100));
456 m.observe(info(1, 100));
457 assert_eq!(calls.load(Ordering::SeqCst), 2);
458 assert_eq!(m.total_evicted(), 8192);
459 assert_eq!(m.eviction_count(), 2);
460 }
461
462 #[test]
463 fn eviction_does_not_fire_when_warning_to_critical_skips_nominal() {
464 let calls = Arc::new(AtomicU64::new(0));
466 let c2 = Arc::clone(&calls);
467 let mut m = MemoryPressureMonitor::new(0.80, 0.95)
468 .expect("ok")
469 .on_eviction(move || {
470 c2.fetch_add(1, Ordering::SeqCst);
471 100
472 });
473 m.observe(info(15, 100)); assert_eq!(calls.load(Ordering::SeqCst), 0);
475 m.observe(info(2, 100)); assert_eq!(calls.load(Ordering::SeqCst), 1);
477 }
478
479 #[test]
480 fn transition_hook_records_escalation_and_de_escalation() {
481 let escalations = Arc::new(AtomicU64::new(0));
482 let de_escalations = Arc::new(AtomicU64::new(0));
483 let up = Arc::clone(&escalations);
484 let down = Arc::clone(&de_escalations);
485 let mut m = MemoryPressureMonitor::new(0.80, 0.95)
486 .expect("ok")
487 .on_transition(move |sample: PressureSample| {
488 if sample.escalated() {
489 up.fetch_add(1, Ordering::SeqCst);
490 }
491 if sample.de_escalated() {
492 down.fetch_add(1, Ordering::SeqCst);
493 }
494 });
495
496 m.observe(info(50, 100)); assert_eq!(escalations.load(Ordering::SeqCst), 0);
498 m.observe(info(15, 100)); m.observe(info(2, 100)); assert_eq!(escalations.load(Ordering::SeqCst), 2);
501 m.observe(info(50, 100)); assert_eq!(de_escalations.load(Ordering::SeqCst), 1);
503 }
504
505 #[test]
506 fn observe_same_level_does_not_transition() {
507 let calls = Arc::new(AtomicU64::new(0));
508 let c2 = Arc::clone(&calls);
509 let mut m = MemoryPressureMonitor::new(0.80, 0.95)
510 .expect("ok")
511 .on_transition(move |_| {
512 c2.fetch_add(1, Ordering::SeqCst);
513 });
514 m.observe(info(50, 100)); m.observe(info(60, 100)); assert_eq!(calls.load(Ordering::SeqCst), 0);
517 }
518
519 #[test]
520 fn pressure_sample_transition_flags() {
521 let escalate = PressureSample {
522 level: PressureLevel::Critical,
523 used_fraction: 0.97,
524 previous: PressureLevel::Warning,
525 };
526 assert!(escalate.escalated());
527 assert!(!escalate.de_escalated());
528
529 let de = PressureSample {
530 level: PressureLevel::Nominal,
531 used_fraction: 0.20,
532 previous: PressureLevel::Critical,
533 };
534 assert!(de.de_escalated());
535 assert!(!de.escalated());
536 }
537
538 #[test]
539 fn reset_clears_state_keeps_thresholds() {
540 let mut m = MemoryPressureMonitor::new(0.80, 0.95)
541 .expect("ok")
542 .on_eviction(|| 1000);
543 m.observe(info(1, 100)); assert_eq!(m.total_evicted(), 1000);
545 assert_eq!(m.level(), PressureLevel::Critical);
546 m.reset();
547 assert_eq!(m.level(), PressureLevel::Nominal);
548 assert_eq!(m.total_evicted(), 0);
549 assert_eq!(m.eviction_count(), 0);
550 assert!((m.warning_fraction() - 0.80).abs() < 1e-12);
552 }
553
554 #[test]
555 fn observe_zero_total_is_nominal() {
556 let mut m = MemoryPressureMonitor::new(0.80, 0.95).expect("ok");
558 assert_eq!(m.observe(info(0, 0)), PressureLevel::Nominal);
559 }
560
561 #[test]
562 fn poll_signature_compiles() {
563 let _: fn(&mut MemoryPressureMonitor) -> CudaResult<PressureLevel> =
564 MemoryPressureMonitor::poll;
565 }
566
567 #[test]
568 fn debug_does_not_panic() {
569 let m = MemoryPressureMonitor::new(0.8, 0.95)
570 .expect("ok")
571 .on_eviction(|| 0);
572 let s = format!("{m:?}");
573 assert!(s.contains("MemoryPressureMonitor"));
574 assert!(s.contains("has_eviction_hook"));
575 }
576}