Skip to main content

ferrix_lib/
ram.rs

1/* ram.rs
2 *
3 * Copyright 2025 Michail Krasnov <mskrasnov07@ya.ru>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21//! Get information about RAM
22//!
23//! Reads information from `/proc/meminfo` file
24//!
25//! ## Example
26//! ```
27//! use ferrix_lib::ram::RAM;
28//! use ferrix_lib::traits::ToJson;
29//!
30//! let ram = RAM::new().unwrap();
31//! let json = ram.to_json().unwrap();
32//! dbg!(json);
33//! ```
34
35use anyhow::{Result, anyhow};
36use serde::Serialize;
37use std::fs::read_to_string;
38use std::path::Path;
39
40use crate::traits::ToJson;
41use crate::utils::Size;
42
43/// A structure containing data from the `/proc/meminfo` file
44#[derive(Debug, Serialize, Default, Clone)]
45pub struct RAM {
46    /// Total usable physical RAM (excludes reserved/firmware memory)
47    pub total: Size,
48
49    /// Completely unused physical RAM. Often **misleadingly low**
50    /// due to caching
51    pub free: Size,
52
53    /// **Best estimate** of memory available for new apps (accounts
54    /// for caches/reclaimable memory)
55    pub available: Size,
56
57    /// Temporary storage for raw disk blocks (e.g. filesystem
58    /// metadata)
59    pub buffers: Size,
60
61    /// Page cache for files read from disk (reclaimed when apps
62    /// need memory)
63    pub cached: Size,
64
65    /// Memory swapped out but later accessed, now present in both
66    /// RAM and swap
67    pub swap_cached: Size,
68
69    /// Recently used memory (harder to reclaim)
70    pub active: Size,
71
72    /// Less recently used memory (eased to reclaim)
73    pub inactive: Size,
74
75    /// Active anonymous pages (e.g. heap/stack, not file-backed)
76    pub active_anon: Size,
77
78    /// Inactive anonymous pages
79    pub inactive_anon: Size,
80
81    /// Active file-backed pages (cached files)
82    pub active_file: Size,
83
84    /// Inactive file-backed pages
85    pub inactive_file: Size,
86
87    /// Memory that cannot be pages out (e.g. locked with `mlock()`)
88    pub unevictable: Size,
89
90    /// Memory that is locked (cannot be swapped out)
91    pub mlocked: Size,
92
93    /// Total amount of swap space available
94    pub swap_total: Size,
95
96    /// Amount of swap space that is currently unused
97    pub swap_free: Size,
98
99    pub zswap: Size,
100    pub zswapped: Size,
101
102    /// Data waiting to be written to disk
103    pub dirty: Size,
104
105    /// Data actively being written to disk
106    pub writeback: Size,
107
108    /// Non file-backed pages mapped into user-space page tables
109    pub anon_pages: Size,
110
111    /// Files (like libraries) that have been mapped into memory
112    /// (also includes `tmpfs`/`shmem`)
113    pub mapped: Size,
114
115    /// Total memory used by shared memory (`shmem`) and `tmpfs`
116    pub shmem: Size,
117
118    /// Kernel allocations that the kernel will attempt to reclaim
119    /// under memory pressure (includes `SReclaimable` and other
120    /// reclaimable slabs)
121    pub kreclaimable: Size,
122
123    /// In-kernel data structures cache (includes `SReclaimable` and
124    /// `SUnreclaim`)
125    pub slab: Size,
126
127    /// Part of `Slab` that might be reclaimed, such as caches for
128    /// directory inodes, etc.
129    pub sreclaimable: Size,
130
131    /// Part of `Slab` that cannot be reclaimed
132    pub sunreclaim: Size,
133
134    /// Memory used by kernel stacks
135    pub kernel_stack: Size,
136
137    /// Memory used by page tables (to map virtual to physical
138    /// addresses)
139    pub page_tables: Size,
140
141    pub sec_page_tables: Size,
142
143    /// Memory that has been sent to the NFS server but not yet
144    /// committed to stable storage
145    pub nfs_unstable: Size,
146
147    /// Memory used for block device bounce buffers (rarely used on
148    /// modern systems)
149    pub bounce: Size,
150
151    /// Memory used by FUSE for temporary writeback buffers
152    pub writeback_tmp: Size,
153
154    /// Based on the overcommit ratio, this is the total amount of
155    /// memory currently available to be allocated
156    pub commit_limit: Size,
157
158    /// he amount of memory currently allocated by the system. The
159    /// kernel may overcommit this
160    pub commited_as: Size,
161
162    /// Total size of `vmalloc` memory area
163    pub vmalloc_total: Size,
164
165    /// Amount of `vmalloc` area which is used
166    pub vmalloc_used: Size,
167
168    /// Largest contiguous block of free `vmalloc` space
169    pub vmalloc_chunk: Size,
170
171    /// Memory used for per-cpu allocations (each CPU has its own
172    /// block)
173    pub percpu: Size,
174
175    /// Memory that the kernel identified as corrupted (when
176    /// `CONFIG_MEMORY_FAILURE` is enabled)
177    pub hardware_corrupted: Size,
178
179    /// Non-file backed huge pages mapped into user-space page tables
180    /// (transparent hugepages)
181    pub anon_huge_pages: Size,
182
183    /// Huge pages used by shared memory (`shmem`) and `tmpfs`.
184    pub shmem_huge_pages: Size,
185
186    /// `shmem`/`tmpfs` memory that is mapped into user space with
187    /// huge pages
188    pub shmem_pmd_mapped: Size,
189
190    /// Total CMA (Contiguous Memory Allocator) area
191    pub cma_total: Option<Size>,
192
193    /// Free memory in the CMA area
194    pub cma_free: Option<Size>,
195
196    pub file_huge_pages: Size,
197    pub file_pmd_mapped: Size,
198    pub unaccepted: Size,
199    pub huge_pages_total: u32,
200    pub huge_pages_free: u32,
201    pub huge_pages_rsvd: u32,
202    pub huge_pages_surp: u32,
203    pub huge_page_size: Size,
204    pub huge_tlb: Size,
205    pub direct_map_4k: Size,
206    pub direct_map_2m: Size,
207    pub direct_map_1g: Size,
208}
209
210impl RAM {
211    pub fn new() -> Result<Self> {
212        let chunks = read_to_string("/proc/meminfo")?;
213        let chunks = chunks
214            .lines()
215            .map(|item| {
216                let mut items = item.split(':').map(|item| item.trim());
217                (items.next(), items.next())
218            })
219            .collect::<Vec<_>>();
220        let mut ram = RAM::default();
221        for chunk in chunks {
222            match chunk {
223                (Some(key), Some(val)) => match key {
224                    "MemTotal" => ram.total = Size::try_from(val)?,
225                    "MemFree" => ram.free = Size::try_from(val)?,
226                    "MemAvailable" => ram.available = Size::try_from(val)?,
227                    "Buffers" => ram.buffers = Size::try_from(val)?,
228                    "Cached" => ram.cached = Size::try_from(val)?,
229                    "SwapCached" => ram.swap_cached = Size::try_from(val)?,
230                    "Active" => ram.active = Size::try_from(val)?,
231                    "Inactive" => ram.inactive = Size::try_from(val)?,
232                    "Active(anon)" => ram.active_anon = Size::try_from(val)?,
233                    "Inactive(anon)" => ram.inactive_anon = Size::try_from(val)?,
234                    "Active(file)" => ram.active_file = Size::try_from(val)?,
235                    "Inactive(file)" => ram.inactive_file = Size::try_from(val)?,
236                    "Unevictable" => ram.unevictable = Size::try_from(val)?,
237                    "Mlocked" => ram.mlocked = Size::try_from(val)?,
238                    "SwapTotal" => ram.swap_total = Size::try_from(val)?,
239                    "SwapFree" => ram.swap_free = Size::try_from(val)?,
240                    "Zswap" => ram.zswap = Size::try_from(val)?,
241                    "Zswapped" => ram.zswapped = Size::try_from(val)?,
242                    "Dirty" => ram.dirty = Size::try_from(val)?,
243                    "Writeback" => ram.writeback = Size::try_from(val)?,
244                    "AnonPages" => ram.anon_pages = Size::try_from(val)?,
245                    "Mapped" => ram.mapped = Size::try_from(val)?,
246                    "Shmem" => ram.shmem = Size::try_from(val)?,
247                    "KReclaimable" => ram.kreclaimable = Size::try_from(val)?,
248                    "Slab" => ram.slab = Size::try_from(val)?,
249                    "SReclaimable" => ram.sreclaimable = Size::try_from(val)?,
250                    "SUnreclaim" => ram.sunreclaim = Size::try_from(val)?,
251                    "KernelStack" => ram.kernel_stack = Size::try_from(val)?,
252                    "PageTables" => ram.page_tables = Size::try_from(val)?,
253                    "SecPageTables" => ram.sec_page_tables = Size::try_from(val)?,
254                    "NFS_Unstable" => ram.nfs_unstable = Size::try_from(val)?,
255                    "Bounce" => ram.bounce = Size::try_from(val)?,
256                    "WritebackTmp" => ram.writeback_tmp = Size::try_from(val)?,
257                    "CommitLimit" => ram.commit_limit = Size::try_from(val)?,
258                    "Committed_AS" => ram.commited_as = Size::try_from(val)?,
259                    "VmallocTotal" => ram.vmalloc_total = Size::try_from(val)?,
260                    "VmallocUsed" => ram.vmalloc_used = Size::try_from(val)?,
261                    "VmallocChunk" => ram.vmalloc_chunk = Size::try_from(val)?,
262                    "Percpu" => ram.percpu = Size::try_from(val)?,
263                    "HardwareCorrupted" => ram.hardware_corrupted = Size::try_from(val)?,
264                    "AnonHugePages" => ram.anon_huge_pages = Size::try_from(val)?,
265                    "ShmemHugePages" => ram.shmem_huge_pages = Size::try_from(val)?,
266                    "ShmemPmdMapped" => ram.shmem_pmd_mapped = Size::try_from(val)?,
267                    "CmaTotal" => ram.cma_total = Size::try_from(val).ok(),
268                    "CmaFree" => ram.cma_free = Size::try_from(val).ok(),
269                    "FileHugePages" => ram.file_huge_pages = Size::try_from(val)?,
270                    "FilePmdMapped" => ram.file_pmd_mapped = Size::try_from(val)?,
271                    "Unaccepted" => ram.unaccepted = Size::try_from(val)?,
272                    "HugePages_Total" => ram.huge_pages_total = val.parse()?,
273                    "HugePages_Free" => ram.huge_pages_free = val.parse()?,
274                    "HugePages_Rsvd" => ram.huge_pages_rsvd = val.parse()?,
275                    "HugePages_Surp" => ram.huge_pages_surp = val.parse()?,
276                    "Hugepagesize" => ram.huge_page_size = Size::try_from(val)?,
277                    "Hugetlb" => ram.huge_tlb = Size::try_from(val)?,
278                    "DirectMap4k" => ram.direct_map_4k = Size::try_from(val)?,
279                    "DirectMap2M" => ram.direct_map_2m = Size::try_from(val)?,
280                    "DirectMap1G" => ram.direct_map_1g = Size::try_from(val)?,
281                    _ => continue,
282                },
283                _ => {}
284            }
285        }
286        Ok(ram)
287    }
288
289    pub fn used_ram(&self, base: u8) -> Size {
290        if base != 2 && base != 10 {
291            panic!("Unknown base: {base} (supported values: 2 or 10)");
292        }
293
294        let total = if base == 2 {
295            self.total.get_bytes2().unwrap_or(0) as f32 / 1024. / 1024. / 1024.
296        } else
297        /* base == 10 */
298        {
299            self.total.get_bytes10().unwrap_or(0) as f32 / 1000. / 1000. / 1000.
300        };
301
302        let avail = if base == 2 {
303            self.available.get_bytes2().unwrap_or(0) as f32 / 1024. / 1024. / 1024.
304        } else
305        /* base == 10 */
306        {
307            self.available.get_bytes10().unwrap_or(0) as f32 / 1000. / 1000. / 1000.
308        };
309        let used = total - avail;
310
311        Size::GB(used)
312    }
313
314    pub fn usage_percentage(&self) -> Option<f64> {
315        let total_ram = self.total.get_bytes2()?;
316        let avail_ram = self.available.get_bytes2()?;
317        let used_ram = total_ram - avail_ram;
318
319        Some(used_ram as f64 / total_ram as f64 * 100.)
320    }
321}
322
323impl ToJson for RAM {}
324
325/// Information about swap files or partitions
326#[derive(Debug, Serialize, Clone)]
327pub struct Swaps {
328    pub swaps: Vec<Swap>,
329}
330
331impl Swaps {
332    pub fn new() -> Result<Self> {
333        let mut swaps = vec![];
334
335        let data = read_to_string("/proc/swaps")?;
336        let items = data.lines().skip(1);
337
338        for swap in items {
339            swaps.push(Swap::try_from(swap)?)
340        }
341
342        Ok(Self { swaps })
343    }
344}
345
346impl ToJson for Swaps {}
347
348#[derive(Debug, Serialize, Clone)]
349pub struct Swap {
350    /// Path to the file or partition
351    pub filename: String,
352
353    /// Type
354    pub swap_type: String,
355
356    /// Swap size
357    pub size: Size,
358
359    /// Used space
360    pub used: Size,
361
362    /// Priority of this swap file/partition
363    pub priority: i8,
364}
365
366impl Swap {
367    pub fn usage_percentage(&self) -> Option<f64> {
368        let total_swap = self.size.get_bytes2()?;
369        let used_swap = self.used.get_bytes2()?;
370
371        Some(used_swap as f64 / total_swap as f64 * 100.)
372    }
373}
374
375impl TryFrom<&str> for Swap {
376    type Error = anyhow::Error;
377
378    fn try_from(value: &str) -> Result<Self> {
379        let data = value.split_whitespace().collect::<Vec<_>>();
380        if data.len() != 5 {
381            return Err(anyhow!("Format of the \"{value}\" string is incorrect!"));
382        }
383
384        Ok(Self {
385            filename: data[0].to_string(),
386            swap_type: data[1].to_string(),
387            size: Size::KB(data[2].parse()?),
388            used: Size::KB(data[3].parse()?),
389            priority: data[4].parse()?,
390        })
391    }
392}
393impl ToJson for Swap {}
394
395#[derive(Debug, Clone, Serialize)]
396pub struct Zswap {
397    pub accept_threshold_percent: Option<u8>,
398    pub compressor: Option<String>,
399    pub enabled: Option<bool>,
400    pub max_pool_percent: Option<u8>,
401    pub non_same_filled_pages_enabled: Option<bool>,
402    pub same_filled_pages_enabled: Option<bool>,
403    pub zpool: Option<String>,
404    pub pool_total_size: Option<Size>,
405    pub stored_pages_size: Option<Size>,
406    pub compression_ratio: Option<f32>,
407}
408
409fn get_page_size() -> u64 {
410    unsafe { libc::sysconf(libc::_SC_PAGESIZE) as u64 }
411}
412
413impl Zswap {
414    pub fn new() -> Self {
415        let rdbg = |f: &str| read_to_string(Path::new("/sys/kernel/debug/zswap/").join(f)).ok();
416        let fcnf =
417            |f: &str| read_to_string(Path::new("/sys/module/zswap/parameters/").join(f)).ok();
418        let get_bool = |content: &str| {
419            if content.trim() == "Y" || content != "0" {
420                true
421            } else {
422                false
423            }
424        };
425
426        let accept_threshold_percent =
427            fcnf("accept_threshold_percent").and_then(|atp| atp.trim().parse::<u8>().ok());
428        let compressor = fcnf("compressor");
429        let enabled = fcnf("enabled").and_then(|e| Some(get_bool(e.trim())));
430        let max_pool_percent =
431            fcnf("max_pool_percent").and_then(|mpp| mpp.trim().parse::<u8>().ok());
432        let non_same_filled_pages_enabled =
433            fcnf("non_same_filled_pages_enabled").and_then(|nsfpe| Some(get_bool(nsfpe.trim())));
434        let same_filled_pages_enabled =
435            fcnf("same_filled_pages_enabled").and_then(|sfpe| Some(get_bool(sfpe.trim())));
436        let zpool = fcnf("zpool");
437
438        let pool_total_size =
439            rdbg("pool_total_size").and_then(|pts| pts.trim().parse::<u64>().ok()); // bytes
440        let stored_page_size =
441            rdbg("stored_page_size").and_then(|sps| sps.trim().parse::<u64>().ok()); // bytes
442        let compression_ratio = match (pool_total_size, stored_page_size) {
443            (Some(pts), Some(sps)) => Some(sps as f32 / pts as f32),
444            _ => None,
445        };
446
447        Self {
448            accept_threshold_percent,
449            compressor,
450            enabled,
451            max_pool_percent,
452            non_same_filled_pages_enabled,
453            same_filled_pages_enabled,
454            zpool,
455            pool_total_size: pool_total_size.and_then(|pts| Some(Size::B(pts))),
456            stored_pages_size: stored_page_size
457                .and_then(|sps| Some(Size::B(sps * get_page_size()))),
458            compression_ratio,
459        }
460    }
461}