use std::fs;
use std::path::Path;
const CGROUP_V2_ROOT: &str = "/sys/fs/cgroup";
pub fn detect_memory_limit() -> u64 {
if let Some(limit) = read_cgroup_v2_limit_at(Path::new(CGROUP_V2_ROOT)) {
tracing::info!(
limit_bytes = limit,
source = "cgroup-v2",
"detected memory limit"
);
return limit;
}
if let Some(limit) = read_cgroup_v1_limit() {
tracing::info!(
limit_bytes = limit,
source = "cgroup-v1",
"detected memory limit"
);
return limit;
}
let mut sys = sysinfo::System::new();
sys.refresh_memory();
let total = sys.total_memory();
tracing::info!(
limit_bytes = total,
source = "system-memory",
"detected memory limit (no cgroup)"
);
total
}
#[must_use]
pub fn detect_memory_high() -> Option<u64> {
read_cgroup_v2_high_at(Path::new(CGROUP_V2_ROOT))
}
#[must_use]
pub fn detect_memory_pressure() -> Option<f64> {
detect_memory_pressure_at(Path::new(CGROUP_V2_ROOT))
}
#[must_use]
pub fn detect_memory_stall() -> Option<f64> {
read_memory_psi_some_avg10_at(Path::new(CGROUP_V2_ROOT))
}
fn detect_memory_pressure_at(root: &Path) -> Option<f64> {
if let Some(current) = read_cgroup_v2_current_at(root) {
let mut worst: Option<f64> = None;
for limit in [read_cgroup_v2_limit_at(root), read_cgroup_v2_high_at(root)]
.into_iter()
.flatten()
.filter(|l| *l > 0)
{
let ratio = current as f64 / limit as f64;
worst = Some(worst.map_or(ratio, |w| w.max(ratio)));
}
return worst;
}
let limit = read_cgroup_v1_limit()?;
if limit == 0 {
return None;
}
let current = read_cgroup_v1_current()?;
Some(current as f64 / limit as f64)
}
fn read_cgroup_v2_limit_at(root: &Path) -> Option<u64> {
let content = fs::read_to_string(root.join("memory.max")).ok()?;
let trimmed = content.trim();
if trimmed == "max" {
return None; }
trimmed.parse::<u64>().ok()
}
fn read_cgroup_v2_high_at(root: &Path) -> Option<u64> {
let content = fs::read_to_string(root.join("memory.high")).ok()?;
let trimmed = content.trim();
if trimmed == "max" {
return None; }
trimmed.parse::<u64>().ok()
}
fn read_cgroup_v2_current_at(root: &Path) -> Option<u64> {
fs::read_to_string(root.join("memory.current"))
.ok()?
.trim()
.parse::<u64>()
.ok()
}
fn read_memory_psi_some_avg10_at(root: &Path) -> Option<f64> {
let content = fs::read_to_string(root.join("memory.pressure")).ok()?;
let some_line = content.lines().find(|l| l.starts_with("some "))?;
let avg10 = some_line
.split_whitespace()
.find_map(|field| field.strip_prefix("avg10="))?
.parse::<f64>()
.ok()?;
if !avg10.is_finite() {
return None;
}
Some((avg10 / 100.0).clamp(0.0, 1.0))
}
fn read_cgroup_v1_current() -> Option<u64> {
fs::read_to_string("/sys/fs/cgroup/memory/memory.usage_in_bytes")
.ok()?
.trim()
.parse::<u64>()
.ok()
}
fn read_cgroup_v1_limit() -> Option<u64> {
let content = fs::read_to_string("/sys/fs/cgroup/memory/memory.limit_in_bytes").ok()?;
let value = content.trim().parse::<u64>().ok()?;
if value > 1 << 62 {
return None;
}
Some(value)
}
#[cfg(test)]
mod tests {
use super::*;
fn cgroup_fixture(files: &[(&str, &str)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
for (name, contents) in files {
std::fs::write(dir.path().join(name), contents).expect("write fixture");
}
dir
}
#[test]
fn test_detect_memory_limit_returns_nonzero() {
let limit = detect_memory_limit();
assert!(limit > 0, "memory limit should be positive");
}
#[test]
fn test_detect_memory_pressure_is_none_or_valid_fraction() {
match detect_memory_pressure() {
None => {}
Some(r) => assert!(
r.is_finite() && r >= 0.0,
"cgroup pressure must be finite and non-negative, got {r}"
),
}
}
#[test]
fn v2_limit_reads_max_and_treats_literal_max_as_unlimited() {
let dir = cgroup_fixture(&[("memory.max", "536870912\n")]);
assert_eq!(read_cgroup_v2_limit_at(dir.path()), Some(536_870_912));
let dir = cgroup_fixture(&[("memory.max", "max\n")]);
assert_eq!(
read_cgroup_v2_limit_at(dir.path()),
None,
"'max' = no limit"
);
}
#[test]
fn v2_high_reads_soft_throttle_and_handles_unset() {
let dir = cgroup_fixture(&[("memory.high", "402653184\n")]);
assert_eq!(read_cgroup_v2_high_at(dir.path()), Some(402_653_184));
let dir = cgroup_fixture(&[("memory.high", "max\n")]);
assert_eq!(
read_cgroup_v2_high_at(dir.path()),
None,
"'max' = no throttle"
);
let dir = cgroup_fixture(&[]);
assert_eq!(read_cgroup_v2_high_at(dir.path()), None);
}
#[test]
fn pressure_takes_worst_of_max_and_high() {
let dir = cgroup_fixture(&[
("memory.current", "314572800\n"),
("memory.max", "536870912\n"),
("memory.high", "419430400\n"),
]);
let p = detect_memory_pressure_at(dir.path()).expect("v2 pressure");
assert!(
(p - 0.75).abs() < 0.01,
"worst-of should pick current/high, got {p}"
);
}
#[test]
fn pressure_uses_max_when_high_unset() {
let dir = cgroup_fixture(&[
("memory.current", "268435456\n"), ("memory.max", "536870912\n"), ("memory.high", "max\n"), ]);
let p = detect_memory_pressure_at(dir.path()).expect("v2 pressure");
assert!((p - 0.5).abs() < 0.01, "falls back to current/max, got {p}");
}
#[test]
fn pressure_is_none_when_no_limit_in_force() {
let dir = cgroup_fixture(&[
("memory.current", "268435456\n"),
("memory.max", "max\n"),
("memory.high", "max\n"),
]);
assert_eq!(detect_memory_pressure_at(dir.path()), None);
}
#[test]
fn psi_parses_some_avg10_as_fraction() {
let dir = cgroup_fixture(&[(
"memory.pressure",
"some avg10=42.00 avg60=10.00 avg300=3.00 total=12345\n\
full avg10=10.00 avg60=2.00 avg300=0.00 total=4567\n",
)]);
let stall = read_memory_psi_some_avg10_at(dir.path()).expect("psi");
assert!(
(stall - 0.42).abs() < 0.001,
"avg10=42 -> 0.42, got {stall}"
);
}
#[test]
fn psi_is_none_when_file_absent() {
let dir = cgroup_fixture(&[]);
assert_eq!(read_memory_psi_some_avg10_at(dir.path()), None);
}
#[test]
fn psi_clamps_and_handles_zero_stall() {
let dir = cgroup_fixture(&[(
"memory.pressure",
"some avg10=0.00 avg60=0.00 avg300=0.00 total=0\n",
)]);
assert_eq!(read_memory_psi_some_avg10_at(dir.path()), Some(0.0));
}
}