rigtop_core/snapshot.rs
1//! Serializable snapshot of everything rigtop displays.
2//!
3//! A [`Snapshot`] is the single unit of data exchanged between a data source
4//! (local collectors, or a remote `rigtop --agent` over SSH) and a front-end
5//! (TUI or GUI). Every type here derives `Serialize`/`Deserialize` so that the
6//! same structure can be streamed over the wire unchanged.
7//!
8//! Conventions:
9//! - Sizes are in bytes, rates in bytes per second.
10//! - Temperatures are in degrees Celsius, power in watts, fan speed in RPM.
11//! - Percentages are in the range `0.0..=100.0` (per-core values are not summed).
12//! - Values that a platform or device cannot provide are `None` (or an empty
13//! list) rather than zero, so that front-ends can hide them.
14//! - Every struct is `#[serde(default)]`: fields missing from the input take
15//! their default, so a client can read snapshots from an older or newer
16//! `rigtop --agent` as long as fields are only added.
17
18use serde::{Deserialize, Serialize};
19
20/// One full measurement of a host.
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22#[serde(default)]
23pub struct Snapshot {
24 /// Wall-clock time of the measurement, in milliseconds since the Unix epoch.
25 pub timestamp_ms: u64,
26 /// Static-ish information about the host.
27 pub host: HostInfo,
28 /// CPU load, frequency and temperature.
29 pub cpu: CpuStats,
30 /// Physical memory and swap.
31 pub memory: MemoryStats,
32 /// Discrete and integrated GPUs that could be queried.
33 pub gpus: Vec<GpuStats>,
34 /// Raw hardware sensor chips (temperatures and fans), grouped by chip.
35 pub sensors: Vec<SensorChip>,
36 /// Power draw summary.
37 pub power: PowerStats,
38 /// I/O activity per physical block device.
39 pub disks: Vec<DiskIo>,
40 /// Capacity of mounted filesystems.
41 pub filesystems: Vec<FilesystemUsage>,
42 /// Software RAID state (btrfs and mdadm).
43 pub raid: RaidStats,
44 /// SMART health per drive, from files written by a privileged timer.
45 pub smart: Vec<SmartDevice>,
46 /// Network interface traffic.
47 pub networks: Vec<NetworkStats>,
48 /// Process list. May be truncated by the source (see [`Snapshot::truncate_processes`]).
49 pub processes: Vec<ProcessInfo>,
50 /// Total number of processes on the host, even if `processes` was truncated.
51 pub process_count: usize,
52 /// Windows: why LibreHardwareMonitor, the source of temperatures, fans
53 /// and CPU power, could not be read. `None` when it was read, when it is
54 /// disabled in the configuration, and on other platforms.
55 pub lhm_error: Option<String>,
56}
57
58impl Snapshot {
59 /// Keeps only the `limit` processes with the highest CPU usage.
60 ///
61 /// Used to reduce the payload when a host is shown in an overview rather
62 /// than in detail. `process_count` keeps the original total.
63 pub fn truncate_processes(&mut self, limit: usize) {
64 if self.processes.len() <= limit {
65 return;
66 }
67 // Partial sort is enough: only the top `limit` entries matter.
68 self.processes
69 .select_nth_unstable_by(limit, |a, b| b.cpu_percent.total_cmp(&a.cpu_percent));
70 self.processes.truncate(limit);
71 self.processes
72 .sort_unstable_by(|a, b| b.cpu_percent.total_cmp(&a.cpu_percent));
73 }
74}
75
76/// Host identification and uptime.
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78#[serde(default)]
79pub struct HostInfo {
80 /// Host name as reported by the OS.
81 pub hostname: String,
82 /// Human readable OS name and version (e.g. "Linux 26.04 Ubuntu").
83 pub os: String,
84 /// Kernel version string.
85 pub kernel: String,
86 /// Seconds since boot.
87 pub uptime_secs: u64,
88}
89
90/// CPU-wide and per-core statistics.
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92#[serde(default)]
93pub struct CpuStats {
94 /// Brand string of the first CPU (e.g. "AMD Ryzen 9 9950X3D 16-Core Processor").
95 pub model: String,
96 /// Usage across all logical cores.
97 pub total_percent: f32,
98 /// Usage of each logical core, in OS order.
99 pub per_core_percent: Vec<f32>,
100 /// Current frequency of each logical core in MHz.
101 pub per_core_mhz: Vec<u64>,
102 /// 1, 5 and 15 minute load averages; `None` on platforms without them
103 /// (Windows).
104 pub load_average: Option<[f64; 3]>,
105 /// CPU temperature readings, e.g. `Tctl`, `Tccd1`, `Tccd2` on AMD or
106 /// `Package id 0`, `Core 0`.. on Intel.
107 pub temperatures: Vec<Temperature>,
108}
109
110/// Physical memory and swap usage.
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112#[serde(default)]
113pub struct MemoryStats {
114 /// Installed (visible) memory.
115 pub total_bytes: u64,
116 /// Memory in use by processes (total minus available).
117 pub used_bytes: u64,
118 /// Memory that can be allocated without swapping.
119 pub available_bytes: u64,
120 /// Completely unused memory.
121 pub free_bytes: u64,
122 /// Page cache and reclaimable slab, where the platform reports it.
123 pub cached_bytes: Option<u64>,
124 /// Total swap space.
125 pub swap_total_bytes: u64,
126 /// Swap space in use.
127 pub swap_used_bytes: u64,
128}
129
130/// A single GPU.
131#[derive(Debug, Clone, Default, Serialize, Deserialize)]
132#[serde(default)]
133pub struct GpuStats {
134 /// Index within its vendor API (NVML index for NVIDIA).
135 pub index: u32,
136 /// Vendor name ("NVIDIA", "AMD", ...).
137 pub vendor: String,
138 /// Marketing name of the device.
139 pub name: String,
140 /// Core utilization.
141 pub utilization_percent: Option<f32>,
142 /// Memory controller utilization.
143 pub memory_utilization_percent: Option<f32>,
144 /// VRAM in use.
145 pub memory_used_bytes: Option<u64>,
146 /// Total VRAM.
147 pub memory_total_bytes: Option<u64>,
148 /// Core temperature.
149 pub temperature_c: Option<f32>,
150 /// Current board power draw.
151 pub power_w: Option<f32>,
152 /// Enforced power limit.
153 pub power_limit_w: Option<f32>,
154 /// Fan speed as a percentage of the maximum (first fan).
155 pub fan_percent: Option<f32>,
156 /// Graphics clock in MHz.
157 pub clock_mhz: Option<u32>,
158 /// Processes currently using this GPU.
159 pub processes: Vec<GpuProcess>,
160}
161
162/// GPU usage attributed to one process.
163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164#[serde(default)]
165pub struct GpuProcess {
166 /// OS process id.
167 pub pid: u32,
168 /// VRAM used by the process, if the driver reports it.
169 pub memory_bytes: Option<u64>,
170 /// Streaming multiprocessor (core) utilization by the process.
171 pub sm_percent: Option<f32>,
172}
173
174/// A hardware monitoring chip (a Linux hwmon device) and its readings.
175#[derive(Debug, Clone, Default, Serialize, Deserialize)]
176#[serde(default)]
177pub struct SensorChip {
178 /// Stable identifier, `"<driver name>:<device name>"`
179 /// (e.g. `"nct6686:nct6683.2592"`, `"nvme:nvme0"`).
180 ///
181 /// Unlike `hwmonN`, this survives reboots, so it is used as the key for
182 /// user-defined labels.
183 pub key: String,
184 /// Driver-provided chip name (e.g. `"k10temp"`, `"nvme"`, `"spd5118"`).
185 pub name: String,
186 /// Temperature channels.
187 pub temperatures: Vec<Temperature>,
188 /// Fan channels.
189 pub fans: Vec<Fan>,
190}
191
192/// One temperature channel.
193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
194#[serde(default)]
195pub struct Temperature {
196 /// Stable key `"<chip key>/temp<N>"` used to look up user-defined names.
197 /// Empty for readings that do not come from a hwmon chip.
198 pub key: String,
199 /// Driver label (e.g. `"Tccd1"`), or a generic `"temp<N>"` when none exists.
200 pub label: String,
201 /// Current reading.
202 pub celsius: f32,
203 /// High threshold, if exposed.
204 pub max_celsius: Option<f32>,
205 /// Critical threshold, if exposed.
206 pub critical_celsius: Option<f32>,
207}
208
209/// One fan channel.
210#[derive(Debug, Clone, Default, Serialize, Deserialize)]
211#[serde(default)]
212pub struct Fan {
213 /// Stable key `"<chip key>/fan<N>"` used to look up user-defined names.
214 pub key: String,
215 /// Driver label, or a generic `"fan<N>"` when none exists.
216 pub label: String,
217 /// Current speed (0 when stopped or not connected).
218 pub rpm: u32,
219}
220
221/// Power draw summary.
222///
223/// There is no portable way to measure whole-system power without external
224/// hardware, so the total is the sum of the CPU package and discrete GPUs,
225/// which dominate consumption on typical workstations.
226#[derive(Debug, Clone, Default, Serialize, Deserialize)]
227#[serde(default)]
228pub struct PowerStats {
229 /// CPU package power (RAPL on Linux). `None` before the second sample or
230 /// when RAPL is unavailable.
231 pub cpu_package_w: Option<f32>,
232 /// Sum of power reported by discrete GPUs.
233 pub gpus_w: Option<f32>,
234 /// Power an AMD APU's graphics driver reports (amdgpu `PPT`). Per the
235 /// kernel documentation it covers the whole SoC, CPU cores included, so
236 /// it overlaps the CPU package and only stands in for it when RAPL cannot
237 /// be read (see [`PowerStats::cpu_or_soc_w`]).
238 pub igpu_w: Option<f32>,
239 /// `cpu_package_w` (or `igpu_w` without it) `+ gpus_w`, counting only the
240 /// parts that are available.
241 pub total_w: Option<f32>,
242 /// Laptop battery, if there is one.
243 pub battery: Option<BatteryStats>,
244}
245
246/// Below this, an APU reading is not whole-SoC power: desktop Ryzen CPUs
247/// with a small iGPU report only a few milliwatts there, while a laptop
248/// APU's SoC never idles under a watt.
249const MIN_SOC_W: f32 = 1.0;
250
251impl PowerStats {
252 /// The CPU figure to show: `("CPU", RAPL)`, else `("SoC", APU PPT)`.
253 pub fn cpu_or_soc_w(&self) -> (&'static str, Option<f32>) {
254 match (self.cpu_package_w, self.igpu_w) {
255 (Some(w), _) => ("CPU", Some(w)),
256 (None, Some(w)) if w >= MIN_SOC_W => ("SoC", Some(w)),
257 _ => ("CPU", None),
258 }
259 }
260}
261
262/// Charge and power of the system's batteries (summed when there are
263/// several), from `/sys/class/power_supply` on Linux.
264#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
265#[serde(default)]
266pub struct BatteryStats {
267 /// Charge in percent.
268 pub percent: Option<f32>,
269 /// Kernel status: `Charging`, `Discharging`, `Full`, `Not charging`...
270 pub status: String,
271 /// Charge or discharge power (not signed; see `status`).
272 pub power_w: Option<f32>,
273}
274
275impl BatteryStats {
276 /// Short form for status lines: `72% -12W` while discharging, `+20W`
277 /// while charging.
278 pub fn summary(&self) -> String {
279 let percent = self
280 .percent
281 .map_or("--%".to_owned(), |p| format!("{p:.0}%"));
282 let sign = match self.status.as_str() {
283 "Discharging" => "-",
284 "Charging" => "+",
285 _ => return percent,
286 };
287 match self.power_w {
288 Some(w) if w >= 0.5 => format!("{percent} {sign}{w:.0}W"),
289 _ => percent,
290 }
291 }
292}
293
294/// I/O activity of a block device.
295#[derive(Debug, Clone, Default, Serialize, Deserialize)]
296#[serde(default)]
297pub struct DiskIo {
298 /// Kernel device name (e.g. `"nvme0n1"`, `"sda"`).
299 pub name: String,
300 /// Read throughput.
301 pub read_bytes_per_sec: f64,
302 /// Write throughput.
303 pub write_bytes_per_sec: f64,
304 /// Fraction of time the device had I/O in flight, where the platform
305 /// reports it (Linux).
306 pub busy_percent: Option<f32>,
307 /// Drive temperature (NVMe composite sensor), if available.
308 pub temperature_c: Option<f32>,
309}
310
311/// Capacity of one filesystem.
312///
313/// Filesystems mounted at several places (e.g. btrfs subvolumes) appear once,
314/// with all mount points listed.
315#[derive(Debug, Clone, Default, Serialize, Deserialize)]
316#[serde(default)]
317pub struct FilesystemUsage {
318 /// Source device (e.g. `"/dev/nvme1n1p1"`).
319 pub device: String,
320 /// Filesystem type (e.g. `"btrfs"`, `"ext4"`).
321 pub fs_type: String,
322 /// All mount points of this filesystem, shortest first.
323 pub mount_points: Vec<String>,
324 /// Total size.
325 pub total_bytes: u64,
326 /// Space available to unprivileged users.
327 pub available_bytes: u64,
328}
329
330/// Returns true if `device` (a disk such as `"sda"`, or an NVMe controller
331/// such as `"nvme0"`) is the drive behind the block device `disk`
332/// (`"sda"`, `"nvme0n1"`).
333///
334/// `"nvme1"` does not match `"nvme10n1"`: after the controller name only the
335/// namespace suffix (`n<N>`) may follow.
336pub fn device_matches_disk(device: &str, disk: &str) -> bool {
337 match disk.strip_prefix(device) {
338 Some("") => true,
339 Some(rest) => device.starts_with("nvme") && rest.starts_with('n'),
340 None => false,
341 }
342}
343
344/// SMART health of one drive, parsed from `smartctl -j -a` output.
345///
346/// Fields a drive or protocol does not report are `None` (NVMe drives have
347/// no sector reallocation counters, ATA drives no "percentage used").
348#[derive(Debug, Clone, Default, Serialize, Deserialize)]
349#[serde(default)]
350pub struct SmartDevice {
351 /// Kernel device name (`"nvme0"`, `"sda"`). See [`device_matches_disk`].
352 pub device: String,
353 /// Protocol reported by smartctl (`"NVMe"`, `"ATA"`, `"SCSI"`).
354 pub protocol: String,
355 /// Drive model.
356 pub model: String,
357 /// Overall health self-assessment; `None` if smartctl could not tell.
358 pub passed: Option<bool>,
359 /// Current drive temperature.
360 pub temperature_c: Option<f32>,
361 /// Accumulated power-on time.
362 pub power_on_hours: Option<u64>,
363 /// NVMe wear estimate (may exceed 100).
364 pub percentage_used: Option<u32>,
365 /// NVMe spare capacity left.
366 pub available_spare_percent: Option<u32>,
367 /// NVMe spare threshold below which the drive reports a warning.
368 pub available_spare_threshold_percent: Option<u32>,
369 /// NVMe critical warning bit field (0 when healthy).
370 pub critical_warning: Option<u32>,
371 /// NVMe unrecovered data integrity errors.
372 pub media_errors: Option<u64>,
373 /// ATA attribute 5 (Reallocated_Sector_Ct), raw value.
374 pub reallocated_sectors: Option<u64>,
375 /// ATA attribute 197 (Current_Pending_Sector), raw value.
376 pub pending_sectors: Option<u64>,
377 /// ATA attribute 198 (Offline_Uncorrectable), raw value.
378 pub offline_uncorrectable: Option<u64>,
379 /// Total host writes, where the drive reports them.
380 pub data_written_bytes: Option<u64>,
381 /// When smartctl ran, in milliseconds since the Unix epoch. Lets front-ends
382 /// flag stale results (e.g. the timer stopped running).
383 pub collected_at_ms: Option<u64>,
384 /// Error reported by smartctl (e.g. the device could not be opened).
385 pub error: Option<String>,
386}
387
388impl SmartDevice {
389 /// Returns true if anything in the SMART data calls for attention.
390 pub fn needs_attention(&self) -> bool {
391 let positive = |v: Option<u64>| v.is_some_and(|v| v > 0);
392 let spare_low = match (
393 self.available_spare_percent,
394 self.available_spare_threshold_percent,
395 ) {
396 (Some(spare), Some(threshold)) => spare < threshold,
397 _ => false,
398 };
399 self.passed == Some(false)
400 || self.error.is_some()
401 || self.critical_warning.is_some_and(|w| w != 0)
402 || positive(self.media_errors)
403 || positive(self.reallocated_sectors)
404 || positive(self.pending_sectors)
405 || positive(self.offline_uncorrectable)
406 || self.percentage_used.is_some_and(|p| p >= 100)
407 || spare_low
408 }
409}
410
411/// Software RAID state.
412#[derive(Debug, Clone, Default, Serialize, Deserialize)]
413#[serde(default)]
414pub struct RaidStats {
415 /// Mounted btrfs filesystems.
416 pub btrfs: Vec<BtrfsFilesystem>,
417 /// Linux md (mdadm) arrays.
418 pub md: Vec<MdArray>,
419}
420
421/// A btrfs filesystem as seen in `/sys/fs/btrfs/<fsid>`.
422#[derive(Debug, Clone, Default, Serialize, Deserialize)]
423#[serde(default)]
424pub struct BtrfsFilesystem {
425 /// Filesystem UUID.
426 pub fsid: String,
427 /// Filesystem label (may be empty).
428 pub label: String,
429 /// Mount points found in the mount table for this filesystem.
430 pub mount_points: Vec<String>,
431 /// Member block devices (e.g. `"nvme0n1"`, `"nvme1n1p1"`).
432 pub devices: Vec<String>,
433 /// Per-device error counters, keyed by btrfs device id.
434 ///
435 /// The kernel does not expose the devid-to-name mapping in sysfs, so this
436 /// list is not aligned with `devices`.
437 pub device_stats: Vec<BtrfsDeviceStats>,
438 /// Allocation per block group type (data, metadata, system).
439 pub allocations: Vec<BtrfsAllocation>,
440 /// Result of the most recent scrub, if it is known. Error counters in
441 /// `device_stats` only grow when data is read, so the scrub time tells
442 /// when all data was last verified.
443 pub scrub: Option<BtrfsScrub>,
444}
445
446/// Outcome of the last btrfs scrub (summed over devices).
447#[derive(Debug, Clone, Default, Serialize, Deserialize)]
448#[serde(default)]
449pub struct BtrfsScrub {
450 /// Start time, in milliseconds since the Unix epoch.
451 pub started_at_ms: u64,
452 /// Run time in seconds.
453 pub duration_secs: u64,
454 /// `"finished"`, `"canceled"` or `"unfinished"` (running or interrupted).
455 pub state: String,
456 /// Bytes verified.
457 pub bytes_scrubbed: u64,
458 /// Read, checksum, verify and superblock errors found.
459 pub errors: u64,
460 /// Errors that were repaired from a good copy.
461 pub corrected: u64,
462 /// Errors that could not be repaired.
463 pub uncorrectable: u64,
464}
465
466impl BtrfsFilesystem {
467 /// Returns true if any member is missing or has a non-zero error counter.
468 pub fn has_problems(&self) -> bool {
469 self.device_stats
470 .iter()
471 .any(|d| d.missing || d.total_errors() > 0)
472 }
473}
474
475/// Error counters of one btrfs member device.
476#[derive(Debug, Clone, Default, Serialize, Deserialize)]
477#[serde(default)]
478pub struct BtrfsDeviceStats {
479 /// btrfs device id (1-based).
480 pub devid: u64,
481 /// Whether the device is missing from the filesystem.
482 pub missing: bool,
483 /// Write I/O errors.
484 pub write_errs: u64,
485 /// Read I/O errors.
486 pub read_errs: u64,
487 /// Flush errors.
488 pub flush_errs: u64,
489 /// Checksum mismatches.
490 pub corruption_errs: u64,
491 /// Generation mismatches (stale data).
492 pub generation_errs: u64,
493}
494
495impl BtrfsDeviceStats {
496 /// Sum of all error counters.
497 pub fn total_errors(&self) -> u64 {
498 self.write_errs
499 + self.read_errs
500 + self.flush_errs
501 + self.corruption_errs
502 + self.generation_errs
503 }
504}
505
506/// Space allocation of one btrfs block group type and RAID profile.
507#[derive(Debug, Clone, Default, Serialize, Deserialize)]
508#[serde(default)]
509pub struct BtrfsAllocation {
510 /// Block group type: `"data"`, `"metadata"` or `"system"`.
511 pub kind: String,
512 /// RAID profile (e.g. `"single"`, `"raid0"`, `"raid1c3"`).
513 pub profile: String,
514 /// Space allocated to this profile (logical size).
515 pub total_bytes: u64,
516 /// Space used within the allocation (logical size).
517 pub used_bytes: u64,
518}
519
520/// One Linux md array parsed from `/proc/mdstat`.
521#[derive(Debug, Clone, Default, Serialize, Deserialize)]
522#[serde(default)]
523pub struct MdArray {
524 /// Array name (e.g. `"md0"`).
525 pub name: String,
526 /// Whether the array is active.
527 pub active: bool,
528 /// Whether the array is read-only (`active (read-only)` / `(auto-read-only)`).
529 pub read_only: bool,
530 /// RAID level (e.g. `"raid1"`); empty for inactive arrays.
531 pub level: String,
532 /// Member devices.
533 pub members: Vec<MdMember>,
534 /// Number of devices the array should have (`[n/m]` -> n).
535 pub expected_devices: Option<u32>,
536 /// Number of devices currently in sync (`[n/m]` -> m).
537 pub working_devices: Option<u32>,
538 /// Ongoing resync/recovery/reshape/check, if any.
539 pub sync: Option<MdSync>,
540}
541
542impl MdArray {
543 /// Returns true if the array runs with fewer devices than expected or has a failed member.
544 pub fn is_degraded(&self) -> bool {
545 let short = match (self.expected_devices, self.working_devices) {
546 (Some(expected), Some(working)) => working < expected,
547 _ => false,
548 };
549 short || self.members.iter().any(|m| m.faulty)
550 }
551}
552
553/// A member device of an md array.
554#[derive(Debug, Clone, Default, Serialize, Deserialize)]
555#[serde(default)]
556pub struct MdMember {
557 /// Device name (e.g. `"sda1"`).
558 pub name: String,
559 /// Role number within the array.
560 pub role: u32,
561 /// Marked faulty `(F)`.
562 pub faulty: bool,
563 /// Spare device `(S)`.
564 pub spare: bool,
565}
566
567/// Progress of an md background operation.
568#[derive(Debug, Clone, Default, Serialize, Deserialize)]
569#[serde(default)]
570pub struct MdSync {
571 /// Operation name: `"resync"`, `"recovery"`, `"reshape"` or `"check"`.
572 pub action: String,
573 /// Completion percentage.
574 pub percent: f32,
575 /// Estimated minutes until completion.
576 pub finish_minutes: Option<f32>,
577 /// Current speed in KiB/s.
578 pub speed_kib_per_sec: Option<u64>,
579}
580
581/// Traffic of one network interface.
582#[derive(Debug, Clone, Default, Serialize, Deserialize)]
583#[serde(default)]
584pub struct NetworkStats {
585 /// Interface name (e.g. `"enp9s0"`).
586 pub name: String,
587 /// Receive throughput.
588 pub rx_bytes_per_sec: f64,
589 /// Transmit throughput.
590 pub tx_bytes_per_sec: f64,
591 /// Bytes received since the interface came up.
592 pub rx_total_bytes: u64,
593 /// Bytes transmitted since the interface came up.
594 pub tx_total_bytes: u64,
595 /// Negotiated link speed in Mbit/s (wired links), if known.
596 pub link_speed_mbps: Option<u32>,
597 /// Whether the link is up, if the OS reports it.
598 pub link_up: Option<bool>,
599 /// Wi-Fi link details, for wireless interfaces.
600 pub wireless: Option<WirelessLink>,
601}
602
603/// State of a Wi-Fi link.
604#[derive(Debug, Clone, Default, Serialize, Deserialize)]
605#[serde(default)]
606pub struct WirelessLink {
607 /// Current receive bit rate in Mbit/s.
608 pub rx_bitrate_mbps: Option<f32>,
609 /// Current transmit bit rate in Mbit/s.
610 pub tx_bitrate_mbps: Option<f32>,
611 /// Signal strength in dBm.
612 pub signal_dbm: Option<i32>,
613}
614
615/// One process.
616#[derive(Debug, Clone, Default, Serialize, Deserialize)]
617#[serde(default)]
618pub struct ProcessInfo {
619 /// Process id.
620 pub pid: u32,
621 /// Parent process id.
622 pub parent_pid: Option<u32>,
623 /// Short name (executable name).
624 pub name: String,
625 /// Full command line joined by spaces.
626 pub command: String,
627 /// Owner user name (or numeric uid when it cannot be resolved).
628 pub user: String,
629 /// Scheduling state (e.g. "Run", "Sleep", "Zombie").
630 pub state: String,
631 /// CPU usage; 100 means one full core, so multi-threaded processes may exceed 100.
632 pub cpu_percent: f32,
633 /// Resident memory.
634 pub memory_bytes: u64,
635 /// Number of threads, where known.
636 pub threads: Option<u32>,
637 /// Disk read throughput.
638 pub read_bytes_per_sec: f64,
639 /// Disk write throughput.
640 pub write_bytes_per_sec: f64,
641 /// Seconds since the process started.
642 pub run_time_secs: u64,
643 /// Kernel thread (e.g. `kworker`), which front-ends may hide.
644 pub kernel_thread: bool,
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650
651 fn process(pid: u32, cpu: f32) -> ProcessInfo {
652 ProcessInfo {
653 pid,
654 cpu_percent: cpu,
655 ..Default::default()
656 }
657 }
658
659 #[test]
660 fn cpu_power_falls_back_to_a_laptop_apu_only() {
661 let power = |cpu: Option<f32>, igpu: Option<f32>| PowerStats {
662 cpu_package_w: cpu,
663 igpu_w: igpu,
664 ..Default::default()
665 };
666 assert_eq!(
667 power(Some(30.0), Some(6.0)).cpu_or_soc_w(),
668 ("CPU", Some(30.0))
669 );
670 assert_eq!(power(None, Some(6.0)).cpu_or_soc_w(), ("SoC", Some(6.0)));
671 // A desktop Ryzen iGPU's few milliwatts are not the SoC.
672 assert_eq!(power(None, Some(0.007)).cpu_or_soc_w(), ("CPU", None));
673 assert_eq!(power(None, None).cpu_or_soc_w(), ("CPU", None));
674 }
675
676 #[test]
677 fn truncate_keeps_top_cpu_processes_in_order() {
678 let mut snapshot = Snapshot {
679 processes: vec![
680 process(1, 5.0),
681 process(2, 50.0),
682 process(3, 0.0),
683 process(4, 20.0),
684 ],
685 process_count: 4,
686 ..Default::default()
687 };
688 snapshot.truncate_processes(2);
689 let pids: Vec<u32> = snapshot.processes.iter().map(|p| p.pid).collect();
690 assert_eq!(pids, vec![2, 4]);
691 // The total is preserved for display ("2 of 4 processes").
692 assert_eq!(snapshot.process_count, 4);
693 }
694
695 #[test]
696 fn snapshot_round_trips_through_json() {
697 let snapshot = Snapshot {
698 timestamp_ms: 42,
699 processes: vec![process(7, 1.5)],
700 ..Default::default()
701 };
702 let json = serde_json::to_string(&snapshot).unwrap();
703 let back: Snapshot = serde_json::from_str(&json).unwrap();
704 assert_eq!(back.timestamp_ms, 42);
705 assert_eq!(back.processes[0].pid, 7);
706 }
707
708 #[test]
709 fn device_matching_handles_nvme_namespaces() {
710 assert!(device_matches_disk("sda", "sda"));
711 assert!(device_matches_disk("nvme0", "nvme0n1"));
712 assert!(device_matches_disk("nvme1", "nvme1n1"));
713 assert!(!device_matches_disk("nvme1", "nvme10n1"));
714 assert!(!device_matches_disk("sd", "sda"));
715 assert!(!device_matches_disk("sda", "sdb"));
716 }
717
718 #[test]
719 fn smart_attention_flags() {
720 let healthy = SmartDevice {
721 passed: Some(true),
722 media_errors: Some(0),
723 available_spare_percent: Some(100),
724 available_spare_threshold_percent: Some(10),
725 ..Default::default()
726 };
727 assert!(!healthy.needs_attention());
728 let worn = SmartDevice {
729 available_spare_percent: Some(5),
730 ..healthy.clone()
731 };
732 assert!(worn.needs_attention());
733 let reallocated = SmartDevice {
734 reallocated_sectors: Some(8),
735 ..healthy
736 };
737 assert!(reallocated.needs_attention());
738 }
739
740 #[test]
741 fn md_array_degraded_detection() {
742 let mut array = MdArray {
743 expected_devices: Some(2),
744 working_devices: Some(2),
745 ..Default::default()
746 };
747 assert!(!array.is_degraded());
748 array.working_devices = Some(1);
749 assert!(array.is_degraded());
750 }
751}