Skip to main content

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}