use core::time::Duration;
use std::time::{Instant, SystemTime};
use crate::model::{
CapabilitySnapshot, CollectorHealth, CpuSnapshot, DiskSnapshot, FilesystemSnapshot,
HostSnapshot, LoadSnapshot, MemorySemantics, MemorySnapshot, MetricState, NetworkSnapshot,
PressureSnapshot, ProcessIdentity, ProcessSnapshot, SensorSnapshot,
};
use crate::units::Percent;
#[derive(Clone, Debug)]
pub struct SystemSnapshot {
pub sequence: u64,
pub captured_at: Instant,
pub wall_time: SystemTime,
pub elapsed: Duration,
pub host: HostSnapshot,
pub cpu: CpuSnapshot,
pub memory: MemorySnapshot,
pub load: MetricState<LoadSnapshot>,
pub processes: Vec<ProcessSnapshot>,
pub disks: Vec<DiskSnapshot>,
pub filesystems: Vec<FilesystemSnapshot>,
pub networks: Vec<NetworkSnapshot>,
pub pressure: PressureSnapshot,
pub sensors: SensorSnapshot,
pub capabilities: CapabilitySnapshot,
pub health: CollectorHealth,
}
impl SystemSnapshot {
#[must_use]
pub fn warming_up(captured_at: Instant, wall_time: SystemTime, logical_cpus: u16) -> Self {
Self {
sequence: 0,
captured_at,
wall_time,
elapsed: Duration::ZERO,
host: HostSnapshot::warming_up(),
cpu: CpuSnapshot::warming_up(logical_cpus),
memory: MemorySnapshot::warming_up(0, MemorySemantics::SysinfoBaseline),
load: MetricState::WarmingUp,
processes: Vec::new(),
disks: Vec::new(),
filesystems: Vec::new(),
networks: Vec::new(),
pressure: PressureSnapshot::warming_up(),
sensors: SensorSnapshot::warming_up(),
capabilities: CapabilitySnapshot::default(),
health: CollectorHealth::default(),
}
}
#[must_use]
pub fn process(&self, identity: ProcessIdentity) -> Option<&ProcessSnapshot> {
self.processes
.iter()
.find(|process| process.identity == identity)
}
#[must_use]
pub fn process_by_pid(&self, pid: u32) -> Option<&ProcessSnapshot> {
self.processes
.iter()
.find(|process| process.identity.pid == pid)
}
#[must_use]
pub fn process_count(&self) -> usize {
self.processes.len()
}
#[must_use]
pub fn total_process_cpu(&self) -> Option<Percent> {
let sum: f32 = self
.processes
.iter()
.filter_map(|process| process.cpu.fresh().map(|cpu| cpu.value()))
.sum();
Percent::new(sum)
}
#[must_use]
pub fn notable_process_count(&self) -> usize {
self.processes
.iter()
.filter(|process| process.state.is_notable())
.count()
}
#[must_use]
pub fn has_valid_interval(&self) -> bool {
!self.elapsed.is_zero()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{ProcessIo, ProcessMemory, ProcessState};
fn process(pid: u32, start_key: u64, cpu: Option<f32>) -> ProcessSnapshot {
ProcessSnapshot {
identity: ProcessIdentity::new(pid, start_key),
parent_pid: Some(1),
name: "test".into(),
command: "test".into(),
exe: None,
user: MetricState::Unsupported,
state: ProcessState::Running,
cpu: cpu
.and_then(Percent::new)
.map_or(MetricState::WarmingUp, MetricState::Available),
memory: ProcessMemory::WARMING_UP,
io: ProcessIo::UNSUPPORTED,
threads: MetricState::Unsupported,
age: MetricState::Unsupported,
started_at: MetricState::Unsupported,
is_kernel_thread: false,
}
}
fn snapshot() -> SystemSnapshot {
SystemSnapshot::warming_up(Instant::now(), SystemTime::UNIX_EPOCH, 8)
}
#[test]
fn the_first_snapshot_measures_nothing_and_has_no_valid_interval() {
let snapshot = snapshot();
assert_eq!(snapshot.sequence, 0);
assert!(!snapshot.has_valid_interval());
assert!(snapshot.cpu.total.is_warming_up());
assert!(snapshot.load.is_warming_up());
assert_eq!(snapshot.process_count(), 0);
assert_eq!(snapshot.cpu.logical_count, 8);
}
#[test]
fn process_lookup_by_identity_rejects_a_reused_pid() {
let mut snapshot = snapshot();
snapshot
.processes
.push(process(31_842, 900_100, Some(287.0)));
let original = ProcessIdentity::new(31_842, 900_100);
let recycled = ProcessIdentity::new(31_842, 977_400);
assert!(snapshot.process(original).is_some());
assert!(
snapshot.process(recycled).is_none(),
"a reused PID must not resolve to the previous process"
);
}
#[test]
fn lookup_by_pid_deliberately_ignores_the_start_key_so_reuse_is_detectable() {
let mut snapshot = snapshot();
snapshot.processes.push(process(31_842, 977_400, None));
let found = snapshot.process_by_pid(31_842).expect("pid is present");
assert_eq!(found.identity.start_key, 977_400);
assert!(
found
.identity
.is_reuse_of(&ProcessIdentity::new(31_842, 900_100))
);
}
#[test]
fn total_process_cpu_sums_only_measured_values() {
let mut snapshot = snapshot();
snapshot.processes.push(process(1, 1, Some(287.0)));
snapshot.processes.push(process(2, 2, Some(54.0)));
snapshot.processes.push(process(3, 3, None));
let total = snapshot.total_process_cpu().expect("two measured values");
assert!((total.value() - 341.0).abs() < 0.01, "got {total}");
}
#[test]
fn total_process_cpu_of_an_empty_table_is_zero_not_undefined() {
let total = snapshot().total_process_cpu().expect("empty sum is zero");
assert!((total.value() - 0.0).abs() < f32::EPSILON);
}
#[test]
fn notable_processes_are_counted_for_the_header() {
let mut snapshot = snapshot();
snapshot.processes.push(process(1, 1, None));
let mut zombie = process(2, 2, None);
zombie.state = ProcessState::Zombie;
snapshot.processes.push(zombie);
let mut blocked = process(3, 3, None);
blocked.state = ProcessState::UninterruptibleSleep;
snapshot.processes.push(blocked);
assert_eq!(snapshot.notable_process_count(), 2);
}
}