1#[cfg(any(target_os = "linux", target_os = "android"))]
5use std::path::Path;
6
7#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
9pub struct MemoryCapacity {
10 pub physical_bytes: Option<u64>,
12 pub available_bytes: Option<u64>,
15 pub limit_bytes: Option<u64>,
17 pub limit_source: &'static str,
19 pub used_in_limit_bytes: Option<u64>,
21}
22
23impl MemoryCapacity {
24 #[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 #[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#[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#[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#[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#[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;