use std::process;
use std::time::Duration;
use sysinfo::{Pid, System};
use tokio::time::interval;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
pub const HIGH_MEMORY_THRESHOLD_MB: u64 = 100;
pub const HIGH_CPU_THRESHOLD_PERCENT: f32 = 80.0;
pub const METRICS_INTERVAL: Duration = Duration::from_secs(60);
#[derive(Debug, Clone, Default)]
pub struct ProcessMetrics {
pub memory_bytes: u64,
pub memory_mb: u64,
pub cpu_percent: f32,
pub memory_high: bool,
pub cpu_high: bool,
}
impl ProcessMetrics {
pub fn is_any_high(&self) -> bool {
self.memory_high || self.cpu_high
}
}
pub struct ProcessMonitor {
system: System,
pid: Pid,
memory_threshold_mb: u64,
cpu_threshold_percent: f32,
}
impl ProcessMonitor {
pub fn new() -> Self {
Self::with_thresholds(HIGH_MEMORY_THRESHOLD_MB, HIGH_CPU_THRESHOLD_PERCENT)
}
pub fn with_thresholds(memory_threshold_mb: u64, cpu_threshold_percent: f32) -> Self {
Self {
system: System::new(),
pid: Pid::from_u32(process::id()),
memory_threshold_mb,
cpu_threshold_percent,
}
}
pub fn refresh(&mut self) -> ProcessMetrics {
self.system.refresh_all();
let (memory_bytes, cpu_percent) = self
.system
.process(self.pid)
.map(|p| (p.memory(), p.cpu_usage()))
.unwrap_or((0, 0.0));
let memory_mb = memory_bytes / 1024 / 1024;
let memory_high = memory_mb > self.memory_threshold_mb;
let cpu_high = cpu_percent > self.cpu_threshold_percent;
ProcessMetrics {
memory_bytes,
memory_mb,
cpu_percent,
memory_high,
cpu_high,
}
}
pub fn memory_threshold_mb(&self) -> u64 {
self.memory_threshold_mb
}
pub fn cpu_threshold_percent(&self) -> f32 {
self.cpu_threshold_percent
}
}
impl Default for ProcessMonitor {
fn default() -> Self {
Self::new()
}
}
pub fn spawn_monitor_task(cancel_token: CancellationToken) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut monitor = ProcessMonitor::new();
let mut tick = interval(METRICS_INTERVAL);
let _ = monitor.refresh();
info!(
memory_threshold_mb = monitor.memory_threshold_mb(),
cpu_threshold_percent = monitor.cpu_threshold_percent(),
interval_secs = METRICS_INTERVAL.as_secs(),
"Process monitor started"
);
loop {
tokio::select! {
biased;
_ = cancel_token.cancelled() => {
info!("Process monitor shutting down");
break;
}
_ = tick.tick() => {
let metrics = monitor.refresh();
log_metrics(&metrics, &monitor);
}
}
}
debug!("Process monitor task completed");
})
}
fn log_metrics(metrics: &ProcessMetrics, monitor: &ProcessMonitor) {
if metrics.memory_high {
warn!(
memory_mb = metrics.memory_mb,
threshold_mb = monitor.memory_threshold_mb(),
cpu_percent = format!("{:.1}", metrics.cpu_percent),
"HIGH MEMORY: Daemon memory usage above threshold"
);
} else if metrics.cpu_high {
warn!(
memory_mb = metrics.memory_mb,
cpu_percent = format!("{:.1}", metrics.cpu_percent),
threshold_percent = monitor.cpu_threshold_percent(),
"HIGH CPU: Daemon CPU usage above threshold"
);
} else {
info!(
memory_mb = metrics.memory_mb,
cpu_percent = format!("{:.1}", metrics.cpu_percent),
"Daemon resource usage"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_metrics_default() {
let metrics = ProcessMetrics::default();
assert_eq!(metrics.memory_bytes, 0);
assert_eq!(metrics.memory_mb, 0);
assert_eq!(metrics.cpu_percent, 0.0);
assert!(!metrics.memory_high);
assert!(!metrics.cpu_high);
assert!(!metrics.is_any_high());
}
#[test]
fn test_process_metrics_high_memory() {
let metrics = ProcessMetrics {
memory_bytes: 200 * 1024 * 1024,
memory_mb: 200,
cpu_percent: 10.0,
memory_high: true,
cpu_high: false,
};
assert!(metrics.is_any_high());
}
#[test]
fn test_process_metrics_high_cpu() {
let metrics = ProcessMetrics {
memory_bytes: 50 * 1024 * 1024,
memory_mb: 50,
cpu_percent: 95.0,
memory_high: false,
cpu_high: true,
};
assert!(metrics.is_any_high());
}
#[test]
fn test_monitor_creation() {
let monitor = ProcessMonitor::new();
assert_eq!(monitor.memory_threshold_mb(), HIGH_MEMORY_THRESHOLD_MB);
assert_eq!(monitor.cpu_threshold_percent(), HIGH_CPU_THRESHOLD_PERCENT);
}
#[test]
fn test_monitor_custom_thresholds() {
let monitor = ProcessMonitor::with_thresholds(50, 50.0);
assert_eq!(monitor.memory_threshold_mb(), 50);
assert_eq!(monitor.cpu_threshold_percent(), 50.0);
}
#[test]
fn test_monitor_refresh_returns_metrics() {
let mut monitor = ProcessMonitor::new();
let metrics = monitor.refresh();
assert!(metrics.memory_bytes > 0);
assert!(metrics.memory_mb > 0 || metrics.memory_bytes < 1024 * 1024);
assert!(metrics.cpu_percent >= 0.0);
}
#[test]
fn test_monitor_cpu_measurement() {
use std::time::Duration;
let mut monitor = ProcessMonitor::new();
let _ = monitor.refresh();
let start = std::time::Instant::now();
let mut sum: u64 = 0;
while start.elapsed() < Duration::from_millis(500) {
for i in 0..100000 {
sum = sum.wrapping_add(i);
}
}
std::hint::black_box(sum);
let metrics = monitor.refresh();
assert!(
metrics.cpu_percent > 50.0,
"Expected CPU > 50%, got {:.2}%",
metrics.cpu_percent
);
assert!(
metrics.cpu_percent < 200.0,
"CPU seems too high: {:.2}%",
metrics.cpu_percent
);
}
#[test]
fn test_constants() {
assert_eq!(HIGH_MEMORY_THRESHOLD_MB, 100);
assert_eq!(HIGH_CPU_THRESHOLD_PERCENT, 80.0);
assert_eq!(METRICS_INTERVAL, Duration::from_secs(60));
}
}