Skip to main content

alpha2_os/
memory.rs

1//! How much memory the machine has, and how much this process may use under a container or job
2//! limit.
3
4#[cfg(any(target_os = "linux", target_os = "android"))]
5use std::path::Path;
6
7/// The machine's memory and this process's limit. Unavailable figures are `None`.
8#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
9pub struct MemoryCapacity {
10    /// Physical memory installed.
11    pub physical_bytes: Option<u64>,
12    /// Memory available without paging: `MemAvailable` on Linux, free plus inactive plus
13    /// purgeable pages on macOS, `ullAvailPhys` on Windows.
14    pub available_bytes: Option<u64>,
15    /// The lowest of a cgroup or job limit and the physical total.
16    pub limit_bytes: Option<u64>,
17    /// What set `limit_bytes`: `cgroup-v2`, `cgroup-v1`, `job` or `physical`.
18    pub limit_source: &'static str,
19    /// Usage charged against a cgroup limit; `None` for physical and job limits.
20    pub used_in_limit_bytes: Option<u64>,
21}
22
23impl MemoryCapacity {
24    /// The limit, else the physical total.
25    #[must_use]
26    pub fn effective_limit(&self) -> Option<u64> {
27        match (self.limit_bytes, self.physical_bytes) {
28            (Some(limit), Some(physical)) => Some(limit.min(physical)),
29            (limit, physical) => limit.or(physical),
30        }
31    }
32
33    /// The lower of available memory and the headroom left under the limit.
34    #[must_use]
35    pub fn headroom(&self) -> Option<u64> {
36        let in_limit = self
37            .limit_bytes
38            .zip(self.used_in_limit_bytes)
39            .map(|(limit, used)| limit.saturating_sub(used));
40        match (self.available_bytes, in_limit) {
41            (Some(available), Some(in_limit)) => Some(available.min(in_limit)),
42            (available, in_limit) => available.or(in_limit),
43        }
44    }
45}
46
47/// The machine's memory and this process's limit, read now.
48#[must_use]
49pub fn memory_capacity() -> MemoryCapacity {
50    #[cfg(any(target_os = "linux", target_os = "android"))]
51    {
52        memory_capacity_at(Path::new("/"))
53    }
54    #[cfg(any(target_os = "macos", windows))]
55    {
56        crate::platform::memory_capacity()
57    }
58    #[cfg(not(any(
59        target_os = "linux",
60        target_os = "android",
61        target_os = "macos",
62        windows
63    )))]
64    {
65        MemoryCapacity {
66            limit_source: "physical",
67            ..MemoryCapacity::default()
68        }
69    }
70}
71
72/// [`memory_capacity`] with `root` standing in for `/`.
73#[cfg(any(target_os = "linux", target_os = "android"))]
74#[must_use]
75pub fn memory_capacity_at(root: &Path) -> MemoryCapacity {
76    let meminfo = std::fs::read_to_string(root.join("proc/meminfo")).unwrap_or_default();
77    let kib = |key: &str| {
78        meminfo
79            .lines()
80            .find_map(|line| line.strip_prefix(key))
81            .and_then(|rest| rest.trim().strip_suffix("kB"))
82            .and_then(|value| value.trim().parse::<u64>().ok())
83            .map(|value| value.saturating_mul(1_024))
84    };
85    let physical = kib("MemTotal:");
86    let available = kib("MemAvailable:");
87    let cgroup = cgroup_limit(root);
88    match cgroup {
89        Some((limit, used, source)) if physical.is_none_or(|physical| limit < physical) => {
90            MemoryCapacity {
91                physical_bytes: physical,
92                available_bytes: available,
93                limit_bytes: Some(limit),
94                limit_source: source,
95                used_in_limit_bytes: used,
96            }
97        }
98        _ => MemoryCapacity {
99            physical_bytes: physical,
100            available_bytes: available,
101            limit_bytes: physical,
102            limit_source: "physical",
103            used_in_limit_bytes: None,
104        },
105    }
106}
107
108/// `(limit, charged, source)` of the lowest limit on the process's cgroup path, v2 first. Levels
109/// without a limit (`max`, or v1's near-`u64::MAX` default) are skipped.
110#[cfg(any(target_os = "linux", target_os = "android"))]
111fn cgroup_limit(root: &Path) -> Option<(u64, Option<u64>, &'static str)> {
112    let membership = std::fs::read_to_string(root.join("proc/self/cgroup")).ok()?;
113    let read = |path: &Path| {
114        std::fs::read_to_string(path)
115            .ok()
116            .and_then(|text| text.trim().parse::<u64>().ok())
117    };
118    let lowest = |mount: &Path, relative: &str, limit_file: &str, usage_file: &str| {
119        let mut best: Option<(u64, Option<u64>)> = None;
120        let mut path = Some(Path::new(relative.trim_start_matches('/')));
121        while let Some(level) = path {
122            let directory = mount.join(level);
123            if let Some(limit) = read(&directory.join(limit_file))
124                && limit < (1_u64 << 60)
125                && best.is_none_or(|(known, _)| limit < known)
126            {
127                best = Some((limit, read(&directory.join(usage_file))));
128            }
129            path = level.parent();
130        }
131        best
132    };
133    if let Some(relative) = membership.lines().find_map(|line| line.strip_prefix("0::"))
134        && let Some((limit, used)) = lowest(
135            &root.join("sys/fs/cgroup"),
136            relative,
137            "memory.max",
138            "memory.current",
139        )
140    {
141        return Some((limit, used, "cgroup-v2"));
142    }
143    let relative = membership.lines().find_map(|line| {
144        let mut fields = line.splitn(3, ':');
145        let _ = fields.next()?;
146        let controllers = fields.next()?;
147        controllers
148            .split(',')
149            .any(|controller| controller == "memory")
150            .then(|| fields.next())
151            .flatten()
152    })?;
153    let (limit, used) = lowest(
154        &root.join("sys/fs/cgroup/memory"),
155        relative,
156        "memory.limit_in_bytes",
157        "memory.usage_in_bytes",
158    )?;
159    Some((limit, used, "cgroup-v1"))
160}
161
162/// The process's cgroup v2 `(working set, memory.max)` when it has a limit. The working set
163/// excludes reclaimable `inactive_file` page cache, which the kernel drops at the limit instead of
164/// killing anything.
165#[cfg(any(target_os = "linux", target_os = "android"))]
166#[must_use]
167pub fn cgroup_v2_usage() -> Option<(u64, u64)> {
168    let membership = std::fs::read_to_string("/proc/self/cgroup").ok()?;
169    let relative = membership
170        .lines()
171        .find_map(|line| line.strip_prefix("0::"))?;
172    let directory = Path::new("/sys/fs/cgroup").join(relative.trim_start_matches('/'));
173    let read = |name: &str| {
174        std::fs::read_to_string(directory.join(name))
175            .ok()
176            .and_then(|text| text.trim().parse::<u64>().ok())
177    };
178    let inactive_file = std::fs::read_to_string(directory.join("memory.stat"))
179        .ok()
180        .and_then(|stat| {
181            stat.lines()
182                .find_map(|line| line.strip_prefix("inactive_file "))
183                .and_then(|value| value.trim().parse::<u64>().ok())
184        })
185        .unwrap_or(0);
186    Some((
187        read("memory.current")?.saturating_sub(inactive_file),
188        read("memory.max")?,
189    ))
190}
191
192#[cfg(test)]
193#[path = "memory_tests.rs"]
194mod tests;