Skip to main content

cbtop/bricks/collectors/
memory.rs

1//! Memory metrics collector
2//!
3//! Collects memory usage from /proc/meminfo (Genchi Genbutsu)
4
5use crate::brick::{Brick, BrickAssertion, BrickBudget, BrickVerification};
6use crate::ring_buffer::RingBuffer;
7use std::any::Any;
8use std::fs::read_to_string;
9use std::time::Instant;
10
11#[derive(Debug, Clone)]
12pub struct MemoryMetrics {
13    pub timestamp: Instant,
14    pub total_kb: u64,
15    pub available_kb: u64,
16    pub free_kb: u64,
17    pub swap_total_kb: u64,
18    pub swap_free_kb: u64,
19}
20
21impl Default for MemoryMetrics {
22    fn default() -> Self {
23        Self {
24            timestamp: Instant::now(),
25            total_kb: 0,
26            available_kb: 0,
27            free_kb: 0,
28            swap_total_kb: 0,
29            swap_free_kb: 0,
30        }
31    }
32}
33
34pub struct MemoryCollectorBrick {
35    history: RingBuffer<MemoryMetrics>,
36}
37
38impl MemoryCollectorBrick {
39    pub fn new() -> Self {
40        Self {
41            history: RingBuffer::new(120),
42        }
43    }
44
45    pub fn collect(&mut self) -> MemoryMetrics {
46        let metrics = self.read_meminfo().unwrap_or_default();
47        self.history.push(metrics.clone());
48        metrics
49    }
50
51    fn read_meminfo(&self) -> Result<MemoryMetrics, std::io::Error> {
52        let content = read_to_string("/proc/meminfo")?;
53        let mut metrics = MemoryMetrics {
54            timestamp: Instant::now(),
55            ..Default::default()
56        };
57
58        for line in content.lines() {
59            let parts: Vec<&str> = line.split_whitespace().collect();
60            if parts.len() < 2 {
61                continue;
62            }
63
64            let value = parts[1].parse::<u64>().unwrap_or(0);
65            match parts[0] {
66                "MemTotal:" => metrics.total_kb = value,
67                "MemAvailable:" => metrics.available_kb = value,
68                "MemFree:" => metrics.free_kb = value,
69                "SwapTotal:" => metrics.swap_total_kb = value,
70                "SwapFree:" => metrics.swap_free_kb = value,
71                _ => {}
72            }
73        }
74        Ok(metrics)
75    }
76
77    pub fn history(&self) -> &RingBuffer<MemoryMetrics> {
78        &self.history
79    }
80}
81
82impl Brick for MemoryCollectorBrick {
83    fn brick_name(&self) -> &'static str {
84        "memory_collector"
85    }
86
87    fn assertions(&self) -> Vec<BrickAssertion> {
88        vec![
89            BrickAssertion::custom("mem_total_positive", |b| {
90                let s = b
91                    .downcast_ref::<MemoryCollectorBrick>()
92                    .expect("brick MUST be MemoryCollectorBrick");
93                s.history.back().map_or(true, |m| m.total_kb > 0)
94            }),
95            BrickAssertion::max_latency_ms(2),
96        ]
97    }
98
99    fn budget(&self) -> BrickBudget {
100        BrickBudget {
101            collect_ms: 2,
102            layout_ms: 0,
103            render_ms: 0,
104        }
105    }
106
107    fn verify(&self) -> BrickVerification {
108        let mut v = BrickVerification::new();
109        for a in self.assertions() {
110            v.check(&a);
111        }
112        v
113    }
114
115    fn as_any(&self) -> &dyn Any {
116        self
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_memory_collector() {
126        let mut collector = MemoryCollectorBrick::new();
127        let metrics = collector.collect();
128        // On linux this should be > 0. In CI/other OS it might fail to read file.
129        // We assert logic, not environment.
130        if std::path::Path::new("/proc/meminfo").exists() {
131            assert!(metrics.total_kb > 0);
132        } else {
133            // Fallback for non-linux
134            assert!(metrics.total_kb == 0);
135        }
136    }
137}