Skip to main content

cgroup_memory/
lib.rs

1#![doc = include_str!("../README.md")]
2
3pub const MEMORY_STAT: &str = "/sys/fs/cgroup/memory.stat";
4pub const MEMORY_MAX: &str = "/sys/fs/cgroup/memory.max";
5
6use std::{
7    error::Error,
8    fmt::{self, Display, Formatter},
9    fs::{self, OpenOptions},
10    io::Read,
11    num::ParseIntError,
12};
13
14/// Represents the memory statistics from the [MEMORY_STAT] file.
15///
16/// The fields were taken from a file as seen within a `gcr.io/distroless/cc-debian12` Docker container image.
17/// No other images/linux distros were tested yet.
18///
19/// To increase compatibility with other distros, all fields are optional.
20#[derive(Debug, Default)]
21pub struct MemoryStat {
22    pub anon: Option<u64>,
23    pub file: Option<u64>,
24    pub kernel: Option<u64>,
25    pub kernel_stack: Option<u64>,
26    pub pagetables: Option<u64>,
27    pub sec_pagetables: Option<u64>,
28    pub percpu: Option<u64>,
29    pub sock: Option<u64>,
30    pub vmalloc: Option<u64>,
31    pub shmem: Option<u64>,
32    pub zswap: Option<u64>,
33    pub zswapped: Option<u64>,
34    pub file_mapped: Option<u64>,
35    pub file_dirty: Option<u64>,
36    pub file_writeback: Option<u64>,
37    pub swapcached: Option<u64>,
38    pub anon_thp: Option<u64>,
39    pub file_thp: Option<u64>,
40    pub shmem_thp: Option<u64>,
41    pub inactive_anon: Option<u64>,
42    pub active_anon: Option<u64>,
43    pub inactive_file: Option<u64>,
44    pub active_file: Option<u64>,
45    pub unevictable: Option<u64>,
46    pub slab_reclaimable: Option<u64>,
47    pub slab_unreclaimable: Option<u64>,
48    pub slab: Option<u64>,
49    pub workingset_refault_anon: Option<u64>,
50    pub workingset_refault_file: Option<u64>,
51    pub workingset_activate_anon: Option<u64>,
52    pub workingset_activate_file: Option<u64>,
53    pub workingset_restore_anon: Option<u64>,
54    pub workingset_restore_file: Option<u64>,
55    pub workingset_nodereclaim: Option<u64>,
56    pub pgscan: Option<u64>,
57    pub pgsteal: Option<u64>,
58    pub pgscan_kswapd: Option<u64>,
59    pub pgscan_direct: Option<u64>,
60    pub pgscan_khugepaged: Option<u64>,
61    pub pgsteal_kswapd: Option<u64>,
62    pub pgsteal_direct: Option<u64>,
63    pub pgsteal_khugepaged: Option<u64>,
64    pub pgfault: Option<u64>,
65    pub pgmajfault: Option<u64>,
66    pub pgrefill: Option<u64>,
67    pub pgactivate: Option<u64>,
68    pub pgdeactivate: Option<u64>,
69    pub pglazyfree: Option<u64>,
70    pub pglazyfreed: Option<u64>,
71    pub zswpin: Option<u64>,
72    pub zswpout: Option<u64>,
73    pub thp_fault_alloc: Option<u64>,
74    pub thp_collapse_alloc: Option<u64>,
75}
76
77#[derive(Debug)]
78pub enum ReadParseError {
79    Io(std::io::Error),
80    Parse(ParseIntError),
81    /// The memory file's values evaluted to a non-zero sum
82    Zero,
83}
84
85impl From<ParseIntError> for ReadParseError {
86    fn from(e: ParseIntError) -> Self {
87        ReadParseError::Parse(e)
88    }
89}
90
91impl From<std::io::Error> for ReadParseError {
92    fn from(e: std::io::Error) -> Self {
93        ReadParseError::Io(e)
94    }
95}
96
97impl Error for ReadParseError {}
98
99impl Display for ReadParseError {
100    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
101        match self {
102            ReadParseError::Io(io_error) => write!(f, "{}", io_error),
103            ReadParseError::Zero => write!(
104                f,
105                "The memory statistics could not be evaluted to a non-zero sum"
106            ),
107            ReadParseError::Parse(parse_error) => write!(f, "{}", parse_error),
108        }
109    }
110}
111
112/// Reads and parses the memory statistics file.
113///
114/// # Optional fields
115///
116/// * The memory statistics file [MEMORY_STAT] may not contain all fields. In this case, the field is assumed to be zero.
117/// * In case the field's value cannot be parsed to u64, the field is assumed to be zero (unlikely scenario).
118///
119/// # Errors
120/// Returns an error if the memory statistics file [MEMORY_STAT] could not be read or parsed.
121pub fn memory_stat() -> Result<MemoryStat, ReadParseError> {
122    let memory_stat_string = fs::read_to_string(MEMORY_STAT).map_err(ReadParseError::Io)?;
123
124    println!("{}", memory_stat_string);
125
126    let mut ms = MemoryStat::default();
127
128    // Parse the memory statistics
129    for line in memory_stat_string.lines() {
130        let parts: Vec<&str> = line.split_whitespace().collect();
131        if parts.len() == 2 {
132            let key = parts[0];
133
134            match key {
135                "anon" => ms.anon = parts[1].parse().ok(),
136                "file" => ms.file = parts[1].parse().ok(),
137                "kernel" => ms.kernel = parts[1].parse().ok(),
138                "kernel_stack" => ms.kernel_stack = parts[1].parse().ok(),
139                "pagetables" => ms.pagetables = parts[1].parse().ok(),
140                "sec_pagetables" => ms.sec_pagetables = parts[1].parse().ok(),
141                "percpu" => ms.percpu = parts[1].parse().ok(),
142                "sock" => ms.sock = parts[1].parse().ok(),
143                "vmalloc" => ms.vmalloc = parts[1].parse().ok(),
144                "shmem" => ms.shmem = parts[1].parse().ok(),
145                "zswap" => ms.zswap = parts[1].parse().ok(),
146                "zswapped" => ms.zswapped = parts[1].parse().ok(),
147                "file_mapped" => ms.file_mapped = parts[1].parse().ok(),
148                "file_dirty" => ms.file_dirty = parts[1].parse().ok(),
149                "file_writeback" => ms.file_writeback = parts[1].parse().ok(),
150                "swapcached" => ms.swapcached = parts[1].parse().ok(),
151                "anon_thp" => ms.anon_thp = parts[1].parse().ok(),
152                "file_thp" => ms.file_thp = parts[1].parse().ok(),
153                "shmem_thp" => ms.shmem_thp = parts[1].parse().ok(),
154                "inactive_anon" => ms.inactive_anon = parts[1].parse().ok(),
155                "active_anon" => ms.active_anon = parts[1].parse().ok(),
156                "inactive_file" => ms.inactive_file = parts[1].parse().ok(),
157                "active_file" => ms.active_file = parts[1].parse().ok(),
158                "unevictable" => ms.unevictable = parts[1].parse().ok(),
159                "slab_reclaimable" => ms.slab_reclaimable = parts[1].parse().ok(),
160                "slab_unreclaimable" => ms.slab_unreclaimable = parts[1].parse().ok(),
161                "slab" => ms.slab = parts[1].parse().ok(),
162                "workingset_refault_anon" => ms.workingset_refault_anon = parts[1].parse().ok(),
163                "workingset_refault_file" => ms.workingset_refault_file = parts[1].parse().ok(),
164                "workingset_activate_anon" => ms.workingset_activate_anon = parts[1].parse().ok(),
165                "workingset_activate_file" => ms.workingset_activate_file = parts[1].parse().ok(),
166                "workingset_restore_anon" => ms.workingset_restore_anon = parts[1].parse().ok(),
167                "workingset_restore_file" => ms.workingset_restore_file = parts[1].parse().ok(),
168                "workingset_nodereclaim" => ms.workingset_nodereclaim = parts[1].parse().ok(),
169                "pgscan" => ms.pgscan = parts[1].parse().ok(),
170                "pgsteal" => ms.pgsteal = parts[1].parse().ok(),
171                "pgscan_kswapd" => ms.pgscan_kswapd = parts[1].parse().ok(),
172                "pgscan_direct" => ms.pgscan_direct = parts[1].parse().ok(),
173                "pgscan_khugepaged" => ms.pgscan_khugepaged = parts[1].parse().ok(),
174                "pgsteal_kswapd" => ms.pgsteal_kswapd = parts[1].parse().ok(),
175                "pgsteal_direct" => ms.pgsteal_direct = parts[1].parse().ok(),
176                "pgsteal_khugepaged" => ms.pgsteal_khugepaged = parts[1].parse().ok(),
177                "pgfault" => ms.pgfault = parts[1].parse().ok(),
178                "pgmajfault" => ms.pgmajfault = parts[1].parse().ok(),
179                "pgrefill" => ms.pgrefill = parts[1].parse().ok(),
180                "pgactivate" => ms.pgactivate = parts[1].parse().ok(),
181                "pgdeactivate" => ms.pgdeactivate = parts[1].parse().ok(),
182                "pglazyfree" => ms.pglazyfree = parts[1].parse().ok(),
183                "pglazyfreed" => ms.pglazyfreed = parts[1].parse().ok(),
184                "zswpin" => ms.zswpin = parts[1].parse().ok(),
185                "zswpout" => ms.zswpout = parts[1].parse().ok(),
186                "thp_fault_alloc" => ms.thp_fault_alloc = parts[1].parse().ok(),
187                "thp_collapse_alloc" => ms.thp_collapse_alloc = parts[1].parse().ok(),
188                _ => {}
189            }
190        }
191    }
192    Ok(ms)
193}
194
195/// Calculates the net used memory in bytes.
196///
197/// Formula: `anon + file + kernel + kernel_stack + pagetables + percpu + slab_unreclaimable - slab_reclaimable`
198///
199/// In case one of the fields is not present in the memory statistics file, the field is assumed to be zero.
200///
201/// # Errors
202/// Returns an error if the memory statistics file [MEMORY_STAT] could not be read or the formula evaluted to a zero sum.
203///
204/// # Overflow
205/// In theory the arithmetic operation may overflow:
206/// * sum of occupied memory fields is greater than `u64::MAX`. Given the current state of technology, it is very unlikely that this will happen as `u64` represents ~ 18446744073 GB.
207/// * `slab_reclaimable` is greater than the sum of all other fields that represent occupied memory.
208pub fn memory_net_used_calc(ms: &MemoryStat) -> Result<u64, ReadParseError> {
209    let total_net_used_memory = ms.anon.unwrap_or(0)
210        + ms.file.unwrap_or(0)
211        + ms.kernel.unwrap_or(0)
212        + ms.kernel_stack.unwrap_or(0)
213        + ms.pagetables.unwrap_or(0)
214        + ms.percpu.unwrap_or(0)
215        + ms.slab_unreclaimable.unwrap_or(0)
216        - ms.slab_reclaimable.unwrap_or(0);
217
218    if total_net_used_memory == 0 {
219        return Err(ReadParseError::Zero);
220    }
221
222    Ok(total_net_used_memory)
223}
224
225/// Returns the net used memory in bytes.
226///
227/// * Reads and parses [MEMORY_STAT] via [memory_stat]
228/// * Calculates the net used memory via [memory_net_used_calc]
229pub fn memory_net_used() -> Result<u64, ReadParseError> {
230    let ms = memory_stat()?;
231    memory_net_used_calc(&ms)
232}
233
234/// Parses the max memory line.
235///
236/// # Returns
237///
238/// - `None` if `line` is "max".
239/// - `Some(u64)` if the line could be parsed to `u64`.
240///
241/// # Errors
242///
243/// Returns an error if the line could not be parsed to `u64` and it is not "max".
244pub fn memory_max_parse(line: &str) -> Result<Option<u64>, ParseIntError> {
245    if line == "max" {
246        return Ok(None);
247    }
248
249    line.trim().parse::<u64>().map(Some)
250}
251
252/// Parses the line to u64.
253///
254/// # Safety
255///
256/// This function is unsafe because it expects the input to be either "max" or a valid u64.
257///
258/// # Panics
259///
260/// This function panics if the input is not "max" or a valid u64.
261fn memory_max_parse_unsafe(line: &str) -> Option<u64> {
262    if line == "max" {
263        return None;
264    }
265
266    unsafe { Some(line.trim().parse::<u64>().unwrap_unchecked()) }
267}
268
269/// Reads and parses the memory max file.
270///
271/// Data source: [MEMORY_MAX]
272///
273/// # Errors
274/// Returns an error if the memory max file [MEMORY_MAX] could not be read or parsed.
275///
276/// # Example
277/// ```rust
278/// match memory_max() {
279///     Ok(Some(v)) => println!("Max memory: {v}"),
280///     Ok(None) => println!("No max memory constraint"),
281///     Err(e) => println!("Failed to read and parse memory files: {e}"),
282/// }
283/// ``````
284pub fn memory_max() -> Result<Option<u64>, ReadParseError> {
285    let mut file = OpenOptions::new().read(true).open(MEMORY_MAX)?;
286    let mut buffer = [0; 4096];
287
288    let bytes_read = file.read(&mut buffer)?;
289    let content = std::str::from_utf8(&buffer[..bytes_read]).map_err(|_e| ReadParseError::Zero)?;
290    Ok(memory_max_parse(content)?)
291}
292
293/// Reads and parses the memory max file using unsafe code.
294///
295/// # Safety
296/// This function is unsafe because it uses `std::str::from_utf8_unchecked`, which can lead to undefined behavior if the content is not valid ASCII. However the [Kernel doc](https://www.kernel.org/doc/Documentation/filesystems/sysfs.txt) states that attributes should be ASCII text files.
297/// 
298/// If the file content exceeds the buffer size (1024 bytes), only part of the file will be read, potentially causing incorrect parsing.
299///
300/// 
301///
302/// # Errors
303/// Returns an error if the memory max file could not be read.
304///
305/// # Example
306/// ```rust
307/// match memory_max_unsafe() {
308///     Ok(Some(v)) => println!("Max memory: {v}"),
309///     Ok(None) => println!("No max memory constraint"),
310///     Err(e) => println!("Failed to read and parse memory files: {e}"),
311/// }
312/// ```
313pub fn memory_max_unsafe() -> Result<Option<u64>, ReadParseError> {
314    let mut file = OpenOptions::new()
315        .read(true)
316        .open(MEMORY_MAX)
317        .map_err(ReadParseError::Io)?;
318    let mut buffer = [0; 1024];
319    let bytes_read = file.read(&mut buffer).map_err(ReadParseError::Io)?;
320
321    // SAFETY: Linux guarantees that all of *sysfs* is valid ASCII.
322    let content = unsafe { std::str::from_utf8_unchecked(&buffer[..bytes_read]) };
323    Ok(memory_max_parse_unsafe(content))
324}
325
326/// Returns the available memory in bytes.
327///
328/// Formula: [memory_max()] - [memory_net_used()]
329///
330/// # Errors
331/// Returns an error if the memory max file [MEMORY_MAX] or the memory statistics file [MEMORY_STAT] could not be read or parsed.
332///
333/// # Panics
334/// This function will panic if any arithmetic operation (subtraction) overflows.
335///
336/// # Example
337/// ```rust
338/// match memory_available() {
339///     Ok(Some(available)) => println!("Available memory: {} bytes", available),
340///     Ok(None) => println!("No memory limit set"),
341///     Err(e) => println!("Failed to read memory information: {}", e),
342/// }
343/// ```
344pub fn memory_available() -> Result<Option<u64>, ReadParseError> {
345    let max = match memory_max()? {
346        Some(value) => value,
347        None => return Ok(None),
348    };
349    let net_used = memory_net_used()?;
350    Ok(Some(max - net_used))
351}