1use core::time::Duration;
8use std::time::{Instant, SystemTime};
9
10use crate::model::{
11 CapabilitySnapshot, CollectorHealth, CpuSnapshot, DiskSnapshot, FilesystemSnapshot,
12 HostSnapshot, LoadSnapshot, MemorySemantics, MemorySnapshot, MetricState, NetworkSnapshot,
13 PressureSnapshot, ProcessIdentity, ProcessSnapshot, SensorSnapshot,
14};
15use crate::units::Percent;
16
17#[derive(Clone, Debug)]
19pub struct SystemSnapshot {
20 pub sequence: u64,
25 pub captured_at: Instant,
30 pub wall_time: SystemTime,
32 pub elapsed: Duration,
36 pub host: HostSnapshot,
38 pub cpu: CpuSnapshot,
40 pub memory: MemorySnapshot,
42 pub load: MetricState<LoadSnapshot>,
44 pub processes: Vec<ProcessSnapshot>,
46 pub disks: Vec<DiskSnapshot>,
48 pub filesystems: Vec<FilesystemSnapshot>,
50 pub networks: Vec<NetworkSnapshot>,
52 pub pressure: PressureSnapshot,
54 pub sensors: SensorSnapshot,
56 pub capabilities: CapabilitySnapshot,
58 pub health: CollectorHealth,
60}
61
62impl SystemSnapshot {
63 #[must_use]
69 pub fn warming_up(captured_at: Instant, wall_time: SystemTime, logical_cpus: u16) -> Self {
70 Self {
71 sequence: 0,
72 captured_at,
73 wall_time,
74 elapsed: Duration::ZERO,
75 host: HostSnapshot::warming_up(),
76 cpu: CpuSnapshot::warming_up(logical_cpus),
77 memory: MemorySnapshot::warming_up(0, MemorySemantics::SysinfoBaseline),
78 load: MetricState::WarmingUp,
79 processes: Vec::new(),
80 disks: Vec::new(),
81 filesystems: Vec::new(),
82 networks: Vec::new(),
83 pressure: PressureSnapshot::warming_up(),
84 sensors: SensorSnapshot::warming_up(),
85 capabilities: CapabilitySnapshot::default(),
86 health: CollectorHealth::default(),
87 }
88 }
89
90 #[must_use]
95 pub fn process(&self, identity: ProcessIdentity) -> Option<&ProcessSnapshot> {
96 self.processes
97 .iter()
98 .find(|process| process.identity == identity)
99 }
100
101 #[must_use]
107 pub fn process_by_pid(&self, pid: u32) -> Option<&ProcessSnapshot> {
108 self.processes
109 .iter()
110 .find(|process| process.identity.pid == pid)
111 }
112
113 #[must_use]
115 pub fn process_count(&self) -> usize {
116 self.processes.len()
117 }
118
119 #[must_use]
126 pub fn total_process_cpu(&self) -> Option<Percent> {
127 let sum: f32 = self
128 .processes
129 .iter()
130 .filter_map(|process| process.cpu.fresh().map(|cpu| cpu.value()))
131 .sum();
132 Percent::new(sum)
133 }
134
135 #[must_use]
137 pub fn notable_process_count(&self) -> usize {
138 self.processes
139 .iter()
140 .filter(|process| process.state.is_notable())
141 .count()
142 }
143
144 #[must_use]
148 pub fn has_valid_interval(&self) -> bool {
149 !self.elapsed.is_zero()
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::model::{ProcessIo, ProcessMemory, ProcessState};
157
158 fn process(pid: u32, start_key: u64, cpu: Option<f32>) -> ProcessSnapshot {
159 ProcessSnapshot {
160 identity: ProcessIdentity::new(pid, start_key),
161 parent_pid: Some(1),
162 name: "test".into(),
163 command: "test".into(),
164 exe: None,
165 user: MetricState::Unsupported,
166 state: ProcessState::Running,
167 cpu: cpu
168 .and_then(Percent::new)
169 .map_or(MetricState::WarmingUp, MetricState::Available),
170 memory: ProcessMemory::WARMING_UP,
171 io: ProcessIo::UNSUPPORTED,
172 threads: MetricState::Unsupported,
173 age: MetricState::Unsupported,
174 started_at: MetricState::Unsupported,
175 is_kernel_thread: false,
176 }
177 }
178
179 fn snapshot() -> SystemSnapshot {
180 SystemSnapshot::warming_up(Instant::now(), SystemTime::UNIX_EPOCH, 8)
181 }
182
183 #[test]
184 fn the_first_snapshot_measures_nothing_and_has_no_valid_interval() {
185 let snapshot = snapshot();
186 assert_eq!(snapshot.sequence, 0);
187 assert!(!snapshot.has_valid_interval());
188 assert!(snapshot.cpu.total.is_warming_up());
189 assert!(snapshot.load.is_warming_up());
190 assert_eq!(snapshot.process_count(), 0);
191 assert_eq!(snapshot.cpu.logical_count, 8);
192 }
193
194 #[test]
195 fn process_lookup_by_identity_rejects_a_reused_pid() {
196 let mut snapshot = snapshot();
197 snapshot
198 .processes
199 .push(process(31_842, 900_100, Some(287.0)));
200
201 let original = ProcessIdentity::new(31_842, 900_100);
202 let recycled = ProcessIdentity::new(31_842, 977_400);
203
204 assert!(snapshot.process(original).is_some());
205 assert!(
206 snapshot.process(recycled).is_none(),
207 "a reused PID must not resolve to the previous process"
208 );
209 }
210
211 #[test]
212 fn lookup_by_pid_deliberately_ignores_the_start_key_so_reuse_is_detectable() {
213 let mut snapshot = snapshot();
214 snapshot.processes.push(process(31_842, 977_400, None));
215
216 let found = snapshot.process_by_pid(31_842).expect("pid is present");
217 assert_eq!(found.identity.start_key, 977_400);
218 assert!(
219 found
220 .identity
221 .is_reuse_of(&ProcessIdentity::new(31_842, 900_100))
222 );
223 }
224
225 #[test]
226 fn total_process_cpu_sums_only_measured_values() {
227 let mut snapshot = snapshot();
228 snapshot.processes.push(process(1, 1, Some(287.0)));
229 snapshot.processes.push(process(2, 2, Some(54.0)));
230 snapshot.processes.push(process(3, 3, None));
231
232 let total = snapshot.total_process_cpu().expect("two measured values");
233 assert!((total.value() - 341.0).abs() < 0.01, "got {total}");
234 }
235
236 #[test]
237 fn total_process_cpu_of_an_empty_table_is_zero_not_undefined() {
238 let total = snapshot().total_process_cpu().expect("empty sum is zero");
241 assert!((total.value() - 0.0).abs() < f32::EPSILON);
242 }
243
244 #[test]
245 fn notable_processes_are_counted_for_the_header() {
246 let mut snapshot = snapshot();
247 snapshot.processes.push(process(1, 1, None));
248 let mut zombie = process(2, 2, None);
249 zombie.state = ProcessState::Zombie;
250 snapshot.processes.push(zombie);
251 let mut blocked = process(3, 3, None);
252 blocked.state = ProcessState::UninterruptibleSleep;
253 snapshot.processes.push(blocked);
254
255 assert_eq!(snapshot.notable_process_count(), 2);
256 }
257}