use crate::Error;
pub struct MemoryGuard {
limit_bytes: u64,
baseline_bytes: u64,
}
impl MemoryGuard {
#[must_use]
pub fn new(limit_bytes: u64) -> Self {
let baseline_bytes = Self::current_rss_bytes().unwrap_or(0);
Self {
limit_bytes,
baseline_bytes,
}
}
#[must_use]
pub fn current_rss_bytes() -> Option<u64> {
#[cfg(target_os = "linux")]
return rss_linux();
#[cfg(target_os = "macos")]
return rss_macos();
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
None
}
pub fn check(&self) -> Result<(), Error> {
if self.limit_bytes == 0 {
return Err(Error::MemoryExceeded {
used_mb: 0,
limit_mb: 0,
});
}
if let Some(rss) = Self::current_rss_bytes() {
let delta = rss.saturating_sub(self.baseline_bytes);
if delta > self.limit_bytes {
return Err(Error::MemoryExceeded {
used_mb: delta / (1024 * 1024),
limit_mb: self.limit_bytes / (1024 * 1024),
});
}
}
Ok(())
}
}
#[cfg(target_os = "linux")]
fn rss_linux() -> Option<u64> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
for line in status.lines() {
if let Some(rest) = line.strip_prefix("VmRSS:") {
let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
return Some(kb * 1024);
}
}
None
}
#[cfg(target_os = "macos")]
fn rss_macos() -> Option<u64> {
let output = std::process::Command::new("vm_stat").output().ok()?;
let text = std::str::from_utf8(&output.stdout).ok()?;
let page_size: u64 = text
.lines()
.next()
.and_then(|hdr| {
let start = hdr.find("page size of ")? + "page size of ".len();
let rest = &hdr[start..];
rest[..rest.find(" bytes")?].parse().ok()
})
.unwrap_or(4096);
let mut active: u64 = 0;
let mut wired: u64 = 0;
for line in text.lines() {
if let Some(v) = line.strip_prefix("Pages active:") {
active = v.trim().trim_end_matches('.').parse().unwrap_or(0);
} else if let Some(v) = line.strip_prefix("Pages wired down:") {
wired = v.trim().trim_end_matches('.').parse().unwrap_or(0);
}
}
Some((active + wired) * page_size)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn guard_passes_under_limit() {
assert!(MemoryGuard::new(u64::MAX).check().is_ok());
}
#[test]
fn guard_fails_with_zero_limit() {
assert!(MemoryGuard::new(0).check().is_err());
}
#[test]
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn rss_is_positive_on_supported_platforms() {
let rss = MemoryGuard::current_rss_bytes();
assert!(rss.is_some());
assert!(rss.unwrap() > 0);
}
}