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 buffers_mib = to_mib(info.buffers);
28        let cached_mib = to_mib(info.cached) + to_mib(info.s_reclaimable.unwrap_or(0));
29        // Python: MemAvailable, falling back to MemFree + Buffers + Cached + SReclaimable
30        // on kernels that predate MemAvailable (Linux 3.14).
31        let available_bytes = info.mem_available.unwrap_or(
32            info.mem_free + info.buffers + info.cached + info.s_reclaimable.unwrap_or(0),
33        );
34        let available_mib = to_mib(available_bytes);
35        // Python: (MemTotal - MemAvailable) / 1024 — psutil-compatible used RAM.
36        let used_mib = to_mib(info.mem_total.saturating_sub(available_bytes));
37        let used_pct = if total_mib > 0 {
38            used_mib as f64 / total_mib as f64 * 100.0
39        } else {
40            0.0
41        };
42
43        let swap_total_mib = to_mib(info.swap_total);
44        let swap_used_mib = swap_total_mib.saturating_sub(to_mib(info.swap_free));
45        let swap_used_pct = if swap_total_mib > 0 {
46            swap_used_mib as f64 / swap_total_mib as f64 * 100.0
47        } else {
48            0.0
49        };
50
51        Ok(MemoryMetrics {
52            total_mib,
53            free_mib,
54            available_mib,
55            used_mib,
56            used_pct,
57            buffers_mib,
58            cached_mib,
59            swap_total_mib,
60            swap_used_mib,
61            swap_used_pct,
62            active_mib: to_mib(info.active),
63            inactive_mib: to_mib(info.inactive),
64        })
65    }
66}
67
68// ---------------------------------------------------------------------------
69// Unit tests
70// ---------------------------------------------------------------------------
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    // T-MEM-01: collect() succeeds on a Linux host and total_mib > 0.
77    #[test]
78    fn test_memory_collect_ok_and_total_positive() {
79        let m = MemoryCollector::new()
80            .collect()
81            .expect("collect() must succeed on Linux");
82        assert!(
83            m.total_mib > 0,
84            "total_mib must be > 0, got {}",
85            m.total_mib
86        );
87    }
88
89    // T-MEM-02: used_pct is in 0..=100.
90    #[test]
91    fn test_memory_used_pct_in_range() {
92        let m = MemoryCollector::new().collect().expect("collect() failed");
93        assert!(
94            m.used_pct >= 0.0 && m.used_pct <= 100.0,
95            "used_pct out of range: {}",
96            m.used_pct
97        );
98    }
99
100    // T-MEM-03: free_mib and available_mib do not exceed total_mib.
101    #[test]
102    fn test_memory_free_and_available_le_total() {
103        let m = MemoryCollector::new().collect().expect("collect() failed");
104        assert!(
105            m.free_mib <= m.total_mib,
106            "free_mib {} > total_mib {}",
107            m.free_mib,
108            m.total_mib
109        );
110        assert!(
111            m.available_mib <= m.total_mib,
112            "available_mib {} > total_mib {}",
113            m.available_mib,
114            m.total_mib
115        );
116    }
117
118    // T-MEM-04: used_mib matches (MemTotal - MemAvailable) in MiB (Python formula).
119    #[test]
120    fn test_memory_used_is_total_minus_available() {
121        let m = MemoryCollector::new().collect().expect("collect() failed");
122        assert!(
123            m.used_mib <= m.total_mib,
124            "used_mib {} > total_mib {}",
125            m.used_mib,
126            m.total_mib
127        );
128        assert!(
129            m.used_mib + m.available_mib <= m.total_mib,
130            "used_mib ({}) + available_mib ({}) exceeds total_mib ({})",
131            m.used_mib,
132            m.available_mib,
133            m.total_mib
134        );
135        // used_pct must agree with used_mib / total_mib.
136        if m.total_mib > 0 {
137            let expected_pct = m.used_mib as f64 / m.total_mib as f64 * 100.0;
138            assert!(
139                (m.used_pct - expected_pct).abs() < 0.01,
140                "used_pct {} != used_mib/total_mib*100 ({expected_pct})",
141                m.used_pct
142            );
143        }
144    }
145
146    // T-MEM-05: swap fields are internally consistent.
147    #[test]
148    fn test_memory_swap_fields_consistent() {
149        let m = MemoryCollector::new().collect().expect("collect() failed");
150        assert!(
151            m.swap_used_mib <= m.swap_total_mib,
152            "swap_used_mib {} > swap_total_mib {}",
153            m.swap_used_mib,
154            m.swap_total_mib
155        );
156        if m.swap_total_mib == 0 {
157            assert_eq!(
158                m.swap_used_mib, 0,
159                "swap_used_mib must be 0 when swap_total_mib is 0"
160            );
161            assert_eq!(
162                m.swap_used_pct, 0.0,
163                "swap_used_pct must be 0.0 when swap_total_mib is 0"
164            );
165        }
166    }
167
168    // T-MEM-06: collect() is deterministic (two calls both succeed).
169    #[test]
170    fn test_memory_collect_is_repeatable() {
171        let c = MemoryCollector::new();
172        let _ = c.collect().expect("first collect() failed");
173        let _ = c.collect().expect("second collect() failed");
174    }
175}