Skip to main content

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}