use std::collections::VecDeque;
use std::time::{Duration, Instant};
const MAX_SAMPLES: usize = 60;
pub const SAMPLE_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Copy)]
pub struct ResourceSample {
pub cpu_percent: f32,
pub memory_mb: f64,
pub timestamp: Instant,
}
#[derive(Debug, Clone)]
pub struct ProcessHistory {
pub pid: u32,
pub samples: VecDeque<ResourceSample>,
pub last_sample_time: Option<Instant>,
}
impl ProcessHistory {
pub fn new(pid: u32) -> Self {
Self {
pid,
samples: VecDeque::with_capacity(MAX_SAMPLES),
last_sample_time: None,
}
}
pub fn push(&mut self, cpu_percent: f32, memory_mb: f64) {
let now = Instant::now();
self.samples.push_back(ResourceSample {
cpu_percent,
memory_mb,
timestamp: now,
});
if self.samples.len() > MAX_SAMPLES {
self.samples.pop_front();
}
self.last_sample_time = Some(now);
}
pub fn cpu_values(&self) -> Vec<u64> {
self.samples.iter().map(|s| s.cpu_percent as u64).collect()
}
pub fn memory_values(&self) -> Vec<u64> {
self.samples.iter().map(|s| s.memory_mb as u64).collect()
}
pub fn avg_cpu(&self) -> f32 {
if self.samples.is_empty() {
return 0.0;
}
self.samples.iter().map(|s| s.cpu_percent).sum::<f32>() / self.samples.len() as f32
}
pub fn avg_memory(&self) -> f64 {
if self.samples.is_empty() {
return 0.0;
}
self.samples.iter().map(|s| s.memory_mb).sum::<f64>() / self.samples.len() as f64
}
pub fn peak_cpu(&self) -> f32 {
self.samples.iter().map(|s| s.cpu_percent).fold(0.0f32, f32::max)
}
pub fn peak_memory(&self) -> f64 {
self.samples.iter().map(|s| s.memory_mb).fold(0.0f64, f64::max)
}
pub fn should_sample(&self) -> bool {
match self.last_sample_time {
None => true,
Some(last) => last.elapsed() >= SAMPLE_INTERVAL,
}
}
}
#[derive(Debug, Clone)]
pub struct ResourceTracker {
histories: std::collections::HashMap<u32, ProcessHistory>,
}
impl ResourceTracker {
pub fn new() -> Self {
Self {
histories: std::collections::HashMap::new(),
}
}
pub fn record(&mut self, pid: u32, cpu_percent: f32, memory_mb: f64) {
let history = self.histories.entry(pid).or_insert_with(|| ProcessHistory::new(pid));
history.push(cpu_percent, memory_mb);
}
pub fn get(&self, pid: u32) -> Option<&ProcessHistory> {
self.histories.get(&pid)
}
pub fn get_mut(&mut self, pid: u32) -> Option<&mut ProcessHistory> {
self.histories.get_mut(&pid)
}
pub fn prune(&mut self, active_pids: &std::collections::HashSet<u32>) {
self.histories.retain(|pid, _| active_pids.contains(pid));
}
pub fn record_batch(&mut self, entries: &[(u32, f32, f64)]) {
for &(pid, cpu, mem) in entries {
self.record(pid, cpu, mem);
}
}
pub fn pids(&self) -> Vec<u32> {
self.histories.keys().copied().collect()
}
}
impl Default for ResourceTracker {
fn default() -> Self {
Self::new()
}
}
pub fn sparkline_text(values: &[u64], width: usize) -> String {
if values.is_empty() {
return " ".repeat(width);
}
let blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
let sampled: Vec<u64> = if values.len() <= width {
let mut v = values.to_vec();
while v.len() < width {
v.insert(0, 0);
}
v
} else {
let step = values.len() as f64 / width as f64;
(0..width)
.map(|i| values[(i as f64 * step) as usize])
.collect()
};
let max = sampled.iter().copied().max().unwrap_or(1).max(1);
sampled
.iter()
.map(|&v| {
let idx = ((v as f64 / max as f64) * 7.0).round() as usize;
blocks[idx.min(7)]
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_history() {
let mut history = ProcessHistory::new(1234);
assert!(history.should_sample());
history.push(10.0, 100.0);
history.push(20.0, 150.0);
history.push(15.0, 120.0);
assert_eq!(history.cpu_values(), vec![10, 20, 15]);
assert_eq!(history.memory_values(), vec![100, 150, 120]);
assert!((history.avg_cpu() - 15.0).abs() < 0.1);
assert!((history.avg_memory() - 123.33).abs() < 1.0);
assert_eq!(history.peak_cpu(), 20.0);
assert_eq!(history.peak_memory(), 150.0);
}
#[test]
fn test_resource_tracker() {
let mut tracker = ResourceTracker::new();
tracker.record(1, 10.0, 100.0);
tracker.record(2, 20.0, 200.0);
tracker.record(1, 15.0, 110.0);
let history1 = tracker.get(1).unwrap();
assert_eq!(history1.samples.len(), 2);
let history2 = tracker.get(2).unwrap();
assert_eq!(history2.samples.len(), 1);
}
#[test]
fn test_sparkline_text() {
let values = vec![0, 10, 50, 100];
let spark = sparkline_text(&values, 4);
assert_eq!(spark.chars().count(), 4);
let empty = sparkline_text(&[], 4);
assert_eq!(empty, " ");
}
#[test]
fn test_max_samples_eviction() {
let mut history = ProcessHistory::new(1);
for i in 0..70 {
history.push(i as f32, i as f64);
}
assert_eq!(history.samples.len(), MAX_SAMPLES);
}
#[test]
fn test_prune() {
let mut tracker = ResourceTracker::new();
tracker.record(1, 10.0, 100.0);
tracker.record(2, 20.0, 200.0);
tracker.record(3, 30.0, 300.0);
let mut active = std::collections::HashSet::new();
active.insert(1);
active.insert(3);
tracker.prune(&active);
assert!(tracker.get(1).is_some());
assert!(tracker.get(2).is_none());
assert!(tracker.get(3).is_some());
}
}