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