Skip to main content

nntp_proxy/tui/
system_stats.rs

1//! System resource monitoring for TUI display
2//!
3//! Tracks CPU usage and memory consumption for the proxy process.
4
5use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};
6
7/// System resource statistics for the current process
8#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
9pub struct SystemStats {
10    /// CPU usage percentage (0.0 - 100.0 per core, can exceed 100.0 on multi-core)
11    pub cpu_usage: f32,
12    /// Peak CPU usage percentage
13    pub peak_cpu_usage: f32,
14    /// Memory usage in bytes
15    pub memory_bytes: u64,
16    /// Peak memory usage in bytes
17    pub peak_memory_bytes: u64,
18}
19
20/// System resource monitor
21///
22/// Efficiently tracks process-specific metrics using sysinfo.
23/// Call `update()` periodically (e.g., every TUI frame) to refresh stats.
24pub struct SystemMonitor {
25    system: System,
26    pid: sysinfo::Pid,
27    peak_cpu: f32,
28    peak_memory: u64,
29}
30
31impl SystemMonitor {
32    fn refresh_kind() -> ProcessRefreshKind {
33        ProcessRefreshKind::nothing()
34            .with_cpu()
35            .with_memory()
36            .without_tasks()
37    }
38
39    /// Create a new system monitor for the current process
40    ///
41    /// # Panics
42    /// Panics if `sysinfo` cannot determine the current process ID.
43    #[must_use]
44    pub fn new() -> Self {
45        let mut system =
46            System::new_with_specifics(RefreshKind::nothing().with_processes(Self::refresh_kind()));
47        let pid = sysinfo::get_current_pid().expect("Failed to get current PID");
48        system.refresh_processes_specifics(
49            ProcessesToUpdate::Some(&[pid]),
50            true,
51            Self::refresh_kind(),
52        );
53
54        Self {
55            system,
56            pid,
57            peak_cpu: 0.0,
58            peak_memory: 0,
59        }
60    }
61
62    /// Update and retrieve current system stats
63    ///
64    /// Should be called at regular intervals (e.g., TUI update rate of ~4Hz).
65    /// First call may return zero for CPU usage - sysinfo needs 2+ samples.
66    #[must_use]
67    pub fn update(&mut self) -> SystemStats {
68        self.system.refresh_processes_specifics(
69            ProcessesToUpdate::Some(&[self.pid]),
70            true,
71            Self::refresh_kind(),
72        );
73
74        if let Some(process) = self.system.process(self.pid) {
75            let cpu = process.cpu_usage();
76            let memory = process.memory();
77
78            if cpu > self.peak_cpu {
79                self.peak_cpu = cpu;
80            }
81            if memory > self.peak_memory {
82                self.peak_memory = memory;
83            }
84
85            SystemStats {
86                cpu_usage: cpu,
87                peak_cpu_usage: self.peak_cpu,
88                memory_bytes: memory,
89                peak_memory_bytes: self.peak_memory,
90            }
91        } else {
92            SystemStats::default()
93        }
94    }
95
96    /// Get current stats without refreshing
97    ///
98    /// Returns the last refreshed stats. Useful if you just called `update()`.
99    #[must_use]
100    pub fn current(&self) -> SystemStats {
101        self.system
102            .process(self.pid)
103            .map_or_else(SystemStats::default, |process| SystemStats {
104                cpu_usage: process.cpu_usage(),
105                peak_cpu_usage: self.peak_cpu,
106                memory_bytes: process.memory(),
107                peak_memory_bytes: self.peak_memory,
108            })
109    }
110}
111
112impl Default for SystemMonitor {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118#[cfg(test)]
119#[allow(clippy::float_cmp)] // These tests assert exact default/current CPU values from deterministic fixtures.
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_refresh_kind_only_enables_needed_process_fields() {
125        let kind = SystemMonitor::refresh_kind();
126
127        assert!(kind.cpu());
128        assert!(kind.memory());
129        assert!(!kind.tasks());
130        assert!(!kind.disk_usage());
131    }
132
133    #[test]
134    fn test_system_monitor_creation() {
135        let monitor = SystemMonitor::new();
136        let stats = monitor.current();
137
138        // Memory should be > 0 for a running process
139        assert!(stats.memory_bytes > 0);
140        // CPU might be 0 on first sample
141        assert!(stats.cpu_usage >= 0.0);
142    }
143
144    #[test]
145    fn test_system_monitor_update() {
146        let mut monitor = SystemMonitor::new();
147
148        // First update
149        let stats1 = monitor.update();
150        assert!(stats1.memory_bytes > 0);
151
152        // Second update - should succeed (memory can fluctuate)
153        let stats2 = monitor.update();
154        assert!(stats2.memory_bytes > 0);
155
156        // CPU should be reasonable
157        assert!(stats2.cpu_usage >= 0.0);
158        assert!(stats2.peak_memory_bytes >= stats1.memory_bytes);
159        assert!(stats2.peak_memory_bytes >= stats2.memory_bytes);
160        assert!(stats2.peak_cpu_usage >= stats2.cpu_usage);
161    }
162
163    #[test]
164    fn test_current_without_update() {
165        let monitor = SystemMonitor::new();
166        let stats1 = monitor.current();
167        let stats2 = monitor.current();
168
169        // Should return same values without update
170        assert_eq!(stats1.memory_bytes, stats2.memory_bytes);
171        assert_eq!(stats1.cpu_usage, stats2.cpu_usage);
172    }
173
174    #[test]
175    fn test_system_stats_default() {
176        let stats = SystemStats::default();
177        assert_eq!(stats.cpu_usage, 0.0);
178        assert_eq!(stats.peak_cpu_usage, 0.0);
179        assert_eq!(stats.memory_bytes, 0);
180        assert_eq!(stats.peak_memory_bytes, 0);
181    }
182}