use std::collections::HashMap;
use crate::{format, sampler::Snapshot};
const CAPACITY: usize = 120;
#[derive(Default)]
struct Series {
values: Vec<f64>,
}
impl Series {
fn push(&mut self, value: f64) {
if self.values.len() == CAPACITY {
self.values.remove(0);
}
self.values.push(value);
}
fn len(&self) -> usize {
self.values.len()
}
fn recent(&self, width: usize) -> &[f64] {
let start = self.values.len().saturating_sub(width);
&self.values[start..]
}
}
#[derive(Default)]
pub struct History {
cpu: Series,
memory: Series,
process_cpu: HashMap<u32, Series>,
}
impl History {
pub fn record(&mut self, snapshot: &Snapshot) {
self.cpu.push(snapshot.totals.cpu_usage as f64);
self.memory.push(memory_percent(snapshot));
let mut live_pids: Vec<u32> = Vec::with_capacity(snapshot.processes.len());
for process in &snapshot.processes {
live_pids.push(process.pid);
self.process_cpu
.entry(process.pid)
.or_default()
.push(process.cpu_usage as f64);
}
self.process_cpu.retain(|pid, _| live_pids.contains(pid));
}
pub fn cpu_recent(&self, width: usize) -> Vec<f64> {
self.cpu.recent(width).to_vec()
}
pub fn memory_recent(&self, width: usize) -> Vec<f64> {
self.memory.recent(width).to_vec()
}
pub fn process_cpu_sparkline(&self, pid: u32, width: usize) -> Option<String> {
let series = self.process_cpu.get(&pid)?;
if series.len() < 2 {
return None;
}
let values = series.recent(width);
let max = values.iter().copied().fold(1.0, f64::max);
Some(format::sparkline(values, max))
}
}
fn memory_percent(snapshot: &Snapshot) -> f64 {
let totals = &snapshot.totals;
if totals.total_memory > 0 {
totals.used_memory as f64 / totals.total_memory as f64 * 100.0
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use crate::sampler::{ProcessRow, ProcessTrend, Snapshot, SystemTotals};
use super::{CAPACITY, History};
fn snapshot(cpu: f32, used_memory: u64, processes: Vec<ProcessRow>) -> Snapshot {
Snapshot {
totals: SystemTotals {
cpu_usage: cpu,
cpu_count: 8,
total_memory: 100,
used_memory,
total_swap: 0,
used_swap: 0,
disk_read_rate: 0.0,
disk_write_rate: 0.0,
net_in_rate: 0.0,
net_out_rate: 0.0,
uptime: 0,
host: "host".into(),
os: "macOS".into(),
},
process_count: processes.len(),
processes,
disks: Vec::new(),
networks: Vec::new(),
sample_span: Duration::from_millis(1_000),
}
}
fn process(pid: u32, cpu_usage: f32) -> ProcessRow {
ProcessRow {
pid,
pid_str: pid.to_string(),
parent_pid: None,
name: format!("process-{pid}"),
sort_name: format!("process-{pid}"),
user: "user".into(),
command: "command".into(),
exe: "-".into(),
cwd: "-".into(),
status: "running".into(),
cpu_usage,
memory: 0,
virtual_memory: 0,
memory_percent: 0.0,
disk_read_rate: 0.0,
disk_write_rate: 0.0,
total_disk_read: 0,
total_disk_write: 0,
run_time: 0,
start_time: 0,
energy_impact: 0.0,
trend: ProcessTrend::default(),
selected_details: None,
search_text: String::new(),
}
}
#[test]
fn records_system_series_and_exposes_recent_values() {
let mut history = History::default();
history.record(&snapshot(0.0, 0, Vec::new()));
history.record(&snapshot(100.0, 100, Vec::new()));
assert_eq!(history.cpu_recent(8), vec![0.0, 100.0]);
assert_eq!(history.memory_recent(8), vec![0.0, 100.0]);
}
#[test]
fn process_series_needs_two_samples_and_drops_dead_pids() {
let mut history = History::default();
history.record(&snapshot(0.0, 0, vec![process(1, 10.0)]));
assert!(history.process_cpu_sparkline(1, 8).is_none());
history.record(&snapshot(0.0, 0, vec![process(1, 20.0)]));
assert!(history.process_cpu_sparkline(1, 8).is_some());
history.record(&snapshot(0.0, 0, vec![process(2, 5.0)]));
assert!(history.process_cpu_sparkline(1, 8).is_none());
}
#[test]
fn series_length_is_bounded_by_capacity() {
let mut history = History::default();
for _ in 0..(CAPACITY + 50) {
history.record(&snapshot(50.0, 50, vec![process(1, 50.0)]));
}
assert_eq!(history.cpu_recent(CAPACITY + 50).len(), CAPACITY);
}
}