use std::num::{NonZeroU64, NonZeroUsize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Threads {
#[default]
Auto,
Count(NonZeroUsize),
Single,
}
impl Threads {
pub fn count_or_single(n: usize) -> Self {
match NonZeroUsize::new(n) {
Some(count) => Self::Count(count),
None => Self::Single,
}
}
pub fn count(&self) -> usize {
match self {
Self::Auto => std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1),
Self::Count(n) => n.get(),
Self::Single => 1,
}
}
pub fn is_single(&self) -> bool {
self.count() == 1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MemoryLimit {
#[default]
Auto,
Bytes(NonZeroU64),
}
const DEFAULT_MEMORY_LIMIT: u64 = 512 * 1024 * 1024;
const MINIMUM_MEMORY_LIMIT: u64 = 16 * 1024 * 1024;
const BUDGET_PER_CORE: u64 = 192 * 1024 * 1024;
impl MemoryLimit {
pub fn bytes_or_auto(bytes: u64) -> Self {
match NonZeroU64::new(bytes) {
Some(bytes) => Self::Bytes(bytes),
None => Self::Auto,
}
}
pub fn bytes(&self) -> u64 {
match self {
Self::Auto => Self::detected(),
Self::Bytes(bytes) => bytes.get().max(MINIMUM_MEMORY_LIMIT),
}
}
fn detected() -> u64 {
static DETECTED: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
*DETECTED.get_or_init(Self::detect_once)
}
fn detect_once() -> u64 {
let cores = Threads::Auto.count() as u64;
#[cfg(feature = "sysinfo")]
{
use sysinfo::System;
let mut system = System::new();
system.refresh_memory();
if system.total_memory() == 0 {
return Self::budget_for(cgroup_headroom(), cores);
}
let capped = cgroup_headroom()
.or_else(|| System::cgroup_limits(&system).map(|limits| limits.free_memory));
let available = match capped {
Some(capped) => capped.min(system.available_memory()),
None => system.available_memory(),
};
Self::budget_for(Some(available), cores)
}
#[cfg(not(feature = "sysinfo"))]
Self::budget_for(cgroup_headroom(), cores)
}
fn budget_for(available: Option<u64>, cores: u64) -> u64 {
let Some(available) = available else {
return DEFAULT_MEMORY_LIMIT;
};
let wanted = cores.max(1).saturating_mul(BUDGET_PER_CORE);
wanted.min(available / 2).max(MINIMUM_MEMORY_LIMIT)
}
}
#[cfg(target_os = "linux")]
fn cgroup_headroom() -> Option<u64> {
let own = std::fs::read_to_string("/proc/self/cgroup").ok()?;
let mounts = std::fs::read_to_string("/proc/self/mountinfo").ok()?;
headroom_from(&own, &mounts)
}
#[cfg(target_os = "linux")]
fn headroom_from(own: &str, mounts: &str) -> Option<u64> {
let mut headroom = None;
for dir in cgroup_dirs(own, mounts) {
let Some(limit) = ["memory.max", "memory.high", "memory.limit_in_bytes"]
.iter()
.filter_map(|file| read_cgroup_value(&dir, file))
.min()
else {
continue;
};
let Some(used) = cgroup_usage(&dir) else {
headroom = Some(0);
continue;
};
let used = match protected_below(&dir) {
Some(protected) => {
let bound = used.saturating_add(protected);
whole_usage(&dir).map_or(bound, |whole| bound.min(whole))
}
None => whole_usage(&dir).unwrap_or(limit),
};
let level = limit.saturating_sub(used);
headroom = Some(headroom.map_or(level, |seen: u64| seen.min(level)));
}
headroom
}
#[cfg(target_os = "linux")]
const SUBTREE_BUDGET: u32 = 4096;
#[cfg(target_os = "linux")]
fn protected_below(dir: &str) -> Option<u64> {
let mut budget = SUBTREE_BUDGET;
protected_within(dir, &mut budget)
}
#[cfg(target_os = "linux")]
fn protected_within(dir: &str, budget: &mut u32) -> Option<u64> {
let mut total = 0u64;
for entry in std::fs::read_dir(dir).ok()? {
let entry = entry.ok()?;
if !entry.file_type().ok()?.is_dir() {
continue;
}
if *budget == 0 {
return None;
}
*budget -= 1;
let child = entry.path();
let child = child.to_str()?;
let held = whole_usage(child)?;
let floor = read_floor(child)?.min(held);
total = total.saturating_add(floor.max(protected_within(child, budget)?));
}
Some(total)
}
#[cfg(target_os = "linux")]
fn read_floor(dir: &str) -> Option<u64> {
match std::fs::read_to_string(format!("{dir}/memory.min")) {
Ok(contents) => contents.trim().parse().ok(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Some(0),
Err(_) => None,
}
}
#[cfg(target_os = "linux")]
fn whole_usage(dir: &str) -> Option<u64> {
["memory.current", "memory.usage_in_bytes"]
.iter()
.find_map(|file| read_cgroup_value(dir, file))
}
#[cfg(not(target_os = "linux"))]
fn cgroup_headroom() -> Option<u64> {
None
}
#[cfg(target_os = "linux")]
struct CgroupMount {
root: String,
point: String,
v2: bool,
}
#[cfg(target_os = "linux")]
fn cgroup_dirs(own: &str, mounts: &str) -> impl Iterator<Item = String> {
let mounts = cgroup_mounts(mounts);
let mut dirs = Vec::new();
for line in own.lines() {
let mut fields = line.splitn(3, ':');
let (Some(_hierarchy), Some(controllers), Some(path)) =
(fields.next(), fields.next(), fields.next())
else {
continue;
};
let v2 = controllers.is_empty();
if !v2 && !controllers.split(',').any(|name| name == "memory") {
continue;
}
for mount in mounts.iter().filter(|mount| mount.v2 == v2) {
let Some(relative) = strip_cgroup_root(path, &mount.root) else {
continue;
};
let leaf = format!("{}{relative}", mount.point);
let mut current = leaf.as_str();
dirs.push(current.to_string());
while current.len() > mount.point.len() {
match current.rfind('/') {
Some(cut) if cut >= mount.point.len() => current = ¤t[..cut],
_ => break,
}
dirs.push(current.to_string());
}
}
}
dirs.into_iter()
}
#[cfg(target_os = "linux")]
fn strip_cgroup_root<'a>(path: &'a str, root: &str) -> Option<&'a str> {
if root == "/" {
return Some(path);
}
match path.strip_prefix(root) {
Some("") => Some(""),
Some(rest) if rest.starts_with('/') => Some(rest),
_ => None,
}
}
#[cfg(target_os = "linux")]
fn cgroup_mounts(mounts: &str) -> Vec<CgroupMount> {
mounts
.lines()
.filter_map(|line| {
let (before, after) = line.split_once(" - ")?;
let mut head = before.split_whitespace().skip(3);
let root = head.next()?;
let point = head.next()?;
let mut tail = after.split_whitespace();
let fstype = tail.next()?;
let _source = tail.next()?;
let options = tail.next().unwrap_or("");
let root = unescape_mount_path(root);
let point = unescape_mount_path(point.trim_end_matches('/'));
match fstype {
"cgroup2" => Some(CgroupMount {
root,
point,
v2: true,
}),
"cgroup" if options.split(',').any(|name| name == "memory") => Some(CgroupMount {
root,
point,
v2: false,
}),
_ => None,
}
})
.collect()
}
#[cfg(target_os = "linux")]
fn cgroup_usage(dir: &str) -> Option<u64> {
let whole = whole_usage(dir);
let broken_down = std::fs::read_to_string(format!("{dir}/memory.stat"))
.ok()
.and_then(|stat| parse_cgroup_usage(&stat));
let (Some(counted), Some(whole)) = (broken_down, whole) else {
return whole;
};
let explained = counted.occupied.saturating_add(counted.reclaimable);
let unexplained = whole.saturating_sub(explained);
Some(counted.occupied.saturating_add(unexplained).min(whole))
}
#[cfg(target_os = "linux")]
#[derive(Debug, PartialEq, Eq)]
struct CgroupBreakdown {
occupied: u64,
reclaimable: u64,
}
#[cfg(target_os = "linux")]
fn parse_cgroup_usage(stat: &str) -> Option<CgroupBreakdown> {
let mut anon = None;
let mut apart = 0u64;
let mut kernel = None;
let mut kernel_parts = 0u64;
let mut reclaimable_slab = 0u64;
let mut file = 0u64;
let mut shmem = 0u64;
let mut unevictable = 0u64;
for line in stat.lines() {
let mut fields = line.split_whitespace();
let (Some(name), Some(value)) = (fields.next(), fields.next()) else {
continue;
};
let Ok(value) = value.parse::<u64>() else {
continue;
};
match name {
"anon" => anon = Some(value),
"file" => file = value,
"shmem" | "unevictable" | "sock" | "hugetlb" => {
match name {
"shmem" => shmem = value,
"unevictable" => unevictable = value,
_ => {}
}
apart = apart.saturating_add(value);
}
"kernel" => kernel = Some(value),
"slab_reclaimable" => reclaimable_slab = value,
"slab_unreclaimable" | "kernel_stack" | "pagetables" | "percpu" | "vmalloc" => {
kernel_parts = kernel_parts.saturating_add(value);
}
_ => {}
}
}
let anon = anon?;
let kernel = match kernel {
Some(kernel) => kernel.saturating_sub(reclaimable_slab),
None => kernel_parts,
};
Some(CgroupBreakdown {
occupied: anon.saturating_add(apart).saturating_add(kernel),
reclaimable: file
.saturating_sub(shmem)
.saturating_sub(unevictable)
.saturating_add(reclaimable_slab),
})
}
#[cfg(target_os = "linux")]
fn unescape_mount_path(field: &str) -> String {
let mut out = String::with_capacity(field.len());
let mut rest = field;
while let Some(cut) = rest.find('\\') {
out.push_str(&rest[..cut]);
let digits = rest.get(cut + 1..cut + 4).unwrap_or("");
match u8::from_str_radix(digits, 8) {
Ok(byte) if digits.len() == 3 => {
out.push(byte as char);
rest = &rest[cut + 4..];
}
_ => {
out.push('\\');
rest = &rest[cut + 1..];
}
}
}
out.push_str(rest);
out
}
#[cfg(target_os = "linux")]
fn read_cgroup_value(dir: &str, file: &str) -> Option<u64> {
let contents = std::fs::read_to_string(format!("{dir}/{file}")).ok()?;
parse_cgroup_value(&contents)
}
#[cfg(target_os = "linux")]
fn parse_cgroup_value(contents: &str) -> Option<u64> {
let value: u64 = contents.trim().parse().ok()?;
const NOT_A_CAP: u64 = 1 << 60;
(value < NOT_A_CAP).then_some(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_threads_count_never_zero() {
assert!(Threads::Auto.count() >= 1);
assert_eq!(Threads::Single.count(), 1);
assert_eq!(Threads::count_or_single(0), Threads::Single);
assert_eq!(Threads::count_or_single(7).count(), 7);
}
#[test]
fn test_threads_is_single() {
assert!(Threads::Single.is_single());
assert!(Threads::count_or_single(1).is_single());
assert!(!Threads::count_or_single(4).is_single());
}
#[test]
fn test_memory_limit_has_a_floor() {
let tiny = MemoryLimit::bytes_or_auto(1);
assert_eq!(tiny.bytes(), MINIMUM_MEMORY_LIMIT);
}
#[test]
fn test_memory_limit_zero_means_auto() {
assert_eq!(MemoryLimit::bytes_or_auto(0), MemoryLimit::Auto);
assert!(MemoryLimit::Auto.bytes() >= MINIMUM_MEMORY_LIMIT);
}
#[test]
fn test_detected_limit_never_grows_as_memory_shrinks() {
const TOTAL: u64 = 8 * 1024 * 1024 * 1024;
let mut previous = u64::MAX;
for available in [
TOTAL,
1024 * 1024 * 1024,
256 * 1024 * 1024,
64 * 1024 * 1024,
63 * 1024 * 1024,
16 * 1024 * 1024,
1,
0,
] {
let budget = MemoryLimit::budget_for(Some(available), 8);
assert!(
budget <= previous,
"{available} bytes free yielded {budget}, more than the \
{previous} allowed with more memory",
);
assert!(budget >= MINIMUM_MEMORY_LIMIT, "below the floor");
previous = budget;
}
}
#[test]
fn test_the_default_is_only_for_an_unanswerable_machine() {
assert_eq!(MemoryLimit::budget_for(None, 8), DEFAULT_MEMORY_LIMIT);
assert_eq!(MemoryLimit::budget_for(Some(0), 8), MINIMUM_MEMORY_LIMIT);
assert!(MemoryLimit::budget_for(Some(0), 8) < DEFAULT_MEMORY_LIMIT);
}
#[test]
fn test_a_larger_machine_gets_a_larger_budget() {
const PLENTY: u64 = 256 * 1024 * 1024 * 1024;
let mut previous = 0;
for cores in [1, 2, 8, 32, 128] {
let budget = MemoryLimit::budget_for(Some(PLENTY), cores);
assert!(
budget > previous,
"{cores} cores yielded {budget}, no more than the {previous} \
that fewer cores were given",
);
previous = budget;
}
}
#[test]
fn test_free_memory_bounds_the_budget() {
const FREE: u64 = 2 * 1024 * 1024 * 1024;
let budget = MemoryLimit::budget_for(Some(FREE), 128);
assert_eq!(budget, FREE / 2);
}
#[test]
fn test_a_small_machine_stays_above_the_floor() {
let budget = MemoryLimit::budget_for(Some(8 * 1024 * 1024), 1);
assert_eq!(budget, MINIMUM_MEMORY_LIMIT);
}
#[cfg(target_os = "linux")]
fn holds(dir: &std::path::Path, bytes: u64) {
std::fs::write(dir.join("memory.current"), format!("{bytes}\n")).expect("current");
}
#[cfg(target_os = "linux")]
fn occupied_of(stat: &str) -> Option<u64> {
super::parse_cgroup_usage(stat).map(|breakdown| breakdown.occupied)
}
#[cfg(target_os = "linux")]
fn mountinfo(mount: &str) -> String {
format!("42 40 0:29 / {mount} rw,nosuid - cgroup2 cgroup2 rw,nsdelegate\n")
}
#[cfg(target_os = "linux")]
#[test]
fn test_a_cap_on_an_ancestor_is_found() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("user.slice/app.slice/run-1.scope");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(mount.path().join("memory.max"), "max\n").expect("root");
std::fs::write(
mount.path().join("user.slice/memory.max"),
format!("{}\n", 2u64 << 30),
)
.expect("cap");
std::fs::write(
mount.path().join("user.slice/memory.stat"),
format!(
"anon {}\nfile {}\nslab 0\nsock 0\nshmem 0\n",
512u64 << 20,
8u64 << 30
),
)
.expect("usage");
holds(&mount.path().join("user.slice"), (8u64 << 30) + (512 << 20));
holds(&mount.path().join("user.slice/app.slice"), 0);
holds(&leaf, 0);
let own = "0::/user.slice/app.slice/run-1.scope\n";
assert_eq!(
super::headroom_from(own, &mountinfo(root)),
Some((2u64 << 30) - (512 << 20)),
"the cap on the ancestor was not found, or its usage was miscounted",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_an_uncapped_hierarchy_reports_nothing() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
std::fs::create_dir_all(mount.path().join("user.slice")).expect("dirs");
std::fs::write(mount.path().join("memory.max"), "max\n").expect("root");
assert_eq!(
super::headroom_from("0::/user.slice\n", &mountinfo(root)),
None
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_the_tightest_cap_wins() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("outer/inner");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(
mount.path().join("outer/memory.max"),
format!("{}\n", 8u64 << 30),
)
.expect("outer");
std::fs::write(mount.path().join("outer/memory.current"), "0\n").expect("outer usage");
std::fs::write(leaf.join("memory.max"), format!("{}\n", 1u64 << 30)).expect("inner");
std::fs::write(leaf.join("memory.current"), "0\n").expect("inner usage");
assert_eq!(
super::headroom_from("0::/outer/inner\n", &mountinfo(root)),
Some(1 << 30)
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_the_first_version_is_read_from_its_own_tree() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("memory/limited");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(
leaf.join("memory.limit_in_bytes"),
format!("{}\n", 3u64 << 30),
)
.expect("cap");
std::fs::write(
leaf.join("memory.usage_in_bytes"),
format!("{}\n", 1u64 << 30),
)
.expect("usage");
let mounts = format!(
"31 25 0:26 / {root}/memory rw - cgroup cgroup rw,memory\n 32 25 0:27 / {root}/cpu rw - cgroup cgroup rw,cpu\n"
);
let own = "4:cpu,cpuacct:/limited\n3:memory:/limited\n";
assert_eq!(super::headroom_from(own, &mounts), Some(2 << 30));
}
#[cfg(target_os = "linux")]
#[test]
fn test_the_mount_point_is_taken_from_mountinfo() {
let mount = tempfile::tempdir().expect("tempdir");
let elsewhere = mount.path().join("run/cgroups");
std::fs::create_dir_all(elsewhere.join("capped")).expect("dirs");
std::fs::write(
elsewhere.join("capped/memory.max"),
format!("{}\n", 1u64 << 30),
)
.expect("cap");
std::fs::write(elsewhere.join("capped/memory.current"), "0\n").expect("usage");
let mounts = mountinfo(elsewhere.to_str().expect("utf-8"));
assert_eq!(super::headroom_from("0::/capped\n", &mounts), Some(1 << 30));
assert_eq!(super::headroom_from("0::/capped\n", ""), None);
}
#[cfg(target_os = "linux")]
#[test]
fn test_a_mount_of_one_branch_is_understood() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
std::fs::create_dir_all(mount.path().join("run-1.scope")).expect("dirs");
std::fs::write(
mount.path().join("run-1.scope/memory.max"),
format!("{}\n", 1u64 << 30),
)
.expect("cap");
std::fs::write(mount.path().join("run-1.scope/memory.current"), "0\n").expect("usage");
let mounts = format!("42 40 0:29 /user.slice {root} rw - cgroup2 cgroup2 rw,nsdelegate\n");
assert_eq!(
super::headroom_from("0::/user.slice/run-1.scope\n", &mounts),
Some(1 << 30),
);
assert_eq!(
super::headroom_from("0::/user.slices/other\n", &mounts),
None
);
assert_eq!(super::headroom_from("0::/system.slice/x\n", &mounts), None);
}
#[cfg(target_os = "linux")]
#[test]
fn test_cached_files_do_not_count_against_the_budget() {
let stat = "anon 1338310656\nfile 21077241856\nkernel 730398720\n\
kernel_stack 12566528\npagetables 0\npercpu 2053168\nsock 8192\n\
vmalloc 229376\nshmem 72044544\nunevictable 0\nslab 685656672\n\
slab_reclaimable 430216992\n";
assert_eq!(
occupied_of(stat),
Some(1338310656 + 72044544 + (730398720 - 430216992) + 8192),
"the file cache was counted, or something unreclaimable was not",
);
assert!(occupied_of(stat).expect("parsed") < 3 << 30);
assert_eq!(occupied_of("something else\n"), None);
}
#[cfg(target_os = "linux")]
#[test]
fn test_the_first_version_is_counted_whole() {
let stat = "cache 402653184\nrss 134217728\nmapped_file 402653184\ntotal_rss 134217728\n";
assert_eq!(
occupied_of(stat),
None,
"a v1 breakdown was trusted, and it cannot account for tmpfs",
);
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("memory/capped");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(
leaf.join("memory.limit_in_bytes"),
format!("{}\n", 512u64 << 20),
)
.expect("cap");
std::fs::write(leaf.join("memory.stat"), stat).expect("stat");
std::fs::write(
leaf.join("memory.usage_in_bytes"),
format!("{}\n", 512u64 << 20),
)
.expect("usage");
let mounts = format!("31 25 0:26 / {root}/memory rw - cgroup cgroup rw,memory\n");
assert_eq!(
super::headroom_from("3:memory:/capped\n", &mounts),
Some(0),
"a full v1 cgroup was read as having room",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_kernel_memory_is_counted_in_full() {
let with_aggregate =
"anon 1000\nfile 5000\nkernel 800\nslab 700\nslab_reclaimable 500\nsock 10\nshmem 0\n";
assert_eq!(
occupied_of(with_aggregate),
Some(1000 + (800 - 500) + 10),
"the aggregate was ignored, counted twice with its parts, kept the \
reclaimable slab, or took the socket buffers down with it",
);
let parts_only = "anon 1000\nfile 5000\nslab_unreclaimable 700\nslab_reclaimable 900\n\
sock 10\nkernel_stack 40\npagetables 30\npercpu 20\nvmalloc 10\n";
assert_eq!(
occupied_of(parts_only),
Some(1000 + 700 + 10 + 40 + 30 + 20 + 10),
"the older layout counted the reclaimable half of the slab",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_a_cap_without_a_usage_is_not_free_space() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("capped");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(leaf.join("memory.max"), format!("{}\n", 2u64 << 30)).expect("cap");
assert_eq!(
super::headroom_from("0::/capped\n", &mountinfo(root)),
Some(0),
"an unreadable usage was read as an empty cgroup",
);
assert_eq!(MemoryLimit::budget_for(Some(0), 24), MINIMUM_MEMORY_LIMIT);
}
#[cfg(target_os = "linux")]
#[test]
fn test_a_floor_below_the_cap_is_not_headroom() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("outer/inner");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(
mount.path().join("outer/memory.max"),
format!("{}\n", 1u64 << 30),
)
.expect("cap");
std::fs::write(
mount.path().join("outer/memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 8u64 << 20, 600u64 << 20),
)
.expect("outer stat");
std::fs::write(leaf.join("memory.min"), format!("{}\n", 256u64 << 20)).expect("floor");
std::fs::write(
leaf.join("memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 4u64 << 20, 300u64 << 20),
)
.expect("inner stat");
holds(&mount.path().join("outer"), 608u64 << 20);
holds(&leaf, 304u64 << 20);
assert_eq!(
super::headroom_from("0::/outer/inner\n", &mountinfo(root)),
Some((1u64 << 30) - (8 << 20) - (256 << 20)),
"a floor held below the cap was offered up as free memory",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_a_groups_own_floor_does_not_bind_its_own_cap() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("outer/inner");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(
mount.path().join("outer/memory.max"),
format!("{}\n", 1u64 << 30),
)
.expect("cap");
std::fs::write(
mount.path().join("outer/memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 50u64 << 20, 300u64 << 20),
)
.expect("outer stat");
std::fs::write(
mount.path().join("outer/memory.current"),
format!("{}\n", 350u64 << 20),
)
.expect("outer usage");
std::fs::write(
mount.path().join("outer/memory.min"),
format!("{}\n", 200u64 << 20),
)
.expect("outer floor");
std::fs::write(leaf.join("memory.min"), format!("{}\n", 150u64 << 20))
.expect("inner floor");
holds(&leaf, 200u64 << 20);
assert_eq!(
super::headroom_from("0::/outer/inner\n", &mountinfo(root)),
Some((1u64 << 30) - (50 << 20) - (150 << 20)),
"the cap was bound by its own floor, or not by the one below it",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_a_floor_in_another_branch_is_not_headroom() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let ours = mount.path().join("slice/ours");
let theirs = mount.path().join("slice/theirs");
let nested = theirs.join("deeper");
std::fs::create_dir_all(&ours).expect("ours");
std::fs::create_dir_all(&nested).expect("theirs");
std::fs::write(
mount.path().join("slice/memory.max"),
format!("{}\n", 1u64 << 30),
)
.expect("cap");
std::fs::write(
mount.path().join("slice/memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 8u64 << 20, 900u64 << 20),
)
.expect("stat");
std::fs::write(theirs.join("memory.min"), format!("{}\n", 500u64 << 20))
.expect("their floor");
std::fs::write(nested.join("memory.min"), format!("{}\n", 400u64 << 20))
.expect("nested floor");
std::fs::write(ours.join("memory.min"), format!("{}\n", 100u64 << 20)).expect("our floor");
holds(&mount.path().join("slice"), 908u64 << 20);
holds(&ours, 100u64 << 20);
holds(&theirs, 500u64 << 20);
holds(&nested, 400u64 << 20);
assert_eq!(
super::headroom_from("0::/slice/ours\n", &mountinfo(root)),
Some((1u64 << 30) - (8 << 20) - (600 << 20)),
"a promise made to another branch was counted as free memory",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_protected_cache_and_anonymous_memory_do_not_overlap() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let ours = mount.path().join("slice/ours");
let theirs = mount.path().join("slice/theirs");
std::fs::create_dir_all(&ours).expect("ours");
std::fs::create_dir_all(&theirs).expect("theirs");
std::fs::write(
mount.path().join("slice/memory.max"),
format!("{}\n", 1u64 << 30),
)
.expect("cap");
std::fs::write(
mount.path().join("slice/memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 400u64 << 20, 500u64 << 20),
)
.expect("stat");
std::fs::write(
mount.path().join("slice/memory.current"),
format!("{}\n", 900u64 << 20),
)
.expect("current");
std::fs::write(theirs.join("memory.min"), format!("{}\n", 500u64 << 20))
.expect("their floor");
holds(&ours, 0);
holds(&theirs, 500u64 << 20);
assert_eq!(
super::headroom_from("0::/slice/ours\n", &mountinfo(root)),
Some((1u64 << 30) - (900 << 20)),
"the anonymous memory and the promised cache were treated as the \
same pages",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_an_unreadable_floor_ends_the_walk() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let ours = mount.path().join("slice/ours");
let theirs = mount.path().join("slice/theirs");
std::fs::create_dir_all(&ours).expect("ours");
std::fs::create_dir_all(&theirs).expect("theirs");
std::fs::write(
mount.path().join("slice/memory.max"),
format!("{}\n", 1u64 << 30),
)
.expect("cap");
std::fs::write(
mount.path().join("slice/memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 8u64 << 20, 700u64 << 20),
)
.expect("stat");
std::fs::write(
mount.path().join("slice/memory.current"),
format!("{}\n", 708u64 << 20),
)
.expect("current");
std::fs::write(theirs.join("memory.min"), "max\n").expect("their floor");
holds(&ours, 0);
holds(&theirs, 8u64 << 20);
assert_eq!(
super::headroom_from("0::/slice/ours\n", &mountinfo(root)),
Some((1u64 << 30) - (708 << 20)),
"a floor that could not be read was taken as no floor at all",
);
std::fs::remove_file(theirs.join("memory.min")).expect("remove");
assert_eq!(
super::headroom_from("0::/slice/ours\n", &mountinfo(root)),
Some((1u64 << 30) - (8 << 20)),
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_the_bound_counts_groups_rather_than_files() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("capped");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(leaf.join("memory.max"), format!("{}\n", 1u64 << 30)).expect("cap");
std::fs::write(
leaf.join("memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 8u64 << 20, 700u64 << 20),
)
.expect("stat");
std::fs::write(leaf.join("memory.current"), format!("{}\n", 708u64 << 20))
.expect("current");
let groups = 8;
let files_each = super::SUBTREE_BUDGET / groups + 1;
for group in 0..groups {
let child = leaf.join(format!("group-{group}"));
std::fs::create_dir(&child).expect("group");
holds(&child, 0);
for file in 0..files_each {
std::fs::write(child.join(format!("controller.{file}")), "0\n").expect("file");
}
}
assert_eq!(
super::headroom_from("0::/capped\n", &mountinfo(root)),
Some((1u64 << 30) - (8 << 20)),
"the walk gave up on a handful of groups because of the files \
inside them",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_a_floor_binds_only_what_a_group_holds() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let ours = mount.path().join("slice/ours");
let idle = mount.path().join("slice/idle");
std::fs::create_dir_all(&ours).expect("ours");
std::fs::create_dir_all(&idle).expect("idle");
std::fs::write(
mount.path().join("slice/memory.max"),
format!("{}\n", 1u64 << 30),
)
.expect("cap");
std::fs::write(
mount.path().join("slice/memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 8u64 << 20, 300u64 << 20),
)
.expect("stat");
holds(&mount.path().join("slice"), 308u64 << 20);
holds(&ours, 0);
std::fs::write(idle.join("memory.min"), format!("{}\n", 250u64 << 20)).expect("floor");
holds(&idle, 2u64 << 20);
assert_eq!(
super::headroom_from("0::/slice/ours\n", &mountinfo(root)),
Some((1u64 << 30) - (8 << 20) - (2 << 20)),
"a floor was charged in full against a group that is nearly empty",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_what_the_breakdown_cannot_explain_is_occupied() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("capped");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(leaf.join("memory.max"), format!("{}\n", 1u64 << 30)).expect("cap");
std::fs::write(
leaf.join("memory.stat"),
format!(
"anon {}\nfile {}\nkernel 0\nsomething_new {}\n",
100u64 << 20,
200u64 << 20,
300u64 << 20
),
)
.expect("stat");
holds(&leaf, 600u64 << 20);
assert_eq!(
super::headroom_from("0::/capped\n", &mountinfo(root)),
Some((1u64 << 30) - (400 << 20)),
"a line the breakdown did not recognise was treated as free memory",
);
std::fs::write(
leaf.join("memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 100u64 << 20, 200u64 << 20),
)
.expect("stat");
holds(&leaf, 300u64 << 20);
assert_eq!(
super::headroom_from("0::/capped\n", &mountinfo(root)),
Some((1u64 << 30) - (100 << 20)),
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_an_overlap_cannot_hide_an_unrecognised_counter() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("capped");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(leaf.join("memory.max"), format!("{}\n", 1u64 << 30)).expect("cap");
std::fs::write(
leaf.join("memory.stat"),
format!(
"anon 0\nfile {}\nunevictable {}\nkernel 0\nsomething_new {}\n",
300u64 << 20,
300u64 << 20,
600u64 << 20
),
)
.expect("stat");
holds(&leaf, 900u64 << 20);
assert_eq!(
super::headroom_from("0::/capped\n", &mountinfo(root)),
Some((1u64 << 30) - (900 << 20)),
"the pinned cache was counted on both sides, and hid the counter \
this does not recognise",
);
std::fs::write(
leaf.join("memory.stat"),
format!(
"anon 0\nfile {}\nunevictable {}\nkernel 0\n",
300u64 << 20,
300u64 << 20
),
)
.expect("stat");
holds(&leaf, 300u64 << 20);
assert_eq!(
super::headroom_from("0::/capped\n", &mountinfo(root)),
Some((1u64 << 30) - (300 << 20)),
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_a_floor_further_down_is_still_found() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let ours = mount.path().join("slice/ours");
let quiet = mount.path().join("slice/quiet");
let deep = quiet.join("deeper/deepest");
std::fs::create_dir_all(&ours).expect("ours");
std::fs::create_dir_all(&deep).expect("deep");
std::fs::write(
mount.path().join("slice/memory.max"),
format!("{}\n", 1u64 << 30),
)
.expect("cap");
std::fs::write(
mount.path().join("slice/memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 8u64 << 20, 500u64 << 20),
)
.expect("stat");
holds(&mount.path().join("slice"), 508u64 << 20);
holds(&ours, 0);
holds(&quiet, 300u64 << 20);
holds(&quiet.join("deeper"), 300u64 << 20);
std::fs::write(deep.join("memory.min"), format!("{}\n", 300u64 << 20)).expect("floor");
holds(&deep, 300u64 << 20);
assert_eq!(
super::headroom_from("0::/slice/ours\n", &mountinfo(root)),
Some((1u64 << 30) - (8 << 20) - (300 << 20)),
"a promise made further down the tree was read as free memory",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_a_subtree_too_large_to_walk_counts_everything() {
let mount = tempfile::tempdir().expect("tempdir");
let root = mount.path().to_str().expect("utf-8");
let leaf = mount.path().join("capped");
std::fs::create_dir_all(&leaf).expect("leaf");
std::fs::write(leaf.join("memory.max"), format!("{}\n", 1u64 << 30)).expect("cap");
std::fs::write(
leaf.join("memory.stat"),
format!("anon {}\nfile {}\nkernel 0\n", 8u64 << 20, 700u64 << 20),
)
.expect("stat");
std::fs::write(leaf.join("memory.current"), format!("{}\n", 708u64 << 20))
.expect("current");
assert_eq!(
super::headroom_from("0::/capped\n", &mountinfo(root)),
Some((1u64 << 30) - (8 << 20)),
);
for i in 0..=super::SUBTREE_BUDGET {
let child = leaf.join(format!("group-{i}"));
std::fs::create_dir(&child).expect("child");
holds(&child, 0);
}
assert_eq!(
super::headroom_from("0::/capped\n", &mountinfo(root)),
Some((1u64 << 30) - (708 << 20)),
"a subtree that could not be accounted for had its cache \
written off as free",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_socket_buffers_survive_the_aggregate() {
let stat =
"anon 1000\nfile 9000\nkernel 2000\nslab 1900\nslab_reclaimable 0\nsock 500000\n";
assert_eq!(
occupied_of(stat),
Some(1000 + 2000 + 500000),
"half a megabyte of socket buffers went missing",
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_huge_pages_count_against_the_cap() {
let stat = "anon 1000\nfile 9000\nkernel 0\nhugetlb 2097152\n";
assert_eq!(occupied_of(stat), Some(1000 + 2097152));
assert_eq!(occupied_of("anon 1000\nfile 9000\nkernel 0\n"), Some(1000));
}
#[cfg(target_os = "linux")]
#[test]
fn test_unevictable_memory_is_not_free_space() {
let stat = "anon 100\nfile 9000\nunevictable 5000\nkernel 0\nshmem 0\n";
assert_eq!(occupied_of(stat), Some(5100));
}
#[cfg(target_os = "linux")]
#[test]
fn test_an_escaped_mount_point_is_decoded() {
let mount = tempfile::tempdir().expect("tempdir");
let awkward = mount.path().join("cgroup mount");
std::fs::create_dir_all(awkward.join("capped")).expect("dirs");
std::fs::write(
awkward.join("capped/memory.max"),
format!("{}\n", 1u64 << 30),
)
.expect("cap");
std::fs::write(awkward.join("capped/memory.current"), "0\n").expect("usage");
let written = awkward.to_str().expect("utf-8").replace(' ', "\\040");
let mounts = format!("42 40 0:29 / {written} rw - cgroup2 cgroup2 rw,nsdelegate\n");
assert_eq!(
super::headroom_from("0::/capped\n", &mounts),
Some(1 << 30),
"the escaped mount point was opened literally",
);
assert_eq!(super::unescape_mount_path("a\\040b\\011c"), "a b\tc");
assert_eq!(super::unescape_mount_path("a\\bc"), "a\\bc");
}
#[cfg(target_os = "linux")]
fn captured() -> (String, String) {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/cgroup-capture");
(
path.to_string(),
format!("42 40 0:29 / {path} rw,nosuid - cgroup2 cgroup2 rw,nsdelegate\n"),
)
}
#[cfg(target_os = "linux")]
#[test]
fn test_the_model_accounts_for_a_real_hierarchy() {
let (path, _) = captured();
let mut groups = 0;
for entry in walkdir(&path) {
let Ok(stat) = std::fs::read_to_string(entry.join("memory.stat")) else {
continue;
};
let Ok(current) = std::fs::read_to_string(entry.join("memory.current")) else {
continue;
};
let current: u64 = current.trim().parse().expect("a byte count");
if current == 0 {
continue;
}
let breakdown = super::parse_cgroup_usage(&stat).expect("a v2 breakdown");
let explained = breakdown.occupied + breakdown.reclaimable;
let gap = current.abs_diff(explained);
const BATCHING_ALLOWANCE: u64 = 16 << 20;
assert!(
gap < BATCHING_ALLOWANCE.max(current / 50),
"{}: the breakdown explains {explained} of {current} bytes, a gap of {gap}. \
Either the kernel has a field this does not know, or one is counted twice.",
entry.display(),
);
groups += 1;
}
assert!(
groups >= 4,
"the capture went missing: only {groups} groups"
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_the_floors_in_a_real_hierarchy_bind_a_cap_above_them() {
let (capture, _) = captured();
let mount = tempfile::tempdir().expect("tempdir");
copy_tree(std::path::Path::new(&capture), mount.path(), &["README.md"]);
let root = mount.path().to_str().expect("utf-8");
let mounts = format!("42 40 0:29 / {root} rw,nosuid - cgroup2 cgroup2 rw,nsdelegate\n");
let slice = mount.path().join("system.slice");
std::fs::write(slice.join("memory.max"), format!("{}\n", 1u64 << 30)).expect("cap");
let own = "0::/system.slice/dbus-broker.service\n";
let bound = super::headroom_from(own, &mounts).expect("a capped hierarchy answers");
let floor: u64 = std::fs::read_to_string(slice.join("systemd-oomd.service/memory.min"))
.expect("the captured floor")
.trim()
.parse()
.expect("a byte count");
let held: u64 = std::fs::read_to_string(slice.join("systemd-oomd.service/memory.current"))
.expect("the captured usage")
.trim()
.parse()
.expect("a byte count");
assert!(
held < floor,
"the capture no longer has a floor larger than what its group holds, \
which is the case this test exists for",
);
std::fs::remove_file(slice.join("systemd-oomd.service/memory.min")).expect("remove");
let unbound = super::headroom_from(own, &mounts).expect("a capped hierarchy answers");
assert_eq!(
unbound - bound,
held,
"a floor in a sibling branch changed the answer by {} rather than by the \
{held} bytes its group is holding",
unbound - bound,
);
assert_ne!(
unbound - bound,
floor,
"the floor was charged in full against a group holding a fraction of it",
);
}
#[cfg(target_os = "linux")]
fn copy_tree(from: &std::path::Path, to: &std::path::Path, skip: &[&str]) {
for entry in std::fs::read_dir(from).expect("readable capture").flatten() {
let name = entry.file_name();
if skip.iter().any(|s| std::path::Path::new(s) == name) {
continue;
}
let target = to.join(&name);
if entry.file_type().expect("a type").is_dir() {
std::fs::create_dir_all(&target).expect("mkdir");
copy_tree(&entry.path(), &target, skip);
} else {
std::fs::copy(entry.path(), &target).expect("copy");
}
}
}
#[cfg(target_os = "linux")]
fn walkdir(root: &str) -> Vec<std::path::PathBuf> {
let mut found = vec![std::path::PathBuf::from(root)];
let mut queue = vec![std::path::PathBuf::from(root)];
while let Some(dir) = queue.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
if entry.file_type().is_ok_and(|kind| kind.is_dir()) {
found.push(entry.path());
queue.push(entry.path());
}
}
}
found
}
#[cfg(target_os = "linux")]
#[test]
fn test_no_limit_is_recognised_however_it_is_written() {
assert_eq!(super::parse_cgroup_value("max\n"), None);
assert_eq!(super::parse_cgroup_value("9223372036854771712\n"), None);
assert_eq!(super::parse_cgroup_value(""), None);
assert_eq!(super::parse_cgroup_value("2147483648\n"), Some(2 << 30));
}
#[test]
fn test_memory_limit_honours_an_explicit_value() {
let limit = MemoryLimit::bytes_or_auto(256 * 1024 * 1024);
assert_eq!(limit.bytes(), 256 * 1024 * 1024);
}
}