oxillama_runtime/offload/pressure.rs
1//! Memory pressure probe — lightweight OS-level RAM usage monitor.
2//!
3//! [`MemoryPressureProbe`] exposes a single [`is_high()`] method that returns
4//! `true` when the process's RSS exceeds a configurable `high_watermark`
5//! fraction of total physical RAM. When pressure is high the pager should
6//! prefer aggressive eviction.
7//!
8//! Currently implemented for Linux (via `/proc/self/status` + `/proc/meminfo`).
9//! macOS is a stub that always returns `false`; patches welcome via the Mach
10//! task-info API.
11//!
12//! [`is_high()`]: MemoryPressureProbe::is_high
13
14/// Returns current host RSS as a fraction of total physical RAM.
15///
16/// Returns `None` if the platform is unsupported or the OS files cannot be
17/// parsed.
18pub fn host_memory_pressure() -> Option<f64> {
19 #[cfg(target_os = "linux")]
20 {
21 linux_rss_fraction()
22 }
23 #[cfg(not(target_os = "linux"))]
24 {
25 None
26 }
27}
28
29#[cfg(target_os = "linux")]
30fn linux_rss_fraction() -> Option<f64> {
31 // Parse /proc/self/status for VmRSS and /proc/meminfo for MemTotal.
32 let status = std::fs::read_to_string("/proc/self/status").ok()?;
33 let vm_rss_kb: u64 = status
34 .lines()
35 .find(|l| l.starts_with("VmRSS:"))?
36 .split_whitespace()
37 .nth(1)?
38 .parse()
39 .ok()?;
40 let vm_rss = vm_rss_kb * 1024; // kB → bytes
41
42 let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
43 let mem_total_kb: u64 = meminfo
44 .lines()
45 .find(|l| l.starts_with("MemTotal:"))?
46 .split_whitespace()
47 .nth(1)?
48 .parse()
49 .ok()?;
50 let mem_total = mem_total_kb * 1024; // kB → bytes
51
52 if mem_total == 0 {
53 return None;
54 }
55 Some(vm_rss as f64 / mem_total as f64)
56}
57
58/// Lightweight memory pressure monitor.
59///
60/// Call [`is_high()`] before eviction decisions to determine whether to be
61/// aggressive. If the platform is unsupported, [`is_high()`] always returns
62/// `false`, meaning the pager relies entirely on the byte-budget to drive
63/// eviction.
64///
65/// # Defaults
66///
67/// - `high_watermark = 0.90` — trigger aggressive eviction above 90% RSS.
68/// - `low_watermark = 0.75` — stop evicting once below 75% RSS.
69///
70/// [`is_high()`]: MemoryPressureProbe::is_high
71#[derive(Debug, Clone)]
72pub struct MemoryPressureProbe {
73 /// RSS / total-RAM fraction above which pressure is considered high.
74 pub high_watermark: f64,
75 /// RSS / total-RAM fraction below which pressure is considered low
76 /// (used by callers implementing hysteresis).
77 pub low_watermark: f64,
78}
79
80impl Default for MemoryPressureProbe {
81 fn default() -> Self {
82 Self {
83 high_watermark: 0.90,
84 low_watermark: 0.75,
85 }
86 }
87}
88
89impl MemoryPressureProbe {
90 /// Create a new probe with the given watermarks.
91 ///
92 /// # Panics (debug)
93 ///
94 /// Panics in debug builds if `low_watermark >= high_watermark`.
95 pub fn new(high_watermark: f64, low_watermark: f64) -> Self {
96 debug_assert!(
97 low_watermark < high_watermark,
98 "low_watermark ({low_watermark}) must be less than high_watermark ({high_watermark})"
99 );
100 Self {
101 high_watermark,
102 low_watermark,
103 }
104 }
105
106 /// Returns `true` if the current RSS is at or above the high watermark.
107 ///
108 /// Returns `false` if the pressure level is unknown (unsupported platform).
109 pub fn is_high(&self) -> bool {
110 host_memory_pressure()
111 .map(|p| p >= self.high_watermark)
112 .unwrap_or(false)
113 }
114
115 /// Returns `true` if the current RSS is below the low watermark.
116 ///
117 /// Useful for hysteresis: stop evicting once [`is_low()`] returns `true`.
118 ///
119 /// Returns `true` if the pressure level is unknown (conservatively assume safe).
120 ///
121 /// [`is_low()`]: MemoryPressureProbe::is_low
122 pub fn is_low(&self) -> bool {
123 host_memory_pressure()
124 .map(|p| p < self.low_watermark)
125 .unwrap_or(true)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn default_watermarks() {
135 let probe = MemoryPressureProbe::default();
136 assert!((probe.high_watermark - 0.90).abs() < 1e-9);
137 assert!((probe.low_watermark - 0.75).abs() < 1e-9);
138 }
139
140 #[test]
141 fn new_probe_stores_watermarks() {
142 let probe = MemoryPressureProbe::new(0.85, 0.60);
143 assert!((probe.high_watermark - 0.85).abs() < 1e-9);
144 assert!((probe.low_watermark - 0.60).abs() < 1e-9);
145 }
146
147 #[test]
148 fn host_memory_pressure_returns_option() {
149 // We don't know the exact value; just verify it doesn't panic and
150 // returns a value in [0.0, 1.0] when Some.
151 if let Some(p) = host_memory_pressure() {
152 assert!(
153 (0.0..=1.0).contains(&p),
154 "memory pressure {p} must be in [0.0, 1.0]"
155 );
156 }
157 }
158
159 #[test]
160 fn is_high_does_not_panic() {
161 let probe = MemoryPressureProbe::default();
162 // Just ensure it returns without panic; value is OS-dependent.
163 let _ = probe.is_high();
164 }
165
166 #[test]
167 fn is_low_does_not_panic() {
168 let probe = MemoryPressureProbe::default();
169 let _ = probe.is_low();
170 }
171
172 #[test]
173 fn probe_clone_is_independent() {
174 let original = MemoryPressureProbe::new(0.95, 0.80);
175 let cloned = original.clone();
176 assert!((cloned.high_watermark - 0.95).abs() < 1e-9);
177 assert!((cloned.low_watermark - 0.80).abs() < 1e-9);
178 }
179}