resource_tracker/collector/
memory.rs1use crate::metrics::MemoryMetrics;
2use procfs::Meminfo;
3use procfs::prelude::*;
4
5type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
6
7pub 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 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 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 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#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[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 #[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 #[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 #[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 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 #[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 #[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}