1use std::time::{SystemTime, UNIX_EPOCH};
10
11use crate::config::Config;
12use crate::hosts::{HostKind, HostSpec};
13use crate::snapshot::{
14 BatteryStats, BtrfsAllocation, BtrfsDeviceStats, BtrfsFilesystem, BtrfsScrub, CpuStats, DiskIo,
15 Fan, FilesystemUsage, GpuProcess, GpuStats, HostInfo, MdArray, MdMember, MdSync, MemoryStats,
16 NetworkStats, PowerStats, ProcessInfo, RaidStats, SensorChip, SmartDevice, Snapshot,
17 Temperature, WirelessLink,
18};
19use crate::source::{DataSource, ProcessSignal, SourceError};
20
21const GIB: u64 = 1 << 30;
22const TIB: u64 = 1 << 40;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum DemoHost {
27 Workstation,
29 Laptop,
31 Nas,
34}
35
36impl DemoHost {
37 pub const ALL: [DemoHost; 3] = [DemoHost::Workstation, DemoHost::Laptop, DemoHost::Nas];
39
40 pub fn name(self) -> &'static str {
42 match self {
43 DemoHost::Workstation => "workstation",
44 DemoHost::Laptop => "laptop",
45 DemoHost::Nas => "nas",
46 }
47 }
48}
49
50pub fn host_specs(config: &Config) -> Vec<HostSpec> {
52 DemoHost::ALL
53 .iter()
54 .map(|&host| HostSpec {
55 name: host.name().to_owned(),
56 kind: HostKind::Demo(host),
57 sections: config.overview.sections.clone(),
58 })
59 .collect()
60}
61
62#[derive(Debug, Clone)]
64pub struct DemoSource {
65 host: DemoHost,
66 time: f64,
68 rx_total: f64,
69 tx_total: f64,
70}
71
72impl DemoSource {
73 pub fn new(host: DemoHost) -> Self {
75 Self {
76 host,
77 time: 0.0,
78 rx_total: 412.0 * GIB as f64,
80 tx_total: 38.0 * GIB as f64,
81 }
82 }
83}
84
85impl DataSource for DemoSource {
86 fn poll(&mut self) -> Result<Snapshot, SourceError> {
87 self.time += 1.0;
88 let mut snapshot = match self.host {
89 DemoHost::Workstation => workstation(self.time),
90 DemoHost::Laptop => laptop(self.time),
91 DemoHost::Nas => nas(self.time),
92 };
93 for net in &mut snapshot.networks {
94 if net.name == primary_interface(self.host) {
95 self.rx_total += net.rx_bytes_per_sec;
96 self.tx_total += net.tx_bytes_per_sec;
97 net.rx_total_bytes = self.rx_total as u64;
98 net.tx_total_bytes = self.tx_total as u64;
99 }
100 }
101 snapshot.timestamp_ms = now_ms();
102 snapshot.process_count = snapshot.processes.len();
103 Ok(snapshot)
104 }
105
106 fn signal_process(&mut self, _pid: u32, _signal: ProcessSignal) -> Result<(), SourceError> {
107 Ok(())
109 }
110}
111
112fn primary_interface(host: DemoHost) -> &'static str {
113 match host {
114 DemoHost::Workstation => "enp9s0",
115 DemoHost::Laptop => "wlp1s0",
116 DemoHost::Nas => "eno1",
117 }
118}
119
120fn now_ms() -> u64 {
121 SystemTime::now()
122 .duration_since(UNIX_EPOCH)
123 .map_or(0, |d| d.as_millis() as u64)
124}
125
126fn wobble(seed: u64, t: f64, period: f64) -> f64 {
129 let x = t / period;
130 let (cell, frac) = (x.floor(), x.fract());
131 let at = |c: f64| hash(seed ^ (c as i64 as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
132 let s = frac * frac * (3.0 - 2.0 * frac);
134 at(cell) * (1.0 - s) + at(cell + 1.0) * s
135}
136
137fn hash(seed: u64) -> f64 {
139 let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
140 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
141 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
142 z ^= z >> 31;
143 (z >> 11) as f64 / (1u64 << 53) as f64
144}
145
146fn duty(t: f64, period: f64, busy: f64) -> f64 {
149 let phase = t.rem_euclid(period);
150 let ramp = 4.0;
151 if phase < ramp {
152 phase / ramp
153 } else if phase < busy {
154 1.0
155 } else if phase < busy + ramp {
156 1.0 - (phase - busy) / ramp
157 } else {
158 0.0
159 }
160}
161
162fn temp(chip: &str, n: u32, label: &str, celsius: f64) -> Temperature {
163 Temperature {
164 key: format!("{chip}/temp{n}"),
165 label: label.to_owned(),
166 celsius: celsius as f32,
167 ..Default::default()
168 }
169}
170
171fn fan(chip: &str, n: u32, rpm: f64) -> Fan {
172 Fan {
173 key: format!("{chip}/fan{n}"),
174 label: format!("fan{n}"),
175 rpm: rpm as u32,
176 }
177}
178
179fn cores(seed: u64, t: f64, count: usize, total: f64) -> Vec<f32> {
181 (0..count)
182 .map(|i| {
183 let spread = wobble(seed + i as u64, t, 3.0 + (i % 5) as f64);
184 ((total * (0.55 + 0.9 * spread)) + 2.0 * spread).clamp(0.0, 100.0) as f32
185 })
186 .collect()
187}
188
189#[allow(clippy::too_many_arguments)]
191fn process(
192 pid: u32,
193 name: &str,
194 command: &str,
195 user: &str,
196 cpu: f64,
197 memory: u64,
198 threads: u32,
199 run_time_secs: u64,
200) -> ProcessInfo {
201 ProcessInfo {
202 pid,
203 parent_pid: Some(1),
204 name: name.to_owned(),
205 command: command.to_owned(),
206 user: user.to_owned(),
207 state: if cpu > 1.0 { "R" } else { "S" }.to_owned(),
208 cpu_percent: cpu.max(0.0) as f32,
209 memory_bytes: memory,
210 threads: Some(threads),
211 run_time_secs,
212 ..Default::default()
213 }
214}
215
216fn workstation(t: f64) -> Snapshot {
217 let build = duty(t, 120.0, 45.0);
219 let cpu_total = 9.0 + 70.0 * build + 6.0 * wobble(1, t, 4.0);
220 let per_core = cores(100, t, 32, cpu_total);
221 let tctl = 48.0 + 0.38 * cpu_total + 2.0 * wobble(2, t, 6.0);
222 let cpu_w = 38.0 + 1.6 * cpu_total;
223 let eval = duty(t + 200.0, 300.0, 40.0);
225 let gpu0_util = 97.0 - 60.0 * eval + 3.0 * wobble(3, t, 2.0);
226 let gpu0_w = 120.0 + 3.0 * gpu0_util;
227 let requests = (wobble(4, t, 9.0) - 0.35).max(0.0) / 0.65;
229 let gpu1_util = 85.0 * requests;
230 let gpu1_w = 22.0 + 1.1 * gpu1_util;
231 let download = (wobble(5, t, 15.0) - 0.4).max(0.0) / 0.6;
232 let k10 = "k10temp:0000:00:18.3";
233 let nct = "nct6686:nct6683.2592";
234 let train_pid = 48_211;
235 let ollama_pid = 2_204;
236 let mut processes = vec![
237 process(
238 train_pid,
239 "python",
240 "python train.py --config configs/vit-large.yaml",
241 "demo",
242 160.0 + 40.0 * wobble(6, t, 3.0) - 80.0 * eval,
243 18 * GIB + GIB / 3,
244 64,
245 5 * 3600 + t as u64,
246 ),
247 process(
248 ollama_pid,
249 "ollama",
250 "/usr/local/bin/ollama serve",
251 "ollama",
252 4.0 + 60.0 * requests,
253 3 * GIB,
254 42,
255 9 * 86_400,
256 ),
257 process(
258 3_120,
259 "gnome-shell",
260 "/usr/bin/gnome-shell",
261 "demo",
262 3.0 + 2.0 * wobble(7, t, 2.0),
263 GIB,
264 28,
265 9 * 86_400,
266 ),
267 process(
268 5_310,
269 "firefox",
270 "/usr/lib/firefox/firefox",
271 "demo",
272 2.0 + 6.0 * wobble(8, t, 5.0),
273 2 * GIB + GIB / 2,
274 112,
275 2 * 86_400,
276 ),
277 process(
278 6_002,
279 "code",
280 "/usr/share/code/code",
281 "demo",
282 1.0 + 3.0 * wobble(9, t, 4.0),
283 GIB + GIB / 4,
284 40,
285 86_400,
286 ),
287 process(
288 7_734,
289 "rigtop-gui",
290 "rigtop-gui",
291 "demo",
292 2.0,
293 180 << 20,
294 12,
295 3_600,
296 ),
297 process(
298 1_402,
299 "postgres",
300 "postgres: checkpointer",
301 "postgres",
302 0.3,
303 420 << 20,
304 1,
305 9 * 86_400,
306 ),
307 process(
308 1_388,
309 "dockerd",
310 "/usr/bin/dockerd -H fd://",
311 "root",
312 0.4,
313 96 << 20,
314 30,
315 9 * 86_400,
316 ),
317 process(
318 1_105,
319 "sshd",
320 "sshd: /usr/sbin/sshd -D",
321 "root",
322 0.0,
323 12 << 20,
324 1,
325 9 * 86_400,
326 ),
327 process(
328 1,
329 "systemd",
330 "/sbin/init",
331 "root",
332 0.1,
333 14 << 20,
334 1,
335 9 * 86_400,
336 ),
337 ];
338 let idle: [(u32, &str, &str, &str, u64, u32); 22] = [
340 (
341 2_301,
342 "jupyter-lab",
343 "python -m jupyterlab --no-browser",
344 "demo",
345 310,
346 6,
347 ),
348 (
349 2_388,
350 "tensorboard",
351 "python -m tensorboard.main --logdir runs",
352 "demo",
353 520,
354 18,
355 ),
356 (
357 3_044,
358 "Xwayland",
359 "/usr/bin/Xwayland :0 -rootless",
360 "demo",
361 110,
362 9,
363 ),
364 (3_090, "pipewire", "/usr/bin/pipewire", "demo", 24, 3),
365 (3_092, "wireplumber", "/usr/bin/wireplumber", "demo", 30, 6),
366 (
367 3_401,
368 "tmux: server",
369 "tmux new-session -s work",
370 "demo",
371 9,
372 1,
373 ),
374 (3_420, "zsh", "-zsh", "demo", 7, 1),
375 (
376 3_520,
377 "node",
378 "node /usr/lib/node_modules/vite/bin/vite.js",
379 "demo",
380 260,
381 11,
382 ),
383 (3_610, "claude", "claude", "demo", 330, 19),
384 (1_201, "containerd", "/usr/bin/containerd", "root", 52, 18),
385 (
386 1_266,
387 "redis-server",
388 "/usr/bin/redis-server 127.0.0.1:6379",
389 "redis",
390 18,
391 5,
392 ),
393 (1_270, "nginx", "nginx: worker process", "www-data", 11, 1),
394 (
395 512,
396 "systemd-journal",
397 "/usr/lib/systemd/systemd-journald",
398 "root",
399 64,
400 1,
401 ),
402 (
403 880,
404 "NetworkManager",
405 "/usr/sbin/NetworkManager --no-daemon",
406 "root",
407 21,
408 4,
409 ),
410 (902, "tailscaled", "/usr/sbin/tailscaled", "root", 48, 14),
411 (
412 930,
413 "nvidia-persiste",
414 "/usr/bin/nvidia-persistenced",
415 "nvidia-persistenced",
416 2,
417 1,
418 ),
419 (1_010, "cupsd", "/usr/sbin/cupsd -l", "root", 8, 1),
420 (1_050, "smartd", "/usr/sbin/smartd -n", "root", 6, 1),
421 (
422 3_150,
423 "gnome-terminal-",
424 "/usr/libexec/gnome-terminal-server",
425 "demo",
426 90,
427 5,
428 ),
429 (
430 3_180,
431 "nautilus",
432 "/usr/bin/nautilus --gapplication-service",
433 "demo",
434 140,
435 9,
436 ),
437 (
438 3_230,
439 "evolution-alarm",
440 "/usr/libexec/evolution-data-server/evolution-alarm-notify",
441 "demo",
442 70,
443 7,
444 ),
445 (
446 3_260,
447 "ibus-daemon",
448 "/usr/bin/ibus-daemon --panel disable",
449 "demo",
450 15,
451 4,
452 ),
453 ];
454 for (i, &(pid, name, command, user, mib, threads)) in idle.iter().enumerate() {
455 let cpu = (wobble(200 + i as u64, t, 4.0) - 0.6).max(0.0) * 2.0;
456 processes.push(process(
457 pid,
458 name,
459 command,
460 user,
461 cpu,
462 mib << 20,
463 threads,
464 9 * 86_400 - pid as u64 * 7,
465 ));
466 }
467 if build > 0.0 {
468 processes.push(process(
470 60_100,
471 "cargo",
472 "cargo build --release",
473 "demo",
474 1.0,
475 200 << 20,
476 8,
477 40,
478 ));
479 for i in 0..6 {
480 processes.push(process(
481 60_120 + i,
482 "rustc",
483 "rustc --crate-name rigtop_core --edition=2024",
484 "demo",
485 (90.0 + 10.0 * wobble(10 + i as u64, t, 2.0)) * build,
486 (600 + 100 * i as u64) << 20,
487 2,
488 20,
489 ));
490 }
491 }
492 let nvme = |i: u32, celsius: f64| SensorChip {
493 key: format!("nvme:nvme{i}"),
494 name: "nvme".to_owned(),
495 temperatures: vec![temp(&format!("nvme:nvme{i}"), 1, "Composite", celsius)],
496 fans: Vec::new(),
497 };
498 let drive_temp = |i: u32| 44.0 + 4.0 * i as f64 + 5.0 * build;
499 let disk_io = |i: u64| {
500 let busy = build * (0.5 + 0.5 * wobble(20 + i, t, 2.0));
501 DiskIo {
502 name: format!("nvme{i}n1"),
503 read_bytes_per_sec: 180e6 * busy + 2e6 * wobble(30 + i, t, 3.0),
504 write_bytes_per_sec: 90e6 * busy + 40e6 * download,
505 busy_percent: Some((35.0 * busy + 3.0 * download) as f32),
506 temperature_c: Some(drive_temp(i as u32) as f32),
507 }
508 };
509 let smart = |i: u32| SmartDevice {
510 device: format!("nvme{i}"),
511 protocol: "NVMe".to_owned(),
512 model: "Samsung SSD 990 PRO 2TB".to_owned(),
513 passed: Some(true),
514 temperature_c: Some(drive_temp(i) as f32),
515 power_on_hours: Some(4_210 + 13 * i as u64),
516 percentage_used: Some(3),
517 available_spare_percent: Some(100),
518 available_spare_threshold_percent: Some(10),
519 critical_warning: Some(0),
520 media_errors: Some(0),
521 data_written_bytes: Some(41 * TIB + i as u64 * TIB),
522 collected_at_ms: Some(now_ms() - 140_000),
523 ..Default::default()
524 };
525 let used_memory = 38.0 + 6.0 * build + 1.5 * wobble(11, t, 20.0);
526 Snapshot {
527 host: HostInfo {
528 hostname: "workstation".to_owned(),
529 os: "Linux (Ubuntu 26.04)".to_owned(),
530 kernel: "7.0.0-38-generic".to_owned(),
531 uptime_secs: 9 * 86_400 + 4 * 3600 + t as u64,
532 rigtop_version: env!("CARGO_PKG_VERSION").to_owned(),
533 },
534 cpu: CpuStats {
535 model: "AMD Ryzen 9 9950X3D 16-Core Processor".to_owned(),
536 total_percent: cpu_total as f32,
537 per_core_mhz: per_core.iter().map(|u| 3_000 + (*u as u64) * 25).collect(),
538 per_core_percent: per_core,
539 load_average: Some([
540 0.4 + 22.0 * build,
541 3.0 + 9.0 * duty(t + 30.0, 120.0, 90.0),
542 6.2,
543 ]),
544 temperatures: vec![
545 temp(k10, 1, "Tctl", tctl),
546 temp(k10, 3, "Tccd1", tctl - 8.0),
547 temp(k10, 4, "Tccd2", tctl - 9.0),
548 ],
549 },
550 memory: MemoryStats {
551 total_bytes: 96 * GIB,
552 used_bytes: (used_memory * GIB as f64) as u64,
553 available_bytes: ((96.0 - used_memory) * GIB as f64) as u64,
554 free_bytes: 12 * GIB,
555 cached_bytes: Some(44 * GIB),
556 swap_total_bytes: 8 * GIB,
557 swap_used_bytes: 256 << 20,
558 },
559 gpus: vec![
560 GpuStats {
561 index: 0,
562 vendor: "NVIDIA".to_owned(),
563 name: "NVIDIA GeForce RTX 4090".to_owned(),
564 utilization_percent: Some(gpu0_util.clamp(0.0, 100.0) as f32),
565 memory_utilization_percent: Some((gpu0_util * 0.6) as f32),
566 memory_used_bytes: Some(21 * GIB + GIB / 3),
567 memory_total_bytes: Some(24 * GIB),
568 temperature_c: Some((52.0 + 0.2 * gpu0_util) as f32),
569 power_w: Some(gpu0_w as f32),
570 power_limit_w: Some(450.0),
571 fan_percent: Some((30.0 + 0.3 * gpu0_util) as f32),
572 clock_mhz: Some(2_520),
573 processes: vec![GpuProcess {
574 pid: train_pid,
575 memory_bytes: Some(21 * GIB),
576 sm_percent: Some(gpu0_util.clamp(0.0, 100.0) as f32),
577 }],
578 ..Default::default()
579 },
580 GpuStats {
581 index: 1,
582 vendor: "NVIDIA".to_owned(),
583 name: "NVIDIA RTX 4000 Ada Generation".to_owned(),
584 utilization_percent: Some(gpu1_util as f32),
585 memory_utilization_percent: Some((gpu1_util * 0.4) as f32),
586 memory_used_bytes: Some(9 * GIB + GIB * 4 / 5),
587 memory_total_bytes: Some(20 * GIB),
588 temperature_c: Some((44.0 + 0.2 * gpu1_util) as f32),
589 power_w: Some(gpu1_w as f32),
590 power_limit_w: Some(130.0),
591 fan_percent: Some((30.0 + 0.4 * gpu1_util) as f32),
592 clock_mhz: Some(if gpu1_util > 5.0 { 2_175 } else { 210 }),
593 processes: vec![GpuProcess {
594 pid: ollama_pid,
595 memory_bytes: Some(9 * GIB + GIB / 2),
596 sm_percent: Some(gpu1_util as f32),
597 }],
598 ..Default::default()
599 },
600 ],
601 sensors: vec![
602 SensorChip {
603 key: k10.to_owned(),
604 name: "k10temp".to_owned(),
605 temperatures: vec![
606 temp(k10, 1, "Tctl", tctl),
607 temp(k10, 3, "Tccd1", tctl - 8.0),
608 temp(k10, 4, "Tccd2", tctl - 9.0),
609 ],
610 fans: Vec::new(),
611 },
612 SensorChip {
613 key: nct.to_owned(),
614 name: "nct6686".to_owned(),
615 temperatures: vec![
616 temp(nct, 1, "Thermistor 14", 38.0 + 0.1 * cpu_total),
617 temp(nct, 2, "AMD TSI Addr 98h", tctl - 2.0),
618 ],
619 fans: vec![
620 fan(nct, 1, 850.0 + 9.0 * cpu_total),
621 fan(nct, 2, 0.0),
622 fan(nct, 3, 780.0 + 4.0 * cpu_total),
623 ],
624 },
625 nvme(0, drive_temp(0)),
626 nvme(1, drive_temp(1)),
627 nvme(2, drive_temp(2)),
628 ],
629 power: PowerStats {
630 cpu_package_w: Some(cpu_w as f32),
631 gpus_w: Some((gpu0_w + gpu1_w) as f32),
632 igpu_w: None,
633 total_w: Some((cpu_w + gpu0_w + gpu1_w) as f32),
634 battery: None,
635 },
636 disks: (0..3).map(disk_io).collect(),
637 filesystems: vec![
638 FilesystemUsage {
639 device: "/dev/nvme0n1p2".to_owned(),
640 fs_type: "btrfs".to_owned(),
641 mount_points: vec!["/".to_owned(), "/home".to_owned()],
642 total_bytes: 5 * TIB + TIB / 2,
643 available_bytes: 3 * TIB + TIB / 10,
644 },
645 FilesystemUsage {
646 device: "/dev/nvme0n1p1".to_owned(),
647 fs_type: "vfat".to_owned(),
648 mount_points: vec!["/boot/efi".to_owned()],
649 total_bytes: GIB,
650 available_bytes: GIB - (60 << 20),
651 },
652 ],
653 raid: RaidStats {
654 btrfs: vec![BtrfsFilesystem {
655 fsid: "6f1c2a9e-0d4b-4e58-9a3e-2b7c51d0e8f4".to_owned(),
656 label: String::new(),
657 mount_points: vec!["/".to_owned(), "/home".to_owned()],
658 devices: vec![
659 "nvme0n1p2".to_owned(),
660 "nvme1n1".to_owned(),
661 "nvme2n1".to_owned(),
662 ],
663 device_stats: (1..=3)
664 .map(|devid| BtrfsDeviceStats {
665 devid,
666 name: if devid == 1 {
667 "nvme0n1p2".to_owned()
668 } else {
669 format!("nvme{}n1", devid - 1)
670 },
671 size_bytes: Some(2 * TIB - (1 << 33)),
672 used_bytes: Some(TIB - (1 << 36)),
673 ..Default::default()
674 })
675 .collect(),
676 allocations: vec![
677 BtrfsAllocation {
678 kind: "data".to_owned(),
679 profile: "raid0".to_owned(),
680 total_bytes: 2 * TIB + TIB / 2,
681 used_bytes: 2 * TIB + TIB / 3,
682 },
683 BtrfsAllocation {
684 kind: "metadata".to_owned(),
685 profile: "raid1c3".to_owned(),
686 total_bytes: 12 * GIB,
687 used_bytes: 9 * GIB,
688 },
689 BtrfsAllocation {
690 kind: "system".to_owned(),
691 profile: "raid1c3".to_owned(),
692 total_bytes: 32 << 20,
693 used_bytes: 1 << 20,
694 },
695 ],
696 scrub: Some(BtrfsScrub {
697 started_at_ms: now_ms() - 3 * 86_400_000,
698 duration_secs: 1_920,
699 state: "finished".to_owned(),
700 bytes_scrubbed: 2 * TIB + TIB / 3,
701 ..Default::default()
702 }),
703 }],
704 md: Vec::new(),
705 },
706 smart: (0..3).map(smart).collect(),
707 networks: vec![
708 NetworkStats {
709 name: "enp9s0".to_owned(),
710 rx_bytes_per_sec: 280e6 * download + 60e3 * wobble(12, t, 1.5),
711 tx_bytes_per_sec: 4e6 * download + 25e3 * wobble(13, t, 2.0),
712 link_speed_mbps: Some(10_000),
713 link_up: Some(true),
714 ..Default::default()
715 },
716 NetworkStats {
717 name: "tailscale0".to_owned(),
718 rx_bytes_per_sec: 9e3 * wobble(14, t, 3.0),
719 tx_bytes_per_sec: 40e3 * wobble(15, t, 4.0),
720 rx_total_bytes: 3 * GIB,
721 tx_total_bytes: 9 * GIB,
722 link_up: Some(true),
723 ..Default::default()
724 },
725 ],
726 processes,
727 ..Default::default()
728 }
729}
730
731fn laptop(t: f64) -> Snapshot {
732 let busy = (wobble(40, t, 8.0) - 0.3).max(0.0) / 0.7;
733 let cpu_total = 6.0 + 35.0 * busy;
734 let per_core = cores(200, t, 16, cpu_total);
735 let tctl = 46.0 + 0.5 * cpu_total;
736 let soc_w = 5.0 + 0.4 * cpu_total;
737 let k10 = "k10temp:0000:00:18.3";
738 Snapshot {
739 host: HostInfo {
740 hostname: "laptop".to_owned(),
741 os: "Linux (CachyOS)".to_owned(),
742 kernel: "7.0.2-cachyos".to_owned(),
743 uptime_secs: 8 * 86_400 + 10 * 3600 + t as u64,
744 rigtop_version: "0.8.2".to_owned(),
746 },
747 cpu: CpuStats {
748 model: "AMD Ryzen 7 PRO 7840U w/ Radeon 780M Graphics".to_owned(),
749 total_percent: cpu_total as f32,
750 per_core_mhz: per_core.iter().map(|u| 1_400 + (*u as u64) * 35).collect(),
751 per_core_percent: per_core,
752 load_average: Some([0.8 + 3.0 * busy, 1.6, 1.4]),
753 temperatures: vec![temp(k10, 1, "Tctl", tctl)],
754 },
755 memory: MemoryStats {
756 total_bytes: 30 * GIB,
757 used_bytes: 15 * GIB + (busy * 2.0 * GIB as f64) as u64,
758 available_bytes: 14 * GIB,
759 free_bytes: 3 * GIB,
760 cached_bytes: Some(10 * GIB),
761 swap_total_bytes: 30 * GIB,
762 swap_used_bytes: 0,
763 },
764 gpus: vec![GpuStats {
765 index: 0,
766 vendor: "AMD".to_owned(),
767 name: "AMD Radeon 780M".to_owned(),
768 integrated: true,
769 utilization_percent: Some((4.0 + 20.0 * busy) as f32),
770 memory_used_bytes: Some(GIB + GIB / 2),
771 memory_total_bytes: Some(2 * GIB),
772 temperature_c: Some((tctl - 3.0) as f32),
773 clock_mhz: Some(800),
774 ..Default::default()
775 }],
776 sensors: vec![SensorChip {
777 key: k10.to_owned(),
778 name: "k10temp".to_owned(),
779 temperatures: vec![temp(k10, 1, "Tctl", tctl)],
780 fans: Vec::new(),
781 }],
782 power: PowerStats {
783 igpu_w: Some(soc_w as f32),
784 total_w: Some(soc_w as f32),
785 battery: Some(BatteryStats {
786 percent: Some((78.0 - t / 600.0).max(5.0) as f32),
787 status: "Discharging".to_owned(),
788 power_w: Some((soc_w + 3.0) as f32),
789 }),
790 ..Default::default()
791 },
792 disks: vec![DiskIo {
793 name: "nvme0n1".to_owned(),
794 read_bytes_per_sec: 6e6 * busy,
795 write_bytes_per_sec: 1e6 * busy,
796 busy_percent: Some((4.0 * busy) as f32),
797 temperature_c: Some((38.0 + 4.0 * busy) as f32),
798 }],
799 filesystems: vec![FilesystemUsage {
800 device: "/dev/nvme0n1p2".to_owned(),
801 fs_type: "ext4".to_owned(),
802 mount_points: vec!["/".to_owned()],
803 total_bytes: 930 * GIB,
804 available_bytes: 352 * GIB,
805 }],
806 networks: vec![NetworkStats {
807 name: "wlp1s0".to_owned(),
808 rx_bytes_per_sec: 1.5e6 * busy + 20e3 * wobble(41, t, 2.0),
809 tx_bytes_per_sec: 0.3e6 * busy + 8e3 * wobble(42, t, 3.0),
810 link_up: Some(true),
811 wireless: Some(WirelessLink {
812 rx_bitrate_mbps: Some(1_201.0),
813 tx_bitrate_mbps: Some(864.0),
814 signal_dbm: Some(-54),
815 }),
816 ..Default::default()
817 }],
818 processes: vec![
819 process(
820 2_810,
821 "plasmashell",
822 "/usr/bin/plasmashell",
823 "demo",
824 1.0 + 8.0 * busy,
825 520 << 20,
826 24,
827 8 * 86_400,
828 ),
829 process(
830 4_420,
831 "code",
832 "/opt/visual-studio-code/code",
833 "demo",
834 2.0 + 30.0 * busy,
835 900 << 20,
836 38,
837 86_400,
838 ),
839 process(
840 4_980,
841 "firefox",
842 "/usr/lib/firefox/firefox",
843 "demo",
844 1.5 + 10.0 * wobble(43, t, 6.0),
845 1_800 << 20,
846 96,
847 3 * 86_400,
848 ),
849 process(
850 5_102,
851 "rigtop",
852 "rigtop --agent",
853 "demo",
854 0.6,
855 10 << 20,
856 3,
857 3_600,
858 ),
859 process(
860 1,
861 "systemd",
862 "/sbin/init",
863 "root",
864 0.0,
865 12 << 20,
866 1,
867 8 * 86_400,
868 ),
869 ],
870 ..Default::default()
871 }
872}
873
874fn nas(t: f64) -> Snapshot {
875 let cpu_total = 18.0 + 10.0 * wobble(60, t, 5.0);
876 let per_core = cores(300, t, 4, cpu_total);
877 let resync = 42.0 + t / 120.0;
878 let disks = ["sda", "sdb", "sdc", "sdd"];
879 let smart = |i: usize| SmartDevice {
880 device: disks[i].to_owned(),
881 protocol: "ATA".to_owned(),
882 model: "WDC WD80EFPX-68C4ZN0".to_owned(),
883 passed: Some(true),
884 temperature_c: Some(38.0 + i as f32),
885 power_on_hours: Some(21_400 + 200 * i as u64),
886 reallocated_sectors: Some(if i == 2 { 8 } else { 0 }),
887 pending_sectors: Some(0),
888 offline_uncorrectable: Some(0),
889 collected_at_ms: Some(now_ms() - 300_000),
890 ..Default::default()
891 };
892 Snapshot {
893 host: HostInfo {
894 hostname: "nas".to_owned(),
895 os: "Linux (Debian 13)".to_owned(),
896 kernel: "6.12.48-amd64".to_owned(),
897 uptime_secs: 41 * 86_400 + t as u64,
898 rigtop_version: env!("CARGO_PKG_VERSION").to_owned(),
899 },
900 cpu: CpuStats {
901 model: "Intel(R) Celeron(R) N5105 @ 2.00GHz".to_owned(),
902 total_percent: cpu_total as f32,
903 per_core_mhz: vec![2_000; 4],
904 per_core_percent: per_core,
905 load_average: Some([1.9, 2.1, 2.0]),
906 temperatures: vec![temp("coretemp:coretemp.0", 1, "Package id 0", 52.0)],
907 },
908 memory: MemoryStats {
909 total_bytes: 16 * GIB,
910 used_bytes: 3 * GIB,
911 available_bytes: 12 * GIB,
912 free_bytes: GIB,
913 cached_bytes: Some(11 * GIB),
914 ..Default::default()
915 },
916 disks: (0..4)
917 .map(|i| DiskIo {
918 name: disks[i].to_owned(),
919 read_bytes_per_sec: 150e6 + 20e6 * wobble(61 + i as u64, t, 4.0),
920 write_bytes_per_sec: if i == 3 { 148e6 } else { 1e6 },
921 busy_percent: Some(88.0 + 8.0 * wobble(65 + i as u64, t, 3.0) as f32),
922 temperature_c: Some(38.0 + i as f32),
923 })
924 .collect(),
925 filesystems: vec![
926 FilesystemUsage {
927 device: "/dev/md0".to_owned(),
928 fs_type: "ext4".to_owned(),
929 mount_points: vec!["/srv".to_owned()],
930 total_bytes: 21 * TIB + TIB / 2,
931 available_bytes: 2 * TIB - TIB / 5,
932 },
933 FilesystemUsage {
934 device: "/dev/sde2".to_owned(),
935 fs_type: "ext4".to_owned(),
936 mount_points: vec!["/".to_owned()],
937 total_bytes: 230 * GIB,
938 available_bytes: 180 * GIB,
939 },
940 ],
941 raid: RaidStats {
942 btrfs: Vec::new(),
943 md: vec![MdArray {
944 name: "md0".to_owned(),
945 active: true,
946 level: "raid5".to_owned(),
947 members: disks
948 .iter()
949 .enumerate()
950 .map(|(role, name)| MdMember {
951 name: (*name).to_owned(),
952 role: role as u32,
953 ..Default::default()
954 })
955 .collect(),
956 expected_devices: Some(4),
957 working_devices: Some(4),
958 sync: Some(MdSync {
959 action: "resync".to_owned(),
960 percent: resync.min(99.9) as f32,
961 finish_minutes: Some((100.0 - resync).max(0.0) as f32 * 9.0),
962 speed_kib_per_sec: Some(148_000),
963 }),
964 ..Default::default()
965 }],
966 },
967 smart: (0..4).map(smart).collect(),
968 networks: vec![NetworkStats {
969 name: "eno1".to_owned(),
970 rx_bytes_per_sec: 2e6 * wobble(70, t, 6.0),
971 tx_bytes_per_sec: 95e6 * ((wobble(71, t, 10.0) - 0.3).max(0.0) / 0.7),
972 link_speed_mbps: Some(2_500),
973 link_up: Some(true),
974 ..Default::default()
975 }],
976 processes: vec![
977 process(812, "md0_resync", "", "root", 9.0, 0, 1, 3 * 3600),
978 process(
979 1_204,
980 "smbd",
981 "/usr/sbin/smbd --foreground --no-process-group",
982 "root",
983 6.0 + 20.0 * wobble(72, t, 10.0),
984 120 << 20,
985 4,
986 41 * 86_400,
987 ),
988 process(
989 1_330,
990 "jellyfin",
991 "/usr/lib/jellyfin/bin/jellyfin",
992 "jellyfin",
993 3.0 + 4.0 * wobble(73, t, 7.0),
994 700 << 20,
995 30,
996 41 * 86_400,
997 ),
998 process(
999 1_002,
1000 "rigtop",
1001 "rigtop --agent",
1002 "admin",
1003 0.4,
1004 9 << 20,
1005 3,
1006 3_600,
1007 ),
1008 process(
1009 1,
1010 "systemd",
1011 "/sbin/init",
1012 "root",
1013 0.0,
1014 11 << 20,
1015 1,
1016 41 * 86_400,
1017 ),
1018 ],
1019 ..Default::default()
1020 }
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025 use super::*;
1026 use crate::view::{Severity, alerts};
1027
1028 #[test]
1029 fn demo_hosts_change_over_time_and_repeat_exactly() {
1030 let mut a = DemoSource::new(DemoHost::Workstation);
1031 let mut b = DemoSource::new(DemoHost::Workstation);
1032 let first = a.poll().unwrap();
1033 b.poll().unwrap();
1034 let mut later = first.clone();
1035 for _ in 0..30 {
1036 later = a.poll().unwrap();
1037 b.poll().unwrap();
1038 }
1039 assert_ne!(first.cpu.total_percent, later.cpu.total_percent);
1040 assert_eq!(
1041 b.poll().unwrap().cpu.total_percent,
1042 a.poll().unwrap().cpu.total_percent
1043 );
1044 assert!(later.networks[0].rx_total_bytes > first.networks[0].rx_total_bytes);
1046 assert_eq!(later.process_count, later.processes.len());
1047 for gpu in &later.gpus {
1049 for p in &gpu.processes {
1050 assert!(later.processes.iter().any(|q| q.pid == p.pid));
1051 }
1052 }
1053 }
1054
1055 #[test]
1056 fn the_nas_needs_attention() {
1057 let snapshot = DemoSource::new(DemoHost::Nas).poll().unwrap();
1058 let found = alerts(&snapshot);
1059 let texts: Vec<&str> = found.iter().map(|a| a.text.as_str()).collect();
1060 assert_eq!(found[0].severity, Severity::Critical);
1061 assert!(texts.contains(&"sdc: SMART warning"), "{texts:?}");
1062 assert!(
1063 texts.iter().any(|t| t.starts_with("md0 resync")),
1064 "{texts:?}"
1065 );
1066 assert!(texts.iter().any(|t| t.starts_with("/srv ")), "{texts:?}");
1067 assert!(alerts(&DemoSource::new(DemoHost::Workstation).poll().unwrap()).is_empty());
1068 }
1069
1070 #[test]
1071 fn wobble_stays_in_range() {
1072 for i in 0..1000 {
1073 let v = wobble(7, i as f64 * 0.37, 3.0);
1074 assert!((0.0..=1.0).contains(&v));
1075 }
1076 }
1077}