nntp-proxy 0.5.1

NNTP proxy server with per-command backend multiplexing, caching, metrics, and TUI dashboard
Documentation
//! System resource monitoring for TUI display
//!
//! Tracks CPU usage and memory consumption for the proxy process.

use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};

/// System resource statistics for the current process
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct SystemStats {
    /// CPU usage percentage (0.0 - 100.0 per core, can exceed 100.0 on multi-core)
    pub cpu_usage: f32,
    /// Peak CPU usage percentage
    pub peak_cpu_usage: f32,
    /// Memory usage in bytes
    pub memory_bytes: u64,
    /// Peak memory usage in bytes
    pub peak_memory_bytes: u64,
}

/// System resource monitor
///
/// Efficiently tracks process-specific metrics using sysinfo.
/// Call `update()` periodically (e.g., every TUI frame) to refresh stats.
pub struct SystemMonitor {
    system: System,
    pid: sysinfo::Pid,
    peak_cpu: f32,
    peak_memory: u64,
}

impl SystemMonitor {
    fn refresh_kind() -> ProcessRefreshKind {
        ProcessRefreshKind::nothing()
            .with_cpu()
            .with_memory()
            .without_tasks()
    }

    /// Create a new system monitor for the current process
    ///
    /// # Panics
    /// Panics if `sysinfo` cannot determine the current process ID.
    #[must_use]
    pub fn new() -> Self {
        let mut system =
            System::new_with_specifics(RefreshKind::nothing().with_processes(Self::refresh_kind()));
        let pid = sysinfo::get_current_pid().expect("Failed to get current PID");
        system.refresh_processes_specifics(
            ProcessesToUpdate::Some(&[pid]),
            true,
            Self::refresh_kind(),
        );

        Self {
            system,
            pid,
            peak_cpu: 0.0,
            peak_memory: 0,
        }
    }

    /// Update and retrieve current system stats
    ///
    /// Should be called at regular intervals (e.g., TUI update rate of ~4Hz).
    /// First call may return zero for CPU usage - sysinfo needs 2+ samples.
    #[must_use]
    pub fn update(&mut self) -> SystemStats {
        self.system.refresh_processes_specifics(
            ProcessesToUpdate::Some(&[self.pid]),
            true,
            Self::refresh_kind(),
        );

        if let Some(process) = self.system.process(self.pid) {
            let cpu = process.cpu_usage();
            let memory = process.memory();

            if cpu > self.peak_cpu {
                self.peak_cpu = cpu;
            }
            if memory > self.peak_memory {
                self.peak_memory = memory;
            }

            SystemStats {
                cpu_usage: cpu,
                peak_cpu_usage: self.peak_cpu,
                memory_bytes: memory,
                peak_memory_bytes: self.peak_memory,
            }
        } else {
            SystemStats::default()
        }
    }

    /// Get current stats without refreshing
    ///
    /// Returns the last refreshed stats. Useful if you just called `update()`.
    #[must_use]
    pub fn current(&self) -> SystemStats {
        self.system
            .process(self.pid)
            .map_or_else(SystemStats::default, |process| SystemStats {
                cpu_usage: process.cpu_usage(),
                peak_cpu_usage: self.peak_cpu,
                memory_bytes: process.memory(),
                peak_memory_bytes: self.peak_memory,
            })
    }
}

impl Default for SystemMonitor {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
#[allow(clippy::float_cmp)] // These tests assert exact default/current CPU values from deterministic fixtures.
mod tests {
    use super::*;

    #[test]
    fn test_refresh_kind_only_enables_needed_process_fields() {
        let kind = SystemMonitor::refresh_kind();

        assert!(kind.cpu());
        assert!(kind.memory());
        assert!(!kind.tasks());
        assert!(!kind.disk_usage());
    }

    #[test]
    fn test_system_monitor_creation() {
        let monitor = SystemMonitor::new();
        let stats = monitor.current();

        // Memory should be > 0 for a running process
        assert!(stats.memory_bytes > 0);
        // CPU might be 0 on first sample
        assert!(stats.cpu_usage >= 0.0);
    }

    #[test]
    fn test_system_monitor_update() {
        let mut monitor = SystemMonitor::new();

        // First update
        let stats1 = monitor.update();
        assert!(stats1.memory_bytes > 0);

        // Second update - should succeed (memory can fluctuate)
        let stats2 = monitor.update();
        assert!(stats2.memory_bytes > 0);

        // CPU should be reasonable
        assert!(stats2.cpu_usage >= 0.0);
        assert!(stats2.peak_memory_bytes >= stats1.memory_bytes);
        assert!(stats2.peak_memory_bytes >= stats2.memory_bytes);
        assert!(stats2.peak_cpu_usage >= stats2.cpu_usage);
    }

    #[test]
    fn test_current_without_update() {
        let monitor = SystemMonitor::new();
        let stats1 = monitor.current();
        let stats2 = monitor.current();

        // Should return same values without update
        assert_eq!(stats1.memory_bytes, stats2.memory_bytes);
        assert_eq!(stats1.cpu_usage, stats2.cpu_usage);
    }

    #[test]
    fn test_system_stats_default() {
        let stats = SystemStats::default();
        assert_eq!(stats.cpu_usage, 0.0);
        assert_eq!(stats.peak_cpu_usage, 0.0);
        assert_eq!(stats.memory_bytes, 0);
        assert_eq!(stats.peak_memory_bytes, 0);
    }
}