Skip to main content

resource_tracker/collector/
memory.rs

1use crate::metrics::MemoryMetrics;
2use procfs::Meminfo;
3use procfs::prelude::*;
4
5type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
6
7/// Stateless: each call is a fresh snapshot from /proc/meminfo.
8/// Memory hardware type (DDR4/DDR5) requires SMBIOS/DMI parsing which needs
9/// elevated privileges - deferred to a later phase.
10pub struct MemoryCollector;
11
12impl MemoryCollector {
13    pub fn new() -> Self {
14        Self
15    }
16
17    pub fn collect(&self) -> Result<MemoryMetrics> {
18        let info = Meminfo::current()?;
19
20        // procfs 0.18 converts /proc/meminfo "kB" values to bytes internally.
21        // Divide by 1_048_576 to convert to mebibytes (MiB), standardized to
22        // match Python resource-tracker PR #9.
23        let to_mib = |bytes: u64| bytes / 1_048_576;
24
25        let total_mib = to_mib(info.mem_total);
26        let free_mib = to_mib(info.mem_free);
27        let available_mib = to_mib(info.mem_available.unwrap_or(info.mem_free));
28        let buffers_mib = to_mib(info.buffers);
29        let cached_mib = to_mib(info.cached) + to_mib(info.s_reclaimable.unwrap_or(0));
30        // Python formula: MemTotal - MemFree - Buffers - (Cached + SReclaimable)
31        let used_mib = total_mib
32            .saturating_sub(free_mib)
33            .saturating_sub(buffers_mib)
34            .saturating_sub(cached_mib);
35        let used_pct = if total_mib > 0 {
36            used_mib as f64 / total_mib as f64 * 100.0
37        } else {
38            0.0
39        };
40
41        let swap_total_mib = to_mib(info.swap_total);
42        let swap_used_mib = swap_total_mib.saturating_sub(to_mib(info.swap_free));
43        let swap_used_pct = if swap_total_mib > 0 {
44            swap_used_mib as f64 / swap_total_mib as f64 * 100.0
45        } else {
46            0.0
47        };
48
49        Ok(MemoryMetrics {
50            total_mib,
51            free_mib,
52            available_mib,
53            used_mib,
54            used_pct,
55            buffers_mib,
56            cached_mib,
57            swap_total_mib,
58            swap_used_mib,
59            swap_used_pct,
60            active_mib: to_mib(info.active),
61            inactive_mib: to_mib(info.inactive),
62        })
63    }
64}
65
66// ---------------------------------------------------------------------------
67// Unit tests
68// ---------------------------------------------------------------------------
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    // T-MEM-01: collect() succeeds on a Linux host and total_mib > 0.
75    #[test]
76    fn test_memory_collect_ok_and_total_positive() {
77        let m = MemoryCollector::new()
78            .collect()
79            .expect("collect() must succeed on Linux");
80        assert!(
81            m.total_mib > 0,
82            "total_mib must be > 0, got {}",
83            m.total_mib
84        );
85    }
86
87    // T-MEM-02: used_pct is in 0..=100.
88    #[test]
89    fn test_memory_used_pct_in_range() {
90        let m = MemoryCollector::new().collect().expect("collect() failed");
91        assert!(
92            m.used_pct >= 0.0 && m.used_pct <= 100.0,
93            "used_pct out of range: {}",
94            m.used_pct
95        );
96    }
97
98    // T-MEM-03: free_mib and available_mib do not exceed total_mib.
99    #[test]
100    fn test_memory_free_and_available_le_total() {
101        let m = MemoryCollector::new().collect().expect("collect() failed");
102        assert!(
103            m.free_mib <= m.total_mib,
104            "free_mib {} > total_mib {}",
105            m.free_mib,
106            m.total_mib
107        );
108        assert!(
109            m.available_mib <= m.total_mib,
110            "available_mib {} > total_mib {}",
111            m.available_mib,
112            m.total_mib
113        );
114    }
115
116    // T-MEM-04: swap fields are internally consistent.
117    #[test]
118    fn test_memory_swap_fields_consistent() {
119        let m = MemoryCollector::new().collect().expect("collect() failed");
120        assert!(
121            m.swap_used_mib <= m.swap_total_mib,
122            "swap_used_mib {} > swap_total_mib {}",
123            m.swap_used_mib,
124            m.swap_total_mib
125        );
126        if m.swap_total_mib == 0 {
127            assert_eq!(
128                m.swap_used_mib, 0,
129                "swap_used_mib must be 0 when swap_total_mib is 0"
130            );
131            assert_eq!(
132                m.swap_used_pct, 0.0,
133                "swap_used_pct must be 0.0 when swap_total_mib is 0"
134            );
135        }
136    }
137
138    // T-MEM-05: collect() is deterministic (two calls both succeed).
139    #[test]
140    fn test_memory_collect_is_repeatable() {
141        let c = MemoryCollector::new();
142        let _ = c.collect().expect("first collect() failed");
143        let _ = c.collect().expect("second collect() failed");
144    }
145}