1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum SimdInstructionSet {
37 Scalar,
39 Sse4,
41 Avx2,
43 Avx512,
45 Neon,
47 WasmSimd128,
49}
50
51impl SimdInstructionSet {
52 #[must_use]
54 pub const fn vector_width(self) -> usize {
55 match self {
56 Self::Scalar => 1,
57 Self::Sse4 | Self::Neon | Self::WasmSimd128 => 4,
58 Self::Avx2 => 8,
59 Self::Avx512 => 16,
60 }
61 }
62
63 #[must_use]
65 pub fn detect() -> Self {
66 #[cfg(all(target_arch = "x86_64", target_feature = "avx2"))]
67 {
68 if is_x86_feature_detected!("avx2") {
69 return Self::Avx2;
70 }
71 }
72
73 #[cfg(all(target_arch = "x86_64", target_feature = "sse4.1"))]
74 {
75 if is_x86_feature_detected!("sse4.1") {
76 return Self::Sse4;
77 }
78 }
79
80 #[cfg(target_arch = "aarch64")]
81 {
82 return Self::Neon;
84 }
85
86 #[cfg(target_arch = "wasm32")]
87 {
88 #[cfg(target_feature = "simd128")]
90 return Self::WasmSimd128;
91 }
92
93 Self::Scalar
94 }
95
96 #[must_use]
98 pub const fn name(self) -> &'static str {
99 match self {
100 Self::Scalar => "Scalar",
101 Self::Sse4 => "SSE4.1",
102 Self::Avx2 => "AVX2",
103 Self::Avx512 => "AVX-512",
104 Self::Neon => "NEON",
105 Self::WasmSimd128 => "WASM SIMD128",
106 }
107 }
108}
109
110impl Default for SimdInstructionSet {
111 fn default() -> Self {
112 Self::detect()
113 }
114}
115
116pub trait ComputeBlock {
143 type Input;
145 type Output;
147
148 fn compute(&mut self, input: &Self::Input) -> Self::Output;
154
155 fn simd_supported(&self) -> bool {
157 self.simd_instruction_set() != SimdInstructionSet::Scalar
158 }
159
160 fn simd_instruction_set(&self) -> SimdInstructionSet {
162 SimdInstructionSet::detect()
163 }
164
165 fn latency_budget_us(&self) -> u64 {
167 1000 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173pub enum ComputeBlockId {
174 CpuSparklines, CpuLoadGauge, CpuLoadTrend, CpuFrequency, CpuBoostIndicator, CpuTemperature, CpuTopConsumers, MemSparklines, MemZramRatio, MemPressureGauge, MemSwapThrashing, MemCacheBreakdown, MemHugePages, ConnAge, ConnProc, ConnGeo, ConnLatency, ConnService, ConnHotIndicator, ConnSparkline, NetSparklines, NetProtocolStats, NetErrorRate, NetDropRate, NetLatencyGauge, NetBandwidthUtil, ProcTreeView, ProcSortIndicator, ProcFilter, ProcOomScore, ProcNiceValue, ProcThreadCount, ProcCgroup, }
217
218impl ComputeBlockId {
219 #[must_use]
221 pub const fn id_string(&self) -> &'static str {
222 match self {
223 Self::CpuSparklines => "CB-CPU-001",
224 Self::CpuLoadGauge => "CB-CPU-002",
225 Self::CpuLoadTrend => "CB-CPU-003",
226 Self::CpuFrequency => "CB-CPU-004",
227 Self::CpuBoostIndicator => "CB-CPU-005",
228 Self::CpuTemperature => "CB-CPU-006",
229 Self::CpuTopConsumers => "CB-CPU-007",
230 Self::MemSparklines => "CB-MEM-001",
231 Self::MemZramRatio => "CB-MEM-002",
232 Self::MemPressureGauge => "CB-MEM-003",
233 Self::MemSwapThrashing => "CB-MEM-004",
234 Self::MemCacheBreakdown => "CB-MEM-005",
235 Self::MemHugePages => "CB-MEM-006",
236 Self::ConnAge => "CB-CONN-001",
237 Self::ConnProc => "CB-CONN-002",
238 Self::ConnGeo => "CB-CONN-003",
239 Self::ConnLatency => "CB-CONN-004",
240 Self::ConnService => "CB-CONN-005",
241 Self::ConnHotIndicator => "CB-CONN-006",
242 Self::ConnSparkline => "CB-CONN-007",
243 Self::NetSparklines => "CB-NET-001",
244 Self::NetProtocolStats => "CB-NET-002",
245 Self::NetErrorRate => "CB-NET-003",
246 Self::NetDropRate => "CB-NET-004",
247 Self::NetLatencyGauge => "CB-NET-005",
248 Self::NetBandwidthUtil => "CB-NET-006",
249 Self::ProcTreeView => "CB-PROC-001",
250 Self::ProcSortIndicator => "CB-PROC-002",
251 Self::ProcFilter => "CB-PROC-003",
252 Self::ProcOomScore => "CB-PROC-004",
253 Self::ProcNiceValue => "CB-PROC-005",
254 Self::ProcThreadCount => "CB-PROC-006",
255 Self::ProcCgroup => "CB-PROC-007",
256 }
257 }
258
259 #[must_use]
261 pub const fn simd_vectorizable(&self) -> bool {
262 match self {
263 Self::CpuSparklines
265 | Self::CpuLoadTrend
266 | Self::CpuFrequency
267 | Self::CpuTemperature
268 | Self::CpuTopConsumers
269 | Self::MemSparklines
270 | Self::MemPressureGauge
271 | Self::MemSwapThrashing
272 | Self::ConnAge
273 | Self::ConnGeo
274 | Self::ConnLatency
275 | Self::ConnService
276 | Self::ConnHotIndicator
277 | Self::ConnSparkline
278 | Self::NetSparklines
279 | Self::NetProtocolStats
280 | Self::NetErrorRate
281 | Self::NetDropRate
282 | Self::NetBandwidthUtil
283 | Self::ProcOomScore
284 | Self::ProcNiceValue
285 | Self::ProcThreadCount => true,
286
287 Self::CpuLoadGauge
289 | Self::CpuBoostIndicator
290 | Self::MemZramRatio
291 | Self::MemCacheBreakdown
292 | Self::MemHugePages
293 | Self::ConnProc
294 | Self::NetLatencyGauge
295 | Self::ProcTreeView
296 | Self::ProcSortIndicator
297 | Self::ProcFilter
298 | Self::ProcCgroup => false,
299 }
300 }
301}
302
303#[derive(Debug, Clone)]
308#[allow(dead_code)]
309pub struct SparklineBlock {
310 history: Vec<f32>,
312 max_samples: usize,
314 simd_buffer: [f32; 8],
316 instruction_set: SimdInstructionSet,
318}
319
320impl Default for SparklineBlock {
321 fn default() -> Self {
322 Self::new(60)
323 }
324}
325
326impl SparklineBlock {
327 #[must_use]
329 pub fn new(max_samples: usize) -> Self {
330 debug_assert!(max_samples > 0, "max_samples must be positive");
331 Self {
332 history: Vec::with_capacity(max_samples),
333 max_samples,
334 simd_buffer: [0.0; 8],
335 instruction_set: SimdInstructionSet::detect(),
336 }
337 }
338
339 pub fn push(&mut self, value: f32) {
341 if self.history.len() >= self.max_samples {
342 self.history.remove(0);
343 }
344 self.history.push(value);
345 }
346
347 #[must_use]
349 pub fn history(&self) -> &[f32] {
350 &self.history
351 }
352
353 #[must_use]
355 pub fn render(&self, width: usize) -> Vec<char> {
356 if self.history.is_empty() {
357 return vec![' '; width];
358 }
359
360 let (min, max) = self.find_min_max();
362 let range = max - min;
363
364 let samples = self.sample_to_width(width);
366
367 #[allow(clippy::items_after_statements)]
369 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
370
371 samples
372 .iter()
373 .map(|&v| {
374 if range < f32::EPSILON {
375 BLOCKS[4] } else {
377 let normalized = ((v - min) / range).clamp(0.0, 1.0);
378 let idx = (normalized * 7.0) as usize;
379 BLOCKS[idx.min(7)]
380 }
381 })
382 .collect()
383 }
384
385 fn find_min_max(&self) -> (f32, f32) {
387 if self.history.is_empty() {
388 return (0.0, 1.0);
389 }
390
391 let min = self.history.iter().copied().fold(f32::INFINITY, f32::min);
394 let max = self
395 .history
396 .iter()
397 .copied()
398 .fold(f32::NEG_INFINITY, f32::max);
399
400 (min, max)
401 }
402
403 fn sample_to_width(&self, width: usize) -> Vec<f32> {
405 if self.history.len() <= width {
406 let mut result = vec![0.0; width - self.history.len()];
408 result.extend_from_slice(&self.history);
409 result
410 } else {
411 let step = self.history.len() as f32 / width as f32;
413 (0..width)
414 .map(|i| {
415 let idx = (i as f32 * step) as usize;
416 self.history[idx.min(self.history.len() - 1)]
417 })
418 .collect()
419 }
420 }
421}
422
423impl ComputeBlock for SparklineBlock {
424 type Input = f32;
425 type Output = Vec<char>;
426
427 fn compute(&mut self, input: &Self::Input) -> Self::Output {
428 self.push(*input);
429 self.render(self.max_samples.min(60))
430 }
431
432 fn simd_instruction_set(&self) -> SimdInstructionSet {
433 self.instruction_set
434 }
435
436 fn latency_budget_us(&self) -> u64 {
437 100 }
439}
440
441#[derive(Debug, Clone)]
445pub struct LoadTrendBlock {
446 history: Vec<f32>,
448 window_size: usize,
450}
451
452impl Default for LoadTrendBlock {
453 fn default() -> Self {
454 Self::new(5)
455 }
456}
457
458impl LoadTrendBlock {
459 #[must_use]
461 pub fn new(window_size: usize) -> Self {
462 debug_assert!(window_size > 0, "window_size must be positive");
463 Self {
464 history: Vec::with_capacity(window_size),
465 window_size,
466 }
467 }
468
469 #[must_use]
471 pub fn trend(&self) -> TrendDirection {
472 if self.history.len() < 2 {
473 return TrendDirection::Flat;
474 }
475
476 let recent = self.history.iter().rev().take(self.window_size);
477 let diffs: Vec<f32> = recent
478 .clone()
479 .zip(recent.skip(1))
480 .map(|(a, b)| a - b)
481 .collect();
482
483 if diffs.is_empty() {
484 return TrendDirection::Flat;
485 }
486
487 let avg_diff: f32 = diffs.iter().sum::<f32>() / diffs.len() as f32;
488
489 #[allow(clippy::items_after_statements)]
490 const THRESHOLD: f32 = 0.05;
491 if avg_diff > THRESHOLD {
492 TrendDirection::Up
493 } else if avg_diff < -THRESHOLD {
494 TrendDirection::Down
495 } else {
496 TrendDirection::Flat
497 }
498 }
499}
500
501impl ComputeBlock for LoadTrendBlock {
502 type Input = f32;
503 type Output = TrendDirection;
504
505 fn compute(&mut self, input: &Self::Input) -> Self::Output {
506 if self.history.len() >= self.window_size * 2 {
507 self.history.remove(0);
508 }
509 self.history.push(*input);
510 self.trend()
511 }
512
513 fn latency_budget_us(&self) -> u64 {
514 10 }
516}
517
518#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
520pub enum TrendDirection {
521 Up,
523 Down,
525 #[default]
527 Flat,
528}
529
530impl TrendDirection {
531 #[must_use]
533 pub const fn arrow(self) -> char {
534 match self {
535 Self::Up => '↑',
536 Self::Down => '↓',
537 Self::Flat => '→',
538 }
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn test_simd_instruction_set_detect() {
548 let isa = SimdInstructionSet::detect();
549 assert!(isa.vector_width() >= 1);
551 }
552
553 #[test]
554 fn test_simd_instruction_set_names() {
555 assert_eq!(SimdInstructionSet::Scalar.name(), "Scalar");
556 assert_eq!(SimdInstructionSet::Avx2.name(), "AVX2");
557 assert_eq!(SimdInstructionSet::Neon.name(), "NEON");
558 }
559
560 #[test]
561 fn test_simd_vector_widths() {
562 assert_eq!(SimdInstructionSet::Scalar.vector_width(), 1);
563 assert_eq!(SimdInstructionSet::Sse4.vector_width(), 4);
564 assert_eq!(SimdInstructionSet::Avx2.vector_width(), 8);
565 assert_eq!(SimdInstructionSet::Avx512.vector_width(), 16);
566 }
567
568 #[test]
569 fn test_compute_block_id_strings() {
570 assert_eq!(ComputeBlockId::CpuSparklines.id_string(), "CB-CPU-001");
571 assert_eq!(ComputeBlockId::MemSparklines.id_string(), "CB-MEM-001");
572 assert_eq!(ComputeBlockId::ConnAge.id_string(), "CB-CONN-001");
573 assert_eq!(ComputeBlockId::NetSparklines.id_string(), "CB-NET-001");
574 assert_eq!(ComputeBlockId::ProcTreeView.id_string(), "CB-PROC-001");
575 }
576
577 #[test]
578 fn test_compute_block_simd_vectorizable() {
579 assert!(ComputeBlockId::CpuSparklines.simd_vectorizable());
580 assert!(ComputeBlockId::MemSparklines.simd_vectorizable());
581 assert!(!ComputeBlockId::CpuLoadGauge.simd_vectorizable());
582 assert!(!ComputeBlockId::ProcTreeView.simd_vectorizable());
583 }
584
585 #[test]
586 fn test_sparkline_block_new() {
587 let block = SparklineBlock::new(60);
588 assert!(block.history().is_empty());
589 }
590
591 #[test]
592 fn test_sparkline_block_push() {
593 let mut block = SparklineBlock::new(5);
594 for i in 0..10 {
595 block.push(i as f32);
596 }
597 assert_eq!(block.history().len(), 5);
599 assert_eq!(block.history(), &[5.0, 6.0, 7.0, 8.0, 9.0]);
600 }
601
602 #[test]
603 fn test_sparkline_block_render() {
604 let mut block = SparklineBlock::new(8);
605 for v in [0.0, 25.0, 50.0, 75.0, 100.0] {
606 block.push(v);
607 }
608 let rendered = block.render(5);
609 assert_eq!(rendered.len(), 5);
610 assert_eq!(rendered[0], '▁');
612 assert_eq!(rendered[4], '█');
613 }
614
615 #[test]
616 fn test_sparkline_block_empty() {
617 let block = SparklineBlock::new(8);
618 let rendered = block.render(5);
619 assert_eq!(rendered, vec![' '; 5]);
620 }
621
622 #[test]
623 fn test_sparkline_block_compute() {
624 let mut block = SparklineBlock::new(8);
625 let output = block.compute(&50.0);
626 assert!(!output.is_empty());
627 }
628
629 #[test]
630 fn test_sparkline_block_simd_supported() {
631 let block = SparklineBlock::default();
632 let _ = block.simd_supported();
634 let _ = block.simd_instruction_set();
635 }
636
637 #[test]
638 fn test_load_trend_block_new() {
639 let block = LoadTrendBlock::new(5);
640 assert_eq!(block.trend(), TrendDirection::Flat);
641 }
642
643 #[test]
644 fn test_load_trend_block_up() {
645 let mut block = LoadTrendBlock::new(3);
646 for v in [1.0, 2.0, 3.0, 4.0, 5.0] {
647 block.compute(&v);
648 }
649 assert_eq!(block.trend(), TrendDirection::Up);
650 }
651
652 #[test]
653 fn test_load_trend_block_down() {
654 let mut block = LoadTrendBlock::new(3);
655 for v in [5.0, 4.0, 3.0, 2.0, 1.0] {
656 block.compute(&v);
657 }
658 assert_eq!(block.trend(), TrendDirection::Down);
659 }
660
661 #[test]
662 fn test_load_trend_block_flat() {
663 let mut block = LoadTrendBlock::new(3);
664 for v in [5.0, 5.0, 5.0, 5.0, 5.0] {
665 block.compute(&v);
666 }
667 assert_eq!(block.trend(), TrendDirection::Flat);
668 }
669
670 #[test]
671 fn test_trend_direction_arrows() {
672 assert_eq!(TrendDirection::Up.arrow(), '↑');
673 assert_eq!(TrendDirection::Down.arrow(), '↓');
674 assert_eq!(TrendDirection::Flat.arrow(), '→');
675 }
676
677 #[test]
678 fn test_latency_budgets() {
679 let sparkline = SparklineBlock::default();
680 assert!(sparkline.latency_budget_us() > 0);
681
682 let trend = LoadTrendBlock::default();
683 assert!(trend.latency_budget_us() > 0);
684 }
685
686 #[test]
687 fn test_simd_instruction_set_default() {
688 let isa = SimdInstructionSet::default();
689 assert!(isa.vector_width() >= 1);
690 }
691}
692
693#[derive(Debug, Clone)]
702pub struct CpuFrequencyBlock {
703 frequencies: Vec<u32>,
705 max_frequencies: Vec<u32>,
707 instruction_set: SimdInstructionSet,
709}
710
711impl Default for CpuFrequencyBlock {
712 fn default() -> Self {
713 Self::new()
714 }
715}
716
717impl CpuFrequencyBlock {
718 #[must_use]
720 pub fn new() -> Self {
721 Self {
722 frequencies: Vec::new(),
723 max_frequencies: Vec::new(),
724 instruction_set: SimdInstructionSet::detect(),
725 }
726 }
727
728 pub fn set_frequencies(&mut self, freqs: Vec<u32>, max_freqs: Vec<u32>) {
730 self.frequencies = freqs;
731 self.max_frequencies = max_freqs;
732 }
733
734 #[must_use]
736 pub fn frequency_percentages(&self) -> Vec<f32> {
737 self.frequencies
738 .iter()
739 .zip(self.max_frequencies.iter())
740 .map(|(&cur, &max)| {
741 if max > 0 {
742 (cur as f32 / max as f32 * 100.0).clamp(0.0, 100.0)
743 } else {
744 0.0
745 }
746 })
747 .collect()
748 }
749
750 #[must_use]
752 pub fn scaling_indicators(&self) -> Vec<FrequencyScalingState> {
753 self.frequency_percentages()
754 .iter()
755 .map(|&pct| {
756 if pct >= 95.0 {
757 FrequencyScalingState::Turbo
758 } else if pct >= 75.0 {
759 FrequencyScalingState::High
760 } else if pct >= 50.0 {
761 FrequencyScalingState::Normal
762 } else if pct >= 25.0 {
763 FrequencyScalingState::Scaled
764 } else {
765 FrequencyScalingState::Idle
766 }
767 })
768 .collect()
769 }
770}
771
772impl ComputeBlock for CpuFrequencyBlock {
773 type Input = (Vec<u32>, Vec<u32>); type Output = Vec<FrequencyScalingState>;
775
776 fn compute(&mut self, input: &Self::Input) -> Self::Output {
777 self.set_frequencies(input.0.clone(), input.1.clone());
778 self.scaling_indicators()
779 }
780
781 fn simd_instruction_set(&self) -> SimdInstructionSet {
782 self.instruction_set
783 }
784
785 fn latency_budget_us(&self) -> u64 {
786 50 }
788}
789
790#[derive(Debug, Clone, Copy, PartialEq, Eq)]
792pub enum FrequencyScalingState {
793 Turbo,
795 High,
797 Normal,
799 Scaled,
801 Idle,
803}
804
805impl FrequencyScalingState {
806 #[must_use]
808 pub const fn indicator(self) -> char {
809 match self {
810 Self::Turbo => '⚡',
811 Self::High => '↑',
812 Self::Normal => '→',
813 Self::Scaled => '↓',
814 Self::Idle => '·',
815 }
816 }
817}
818
819#[derive(Debug, Clone)]
823pub struct CpuGovernorBlock {
824 governor: CpuGovernor,
826}
827
828impl Default for CpuGovernorBlock {
829 fn default() -> Self {
830 Self::new()
831 }
832}
833
834impl CpuGovernorBlock {
835 #[must_use]
837 pub fn new() -> Self {
838 Self {
839 governor: CpuGovernor::Unknown,
840 }
841 }
842
843 pub fn set_governor(&mut self, name: &str) {
845 self.governor = CpuGovernor::from_name(name);
846 }
847
848 #[must_use]
850 pub fn governor(&self) -> CpuGovernor {
851 self.governor
852 }
853}
854
855impl ComputeBlock for CpuGovernorBlock {
856 type Input = String;
857 type Output = CpuGovernor;
858
859 fn compute(&mut self, input: &Self::Input) -> Self::Output {
860 self.set_governor(input);
861 self.governor
862 }
863
864 fn latency_budget_us(&self) -> u64 {
865 10 }
867}
868
869#[derive(Debug, Clone, Copy, PartialEq, Eq)]
871pub enum CpuGovernor {
872 Performance,
874 Powersave,
876 Ondemand,
878 Conservative,
880 Schedutil,
882 Userspace,
884 Unknown,
886}
887
888impl CpuGovernor {
889 #[must_use]
891 pub fn from_name(name: &str) -> Self {
892 match name.trim().to_lowercase().as_str() {
893 "performance" => Self::Performance,
894 "powersave" => Self::Powersave,
895 "ondemand" => Self::Ondemand,
896 "conservative" => Self::Conservative,
897 "schedutil" => Self::Schedutil,
898 "userspace" => Self::Userspace,
899 _ => Self::Unknown,
900 }
901 }
902
903 #[must_use]
905 pub const fn as_str(&self) -> &'static str {
906 match self {
907 Self::Performance => "performance",
908 Self::Powersave => "powersave",
909 Self::Ondemand => "ondemand",
910 Self::Conservative => "conservative",
911 Self::Schedutil => "schedutil",
912 Self::Userspace => "userspace",
913 Self::Unknown => "unknown",
914 }
915 }
916
917 #[must_use]
919 pub const fn short_name(self) -> &'static str {
920 match self {
921 Self::Performance => "perf",
922 Self::Powersave => "psav",
923 Self::Ondemand => "odmd",
924 Self::Conservative => "cons",
925 Self::Schedutil => "schu",
926 Self::Userspace => "user",
927 Self::Unknown => "????",
928 }
929 }
930
931 #[must_use]
933 pub const fn icon(self) -> char {
934 match self {
935 Self::Performance => '🚀',
936 Self::Powersave => '🔋',
937 Self::Ondemand => '⚡',
938 Self::Conservative => '📊',
939 Self::Schedutil => '📅',
940 Self::Userspace => '👤',
941 Self::Unknown => '?',
942 }
943 }
944}
945
946#[derive(Debug, Clone)]
950pub struct MemPressureBlock {
951 avg10_some: f32,
953 avg60_some: f32,
955 avg300_some: f32,
957 avg10_full: f32,
959 instruction_set: SimdInstructionSet,
961}
962
963impl Default for MemPressureBlock {
964 fn default() -> Self {
965 Self::new()
966 }
967}
968
969impl MemPressureBlock {
970 #[must_use]
972 pub fn new() -> Self {
973 Self {
974 avg10_some: 0.0,
975 avg60_some: 0.0,
976 avg300_some: 0.0,
977 avg10_full: 0.0,
978 instruction_set: SimdInstructionSet::detect(),
979 }
980 }
981
982 pub fn set_pressure(
984 &mut self,
985 avg10_some: f32,
986 avg60_some: f32,
987 avg300_some: f32,
988 avg10_full: f32,
989 ) {
990 debug_assert!(avg10_some >= 0.0, "avg10_some must be non-negative");
991 debug_assert!(avg60_some >= 0.0, "avg60_some must be non-negative");
992 debug_assert!(avg300_some >= 0.0, "avg300_some must be non-negative");
993 debug_assert!(avg10_full >= 0.0, "avg10_full must be non-negative");
994 self.avg10_some = avg10_some;
995 self.avg60_some = avg60_some;
996 self.avg300_some = avg300_some;
997 self.avg10_full = avg10_full;
998 }
999
1000 #[must_use]
1002 pub fn pressure_level(&self) -> MemoryPressureLevel {
1003 let pct = self.avg10_some;
1004 if pct >= 50.0 {
1005 MemoryPressureLevel::Critical
1006 } else if pct >= 25.0 {
1007 MemoryPressureLevel::High
1008 } else if pct >= 10.0 {
1009 MemoryPressureLevel::Medium
1010 } else if pct >= 1.0 {
1011 MemoryPressureLevel::Low
1012 } else {
1013 MemoryPressureLevel::None
1014 }
1015 }
1016
1017 #[must_use]
1019 pub fn trend(&self) -> TrendDirection {
1020 let diff = self.avg10_some - self.avg300_some;
1021 if diff > 5.0 {
1022 TrendDirection::Up
1023 } else if diff < -5.0 {
1024 TrendDirection::Down
1025 } else {
1026 TrendDirection::Flat
1027 }
1028 }
1029}
1030
1031impl ComputeBlock for MemPressureBlock {
1032 type Input = (f32, f32, f32, f32); type Output = MemoryPressureLevel;
1034
1035 fn compute(&mut self, input: &Self::Input) -> Self::Output {
1036 self.set_pressure(input.0, input.1, input.2, input.3);
1037 self.pressure_level()
1038 }
1039
1040 fn simd_instruction_set(&self) -> SimdInstructionSet {
1041 self.instruction_set
1042 }
1043
1044 fn latency_budget_us(&self) -> u64 {
1045 20 }
1047}
1048
1049#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1051pub enum MemoryPressureLevel {
1052 None,
1054 Low,
1056 Medium,
1058 High,
1060 Critical,
1062}
1063
1064impl MemoryPressureLevel {
1065 #[allow(clippy::match_same_arms)]
1067 pub fn symbol(&self) -> char {
1068 match self {
1069 Self::None => ' ',
1070 Self::Low => '○',
1071 Self::Medium => '◐',
1072 Self::High => '◕',
1073 Self::Critical => '●',
1074 }
1075 }
1076
1077 #[must_use]
1079 pub const fn severity(self) -> u8 {
1080 match self {
1081 Self::None => 0,
1082 Self::Low => 1,
1083 Self::Medium => 2,
1084 Self::High => 3,
1085 Self::Critical => 4,
1086 }
1087 }
1088}
1089
1090#[derive(Debug, Clone)]
1094pub struct HugePagesBlock {
1095 total: u64,
1097 free: u64,
1099 reserved: u64,
1101 page_size_kb: u64,
1103}
1104
1105impl Default for HugePagesBlock {
1106 fn default() -> Self {
1107 Self::new()
1108 }
1109}
1110
1111impl HugePagesBlock {
1112 #[must_use]
1114 pub fn new() -> Self {
1115 Self {
1116 total: 0,
1117 free: 0,
1118 reserved: 0,
1119 page_size_kb: 2048, }
1121 }
1122
1123 pub fn set_values(&mut self, total: u64, free: u64, reserved: u64, page_size_kb: u64) {
1125 debug_assert!(free <= total, "free must be <= total");
1126 debug_assert!(page_size_kb > 0, "page_size_kb must be positive");
1127 self.total = total;
1128 self.free = free;
1129 self.reserved = reserved;
1130 self.page_size_kb = page_size_kb;
1131 }
1132
1133 #[must_use]
1135 pub fn usage_percent(&self) -> f32 {
1136 if self.total == 0 {
1137 0.0
1138 } else {
1139 ((self.total - self.free) as f32 / self.total as f32 * 100.0).clamp(0.0, 100.0)
1140 }
1141 }
1142
1143 #[must_use]
1145 pub fn total_bytes(&self) -> u64 {
1146 self.total * self.page_size_kb * 1024
1147 }
1148
1149 #[must_use]
1151 pub fn used_bytes(&self) -> u64 {
1152 (self.total - self.free) * self.page_size_kb * 1024
1153 }
1154}
1155
1156impl ComputeBlock for HugePagesBlock {
1157 type Input = (u64, u64, u64, u64); type Output = f32; fn compute(&mut self, input: &Self::Input) -> Self::Output {
1161 self.set_values(input.0, input.1, input.2, input.3);
1162 self.usage_percent()
1163 }
1164
1165 fn latency_budget_us(&self) -> u64 {
1166 10 }
1168}
1169
1170#[derive(Debug, Clone)]
1174pub struct GpuThermalBlock {
1175 temperature_c: f32,
1177 power_w: f32,
1179 power_limit_w: f32,
1181 temp_history: Vec<f32>,
1183 instruction_set: SimdInstructionSet,
1185}
1186
1187impl Default for GpuThermalBlock {
1188 fn default() -> Self {
1189 Self::new()
1190 }
1191}
1192
1193impl GpuThermalBlock {
1194 #[must_use]
1196 pub fn new() -> Self {
1197 Self {
1198 temperature_c: 0.0,
1199 power_w: 0.0,
1200 power_limit_w: 0.0,
1201 temp_history: Vec::with_capacity(60),
1202 instruction_set: SimdInstructionSet::detect(),
1203 }
1204 }
1205
1206 pub fn set_values(&mut self, temp_c: f32, power_w: f32, power_limit_w: f32) {
1208 debug_assert!(power_w >= 0.0, "power_w must be non-negative");
1209 debug_assert!(power_limit_w >= 0.0, "power_limit_w must be non-negative");
1210 self.temperature_c = temp_c;
1211 self.power_w = power_w;
1212 self.power_limit_w = power_limit_w;
1213
1214 if self.temp_history.len() >= 60 {
1216 self.temp_history.remove(0);
1217 }
1218 self.temp_history.push(temp_c);
1219 }
1220
1221 #[must_use]
1223 pub fn thermal_state(&self) -> GpuThermalState {
1224 if self.temperature_c >= 90.0 {
1225 GpuThermalState::Critical
1226 } else if self.temperature_c >= 80.0 {
1227 GpuThermalState::Hot
1228 } else if self.temperature_c >= 70.0 {
1229 GpuThermalState::Warm
1230 } else if self.temperature_c >= 50.0 {
1231 GpuThermalState::Normal
1232 } else {
1233 GpuThermalState::Cool
1234 }
1235 }
1236
1237 #[must_use]
1239 pub fn power_percent(&self) -> f32 {
1240 if self.power_limit_w > 0.0 {
1241 (self.power_w / self.power_limit_w * 100.0).clamp(0.0, 100.0)
1242 } else {
1243 0.0
1244 }
1245 }
1246
1247 #[must_use]
1249 pub fn trend(&self) -> TrendDirection {
1250 if self.temp_history.len() < 5 {
1251 return TrendDirection::Flat;
1252 }
1253
1254 let recent: f32 = self.temp_history.iter().rev().take(5).sum::<f32>() / 5.0;
1255 let older: f32 = self.temp_history.iter().rev().skip(5).take(5).sum::<f32>() / 5.0;
1256
1257 let diff = recent - older;
1258 if diff > 2.0 {
1259 TrendDirection::Up
1260 } else if diff < -2.0 {
1261 TrendDirection::Down
1262 } else {
1263 TrendDirection::Flat
1264 }
1265 }
1266}
1267
1268impl ComputeBlock for GpuThermalBlock {
1269 type Input = (f32, f32, f32); type Output = GpuThermalState;
1271
1272 fn compute(&mut self, input: &Self::Input) -> Self::Output {
1273 self.set_values(input.0, input.1, input.2);
1274 self.thermal_state()
1275 }
1276
1277 fn simd_instruction_set(&self) -> SimdInstructionSet {
1278 self.instruction_set
1279 }
1280
1281 fn latency_budget_us(&self) -> u64 {
1282 30 }
1284}
1285
1286#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1288pub enum GpuThermalState {
1289 #[default]
1291 Cool,
1292 Normal,
1294 Warm,
1296 Hot,
1298 Critical,
1300}
1301
1302impl GpuThermalState {
1303 #[must_use]
1305 pub const fn indicator(self) -> char {
1306 match self {
1307 Self::Cool => '❄',
1308 Self::Normal => '●',
1309 Self::Warm => '◐',
1310 Self::Hot => '◕',
1311 Self::Critical => '🔥',
1312 }
1313 }
1314
1315 #[must_use]
1317 pub const fn severity(self) -> u8 {
1318 match self {
1319 Self::Cool => 0,
1320 Self::Normal => 1,
1321 Self::Warm => 2,
1322 Self::Hot => 3,
1323 Self::Critical => 4,
1324 }
1325 }
1326}
1327
1328#[derive(Debug, Clone)]
1332pub struct GpuVramBlock {
1333 total_mb: u64,
1335 used_mb: u64,
1337 per_process: Vec<(u32, u64, String)>, }
1340
1341impl Default for GpuVramBlock {
1342 fn default() -> Self {
1343 Self::new()
1344 }
1345}
1346
1347impl GpuVramBlock {
1348 #[must_use]
1350 pub fn new() -> Self {
1351 Self {
1352 total_mb: 0,
1353 used_mb: 0,
1354 per_process: Vec::new(),
1355 }
1356 }
1357
1358 pub fn set_values(
1360 &mut self,
1361 total_mb: u64,
1362 used_mb: u64,
1363 per_process: Vec<(u32, u64, String)>,
1364 ) {
1365 self.total_mb = total_mb;
1366 self.used_mb = used_mb;
1367 self.per_process = per_process;
1368 }
1369
1370 #[must_use]
1372 pub fn usage_percent(&self) -> f32 {
1373 if self.total_mb == 0 {
1374 0.0
1375 } else {
1376 (self.used_mb as f32 / self.total_mb as f32 * 100.0).clamp(0.0, 100.0)
1377 }
1378 }
1379
1380 #[must_use]
1382 pub fn top_consumers(&self, n: usize) -> Vec<&(u32, u64, String)> {
1383 let mut sorted: Vec<_> = self.per_process.iter().collect();
1384 sorted.sort_by(|a, b| b.1.cmp(&a.1));
1385 sorted.into_iter().take(n).collect()
1386 }
1387}
1388
1389impl ComputeBlock for GpuVramBlock {
1390 type Input = (u64, u64, Vec<(u32, u64, String)>);
1391 type Output = f32; fn compute(&mut self, input: &Self::Input) -> Self::Output {
1394 self.set_values(input.0, input.1, input.2.clone());
1395 self.usage_percent()
1396 }
1397
1398 fn latency_budget_us(&self) -> u64 {
1399 100 }
1401}
1402
1403#[cfg(test)]
1405mod new_block_tests {
1406 use super::*;
1407
1408 #[test]
1409 fn test_cpu_frequency_block_new() {
1410 let block = CpuFrequencyBlock::new();
1411 assert!(block.frequencies.is_empty());
1412 }
1413
1414 #[test]
1415 fn test_cpu_frequency_block_percentages() {
1416 let mut block = CpuFrequencyBlock::new();
1417 block.set_frequencies(vec![2000, 3000, 4000], vec![4000, 4000, 4000]);
1418 let pcts = block.frequency_percentages();
1419 assert_eq!(pcts.len(), 3);
1420 assert!((pcts[0] - 50.0).abs() < 0.1);
1421 assert!((pcts[1] - 75.0).abs() < 0.1);
1422 assert!((pcts[2] - 100.0).abs() < 0.1);
1423 }
1424
1425 #[test]
1426 fn test_cpu_frequency_block_scaling_states() {
1427 let mut block = CpuFrequencyBlock::new();
1428 block.set_frequencies(vec![1000, 2000, 3800, 4000], vec![4000, 4000, 4000, 4000]);
1429 let states = block.scaling_indicators();
1430 assert_eq!(states[0], FrequencyScalingState::Scaled);
1431 assert_eq!(states[1], FrequencyScalingState::Normal);
1432 assert_eq!(states[2], FrequencyScalingState::Turbo);
1433 assert_eq!(states[3], FrequencyScalingState::Turbo);
1434 }
1435
1436 #[test]
1437 fn test_frequency_scaling_state_indicators() {
1438 assert_eq!(FrequencyScalingState::Turbo.indicator(), '⚡');
1439 assert_eq!(FrequencyScalingState::High.indicator(), '↑');
1440 assert_eq!(FrequencyScalingState::Normal.indicator(), '→');
1441 assert_eq!(FrequencyScalingState::Scaled.indicator(), '↓');
1442 assert_eq!(FrequencyScalingState::Idle.indicator(), '·');
1443 }
1444
1445 #[test]
1446 fn test_cpu_governor_from_name() {
1447 assert_eq!(
1448 CpuGovernor::from_name("performance"),
1449 CpuGovernor::Performance
1450 );
1451 assert_eq!(CpuGovernor::from_name("powersave"), CpuGovernor::Powersave);
1452 assert_eq!(CpuGovernor::from_name("schedutil"), CpuGovernor::Schedutil);
1453 assert_eq!(CpuGovernor::from_name("unknown"), CpuGovernor::Unknown);
1454 }
1455
1456 #[test]
1457 fn test_cpu_governor_short_names() {
1458 assert_eq!(CpuGovernor::Performance.short_name(), "perf");
1459 assert_eq!(CpuGovernor::Powersave.short_name(), "psav");
1460 assert_eq!(CpuGovernor::Schedutil.short_name(), "schu");
1461 }
1462
1463 #[test]
1464 fn test_mem_pressure_level() {
1465 let mut block = MemPressureBlock::new();
1466 block.set_pressure(0.5, 0.3, 0.2, 0.1);
1467 assert_eq!(block.pressure_level(), MemoryPressureLevel::None);
1468
1469 block.set_pressure(5.0, 3.0, 2.0, 1.0);
1470 assert_eq!(block.pressure_level(), MemoryPressureLevel::Low);
1471
1472 block.set_pressure(15.0, 10.0, 8.0, 5.0);
1473 assert_eq!(block.pressure_level(), MemoryPressureLevel::Medium);
1474
1475 block.set_pressure(30.0, 20.0, 15.0, 10.0);
1476 assert_eq!(block.pressure_level(), MemoryPressureLevel::High);
1477
1478 block.set_pressure(60.0, 50.0, 40.0, 30.0);
1479 assert_eq!(block.pressure_level(), MemoryPressureLevel::Critical);
1480 }
1481
1482 #[test]
1483 fn test_mem_pressure_trend() {
1484 let mut block = MemPressureBlock::new();
1485 block.set_pressure(20.0, 15.0, 5.0, 10.0);
1486 assert_eq!(block.trend(), TrendDirection::Up);
1487
1488 block.set_pressure(5.0, 10.0, 20.0, 2.0);
1489 assert_eq!(block.trend(), TrendDirection::Down);
1490
1491 block.set_pressure(10.0, 10.0, 10.0, 5.0);
1492 assert_eq!(block.trend(), TrendDirection::Flat);
1493 }
1494
1495 #[test]
1496 fn test_huge_pages_block() {
1497 let mut block = HugePagesBlock::new();
1498 block.set_values(100, 50, 10, 2048);
1499 assert!((block.usage_percent() - 50.0).abs() < 0.1);
1500 assert_eq!(block.total_bytes(), 100 * 2048 * 1024);
1501 assert_eq!(block.used_bytes(), 50 * 2048 * 1024);
1502 }
1503
1504 #[test]
1505 fn test_huge_pages_block_empty() {
1506 let block = HugePagesBlock::new();
1507 assert_eq!(block.usage_percent(), 0.0);
1508 }
1509
1510 #[test]
1511 fn test_gpu_thermal_block() {
1512 let mut block = GpuThermalBlock::new();
1513 block.set_values(45.0, 100.0, 250.0);
1514 assert_eq!(block.thermal_state(), GpuThermalState::Cool);
1515 assert!((block.power_percent() - 40.0).abs() < 0.1);
1516
1517 block.set_values(75.0, 200.0, 250.0);
1518 assert_eq!(block.thermal_state(), GpuThermalState::Warm);
1519
1520 block.set_values(95.0, 250.0, 250.0);
1521 assert_eq!(block.thermal_state(), GpuThermalState::Critical);
1522 }
1523
1524 #[test]
1525 fn test_gpu_thermal_state_indicators() {
1526 assert_eq!(GpuThermalState::Cool.indicator(), '❄');
1527 assert_eq!(GpuThermalState::Normal.indicator(), '●');
1528 assert_eq!(GpuThermalState::Critical.indicator(), '🔥');
1529 }
1530
1531 #[test]
1532 fn test_gpu_vram_block() {
1533 let mut block = GpuVramBlock::new();
1534 let procs = vec![
1535 (1234, 1024, "firefox".to_string()),
1536 (5678, 512, "code".to_string()),
1537 (9012, 2048, "blender".to_string()),
1538 ];
1539 block.set_values(8192, 4096, procs);
1540 assert!((block.usage_percent() - 50.0).abs() < 0.1);
1541
1542 let top = block.top_consumers(2);
1543 assert_eq!(top.len(), 2);
1544 assert_eq!(top[0].2, "blender");
1545 assert_eq!(top[1].2, "firefox");
1546 }
1547
1548 #[test]
1549 fn test_memory_pressure_level_severity() {
1550 assert_eq!(MemoryPressureLevel::None.severity(), 0);
1551 assert_eq!(MemoryPressureLevel::Low.severity(), 1);
1552 assert_eq!(MemoryPressureLevel::Medium.severity(), 2);
1553 assert_eq!(MemoryPressureLevel::High.severity(), 3);
1554 assert_eq!(MemoryPressureLevel::Critical.severity(), 4);
1555 }
1556
1557 #[test]
1558 fn test_gpu_thermal_state_severity() {
1559 assert_eq!(GpuThermalState::Cool.severity(), 0);
1560 assert_eq!(GpuThermalState::Normal.severity(), 1);
1561 assert_eq!(GpuThermalState::Warm.severity(), 2);
1562 assert_eq!(GpuThermalState::Hot.severity(), 3);
1563 assert_eq!(GpuThermalState::Critical.severity(), 4);
1564 }
1565
1566 #[test]
1567 fn test_cpu_frequency_block_compute() {
1568 let mut block = CpuFrequencyBlock::new();
1569 let input = (vec![2000, 4000], vec![4000, 4000]);
1570 let output = block.compute(&input);
1571 assert_eq!(output.len(), 2);
1572 assert_eq!(output[0], FrequencyScalingState::Normal);
1573 assert_eq!(output[1], FrequencyScalingState::Turbo);
1574 }
1575
1576 #[test]
1577 fn test_cpu_governor_block_compute() {
1578 let mut block = CpuGovernorBlock::new();
1579 let output = block.compute(&"performance".to_string());
1580 assert_eq!(output, CpuGovernor::Performance);
1581 }
1582
1583 #[test]
1584 fn test_mem_pressure_block_compute() {
1585 let mut block = MemPressureBlock::new();
1586 let input = (30.0_f32, 25.0_f32, 20.0_f32, 15.0_f32);
1587 let output = block.compute(&input);
1588 assert_eq!(output, MemoryPressureLevel::High);
1589 }
1590
1591 #[test]
1592 fn test_huge_pages_block_compute() {
1593 let mut block = HugePagesBlock::new();
1594 let input = (100_u64, 75_u64, 5_u64, 2048_u64);
1595 let output = block.compute(&input);
1596 assert!((output - 25.0).abs() < 0.1);
1597 }
1598
1599 #[test]
1600 fn test_gpu_thermal_block_compute() {
1601 let mut block = GpuThermalBlock::new();
1602 let input = (85.0_f32, 200.0_f32, 250.0_f32);
1603 let output = block.compute(&input);
1604 assert_eq!(output, GpuThermalState::Hot);
1605 }
1606
1607 #[test]
1608 fn test_gpu_vram_block_compute() {
1609 let mut block = GpuVramBlock::new();
1610 let procs = vec![(1234_u32, 1024_u64, "test".to_string())];
1611 let input = (8192_u64, 4096_u64, procs);
1612 let output = block.compute(&input);
1613 assert!((output - 50.0).abs() < 0.1);
1614 }
1615
1616 #[test]
1617 fn test_latency_budgets_new_blocks() {
1618 assert!(CpuFrequencyBlock::new().latency_budget_us() > 0);
1619 assert!(CpuGovernorBlock::new().latency_budget_us() > 0);
1620 assert!(MemPressureBlock::new().latency_budget_us() > 0);
1621 assert!(HugePagesBlock::new().latency_budget_us() > 0);
1622 assert!(GpuThermalBlock::new().latency_budget_us() > 0);
1623 assert!(GpuVramBlock::new().latency_budget_us() > 0);
1624 }
1625
1626 #[test]
1627 fn test_cpu_governor_icons() {
1628 assert_eq!(CpuGovernor::Performance.icon(), '🚀');
1629 assert_eq!(CpuGovernor::Powersave.icon(), '🔋');
1630 assert_eq!(CpuGovernor::Unknown.icon(), '?');
1631 }
1632
1633 #[test]
1634 fn test_simd_all_names() {
1635 assert_eq!(SimdInstructionSet::Sse4.name(), "SSE4.1");
1636 assert_eq!(SimdInstructionSet::Avx512.name(), "AVX-512");
1637 assert_eq!(SimdInstructionSet::WasmSimd128.name(), "WASM SIMD128");
1638 }
1639
1640 #[test]
1641 fn test_simd_wasm_vector_width() {
1642 assert_eq!(SimdInstructionSet::Neon.vector_width(), 4);
1643 assert_eq!(SimdInstructionSet::WasmSimd128.vector_width(), 4);
1644 }
1645
1646 #[test]
1647 fn test_compute_block_id_all_strings() {
1648 let ids = [
1650 ComputeBlockId::CpuLoadGauge,
1651 ComputeBlockId::CpuLoadTrend,
1652 ComputeBlockId::CpuFrequency,
1653 ComputeBlockId::CpuBoostIndicator,
1654 ComputeBlockId::CpuTemperature,
1655 ComputeBlockId::CpuTopConsumers,
1656 ComputeBlockId::MemZramRatio,
1657 ComputeBlockId::MemPressureGauge,
1658 ComputeBlockId::MemSwapThrashing,
1659 ComputeBlockId::MemCacheBreakdown,
1660 ComputeBlockId::MemHugePages,
1661 ComputeBlockId::ConnProc,
1662 ComputeBlockId::ConnGeo,
1663 ComputeBlockId::ConnLatency,
1664 ComputeBlockId::ConnService,
1665 ComputeBlockId::ConnHotIndicator,
1666 ComputeBlockId::ConnSparkline,
1667 ComputeBlockId::NetProtocolStats,
1668 ComputeBlockId::NetErrorRate,
1669 ComputeBlockId::NetDropRate,
1670 ComputeBlockId::NetLatencyGauge,
1671 ComputeBlockId::NetBandwidthUtil,
1672 ComputeBlockId::ProcSortIndicator,
1673 ComputeBlockId::ProcFilter,
1674 ComputeBlockId::ProcOomScore,
1675 ComputeBlockId::ProcNiceValue,
1676 ComputeBlockId::ProcThreadCount,
1677 ComputeBlockId::ProcCgroup,
1678 ];
1679 for id in ids {
1680 assert!(!id.id_string().is_empty());
1681 }
1682 }
1683
1684 #[test]
1685 fn test_compute_block_id_simd_categories() {
1686 assert!(ComputeBlockId::NetSparklines.simd_vectorizable());
1688 assert!(ComputeBlockId::NetProtocolStats.simd_vectorizable());
1689 assert!(ComputeBlockId::NetErrorRate.simd_vectorizable());
1690 assert!(ComputeBlockId::NetDropRate.simd_vectorizable());
1691 assert!(ComputeBlockId::NetBandwidthUtil.simd_vectorizable());
1692 assert!(ComputeBlockId::ConnAge.simd_vectorizable());
1693 assert!(ComputeBlockId::ConnGeo.simd_vectorizable());
1694 assert!(ComputeBlockId::ConnLatency.simd_vectorizable());
1695 assert!(ComputeBlockId::ConnService.simd_vectorizable());
1696 assert!(ComputeBlockId::ConnHotIndicator.simd_vectorizable());
1697 assert!(ComputeBlockId::ConnSparkline.simd_vectorizable());
1698
1699 assert!(!ComputeBlockId::MemZramRatio.simd_vectorizable());
1701 assert!(!ComputeBlockId::MemCacheBreakdown.simd_vectorizable());
1702 assert!(!ComputeBlockId::MemHugePages.simd_vectorizable());
1703 assert!(!ComputeBlockId::ConnProc.simd_vectorizable());
1704 assert!(!ComputeBlockId::NetLatencyGauge.simd_vectorizable());
1705 assert!(!ComputeBlockId::ProcSortIndicator.simd_vectorizable());
1706 assert!(!ComputeBlockId::ProcFilter.simd_vectorizable());
1707 assert!(!ComputeBlockId::ProcCgroup.simd_vectorizable());
1708 }
1709
1710 #[test]
1711 fn test_sparkline_block_default() {
1712 let block = SparklineBlock::default();
1713 assert!(block.history().is_empty());
1714 assert_eq!(block.max_samples, 60);
1715 }
1716
1717 #[test]
1718 fn test_sparkline_block_render_uniform() {
1719 let mut block = SparklineBlock::new(5);
1720 for _ in 0..5 {
1721 block.push(50.0);
1722 }
1723 let rendered = block.render(5);
1724 for ch in &rendered {
1726 assert_ne!(*ch, ' ');
1727 }
1728 }
1729
1730 #[test]
1731 fn test_sparkline_block_sample_to_width_shorter() {
1732 let mut block = SparklineBlock::new(10);
1733 for i in 0..3 {
1734 block.push(i as f32);
1735 }
1736 let rendered = block.render(5);
1738 assert_eq!(rendered.len(), 5);
1739 }
1740
1741 #[test]
1742 fn test_sparkline_block_sample_to_width_longer() {
1743 let mut block = SparklineBlock::new(20);
1744 for i in 0..15 {
1745 block.push(i as f32 * 10.0);
1746 }
1747 let rendered = block.render(5);
1749 assert_eq!(rendered.len(), 5);
1750 }
1751
1752 #[test]
1753 fn test_load_trend_block_default() {
1754 let block = LoadTrendBlock::default();
1755 assert_eq!(block.window_size, 5);
1756 }
1757
1758 #[test]
1759 fn test_load_trend_block_history_limit() {
1760 let mut block = LoadTrendBlock::new(3);
1761 for i in 0..20 {
1763 block.compute(&(i as f32));
1764 }
1765 assert!(block.history.len() <= block.window_size * 2);
1767 }
1768
1769 #[test]
1770 fn test_load_trend_block_insufficient_history() {
1771 let mut block = LoadTrendBlock::new(5);
1772 block.compute(&1.0);
1773 assert_eq!(block.trend(), TrendDirection::Flat);
1775 }
1776
1777 #[test]
1778 fn test_sparkline_block_find_min_max_empty() {
1779 let block = SparklineBlock::new(5);
1780 let (min, max) = block.find_min_max();
1782 assert_eq!(min, 0.0);
1783 assert_eq!(max, 1.0);
1784 }
1785
1786 #[test]
1787 fn test_sparkline_block_simd_instruction_set() {
1788 let block = SparklineBlock::new(10);
1789 let isa = block.simd_instruction_set();
1790 assert!(isa.vector_width() >= 1);
1791 }
1792
1793 #[test]
1794 fn test_load_trend_latency_budget() {
1795 let trend = LoadTrendBlock::new(5);
1796 assert_eq!(trend.latency_budget_us(), 10);
1797 }
1798
1799 #[test]
1800 fn test_sparkline_latency_budget() {
1801 let sparkline = SparklineBlock::new(60);
1802 assert_eq!(sparkline.latency_budget_us(), 100);
1803 }
1804}
1805
1806#[derive(Debug, Clone, Default)]
1837pub struct MetricsCache {
1838 pub cpu: CpuMetricsCache,
1840 pub memory: MemoryMetricsCache,
1842 pub process: ProcessMetricsCache,
1844 pub network: NetworkMetricsCache,
1846 pub gpu: GpuMetricsCache,
1848 pub frame_id: u64,
1850 pub updated_at_us: u64,
1852}
1853
1854#[derive(Debug, Clone, Default)]
1856pub struct CpuMetricsCache {
1857 pub avg_usage: f32,
1859 pub max_core_usage: f32,
1861 pub hot_cores: u32,
1863 pub load_avg: [f32; 3],
1865 pub freq_ghz: f32,
1867 pub trend: TrendDirection,
1869}
1870
1871#[derive(Debug, Clone, Default)]
1873pub struct MemoryMetricsCache {
1874 pub usage_percent: f32,
1876 pub used_bytes: u64,
1878 pub total_bytes: u64,
1880 pub cached_bytes: u64,
1882 pub swap_percent: f32,
1884 pub zram_ratio: f32,
1886 pub trend: TrendDirection,
1888}
1889
1890#[derive(Debug, Clone, Default)]
1892pub struct ProcessMetricsCache {
1893 pub total_count: u32,
1895 pub running_count: u32,
1897 pub sleeping_count: u32,
1899 pub top_cpu: Option<(u32, f32, String)>,
1901 pub top_mem: Option<(u32, f32, String)>,
1903 pub total_cpu_usage: f32,
1905}
1906
1907#[derive(Debug, Clone, Default)]
1909pub struct NetworkMetricsCache {
1910 pub interface: String,
1912 pub rx_bytes_sec: u64,
1914 pub tx_bytes_sec: u64,
1916 pub total_rx: u64,
1918 pub total_tx: u64,
1920 pub connection_count: u32,
1922}
1923
1924#[derive(Debug, Clone, Default)]
1926pub struct GpuMetricsCache {
1927 pub name: String,
1929 pub usage_percent: f32,
1931 pub vram_percent: f32,
1933 pub temp_c: f32,
1935 pub power_w: f32,
1937 pub thermal_state: GpuThermalState,
1939}
1940
1941impl MetricsCache {
1942 #[must_use]
1944 pub fn new() -> Self {
1945 Self::default()
1946 }
1947
1948 #[must_use]
1950 pub fn is_stale(&self, current_time_us: u64, max_age_us: u64) -> bool {
1951 current_time_us.saturating_sub(self.updated_at_us) > max_age_us
1952 }
1953
1954 pub fn update_cpu(
1956 &mut self,
1957 per_core: &[f64],
1958 load_avg: [f32; 3],
1959 freq_ghz: f32,
1960 frame_id: u64,
1961 ) {
1962 if per_core.is_empty() {
1963 return;
1964 }
1965
1966 let sum: f64 = per_core.iter().sum();
1968 let max: f64 = per_core.iter().copied().fold(0.0, f64::max);
1969 let hot_cores = per_core.iter().filter(|&&c| c > 90.0).count();
1970
1971 self.cpu.avg_usage = (sum / per_core.len() as f64) as f32;
1972 self.cpu.max_core_usage = max as f32;
1973 self.cpu.hot_cores = hot_cores as u32;
1974 self.cpu.load_avg = load_avg;
1975 self.cpu.freq_ghz = freq_ghz;
1976 self.frame_id = frame_id;
1977 }
1978
1979 pub fn update_memory(
1981 &mut self,
1982 used: u64,
1983 total: u64,
1984 cached: u64,
1985 swap_used: u64,
1986 swap_total: u64,
1987 zram_ratio: f32,
1988 ) {
1989 self.memory.used_bytes = used;
1990 self.memory.total_bytes = total;
1991 self.memory.cached_bytes = cached;
1992 self.memory.usage_percent = if total > 0 {
1993 used as f32 / total as f32 * 100.0
1994 } else {
1995 0.0
1996 };
1997 self.memory.swap_percent = if swap_total > 0 {
1998 swap_used as f32 / swap_total as f32 * 100.0
1999 } else {
2000 0.0
2001 };
2002 self.memory.zram_ratio = zram_ratio;
2003 }
2004
2005 pub fn update_process(
2007 &mut self,
2008 total: u32,
2009 running: u32,
2010 sleeping: u32,
2011 top_cpu: Option<(u32, f32, String)>,
2012 top_mem: Option<(u32, f32, String)>,
2013 total_cpu: f32,
2014 ) {
2015 self.process.total_count = total;
2016 self.process.running_count = running;
2017 self.process.sleeping_count = sleeping;
2018 self.process.top_cpu = top_cpu;
2019 self.process.top_mem = top_mem;
2020 self.process.total_cpu_usage = total_cpu;
2021 }
2022
2023 pub fn update_network(
2025 &mut self,
2026 interface: String,
2027 rx_rate: u64,
2028 tx_rate: u64,
2029 total_rx: u64,
2030 total_tx: u64,
2031 conn_count: u32,
2032 ) {
2033 self.network.interface = interface;
2034 self.network.rx_bytes_sec = rx_rate;
2035 self.network.tx_bytes_sec = tx_rate;
2036 self.network.total_rx = total_rx;
2037 self.network.total_tx = total_tx;
2038 self.network.connection_count = conn_count;
2039 }
2040
2041 pub fn update_gpu(&mut self, name: String, usage: f32, vram: f32, temp: f32, power: f32) {
2043 self.gpu.name = name;
2044 self.gpu.usage_percent = usage;
2045 self.gpu.vram_percent = vram;
2046 self.gpu.temp_c = temp;
2047 self.gpu.power_w = power;
2048 self.gpu.thermal_state = if temp >= 90.0 {
2049 GpuThermalState::Critical
2050 } else if temp >= 80.0 {
2051 GpuThermalState::Hot
2052 } else if temp >= 70.0 {
2053 GpuThermalState::Warm
2054 } else if temp >= 50.0 {
2055 GpuThermalState::Normal
2056 } else {
2057 GpuThermalState::Cool
2058 };
2059 }
2060
2061 pub fn mark_updated(&mut self, timestamp_us: u64) {
2063 self.updated_at_us = timestamp_us;
2064 }
2065}
2066
2067#[derive(Debug, Clone, Default)]
2069pub struct MetricsCacheBlock {
2070 cache: MetricsCache,
2071 instruction_set: SimdInstructionSet,
2072}
2073
2074impl MetricsCacheBlock {
2075 #[must_use]
2077 pub fn new() -> Self {
2078 Self {
2079 cache: MetricsCache::new(),
2080 instruction_set: SimdInstructionSet::detect(),
2081 }
2082 }
2083
2084 #[must_use]
2086 pub fn cache(&self) -> &MetricsCache {
2087 &self.cache
2088 }
2089
2090 pub fn cache_mut(&mut self) -> &mut MetricsCache {
2092 &mut self.cache
2093 }
2094}
2095
2096impl ComputeBlock for MetricsCacheBlock {
2097 type Input = (); type Output = MetricsCache;
2099
2100 fn compute(&mut self, _input: &Self::Input) -> Self::Output {
2101 self.cache.clone()
2102 }
2103
2104 fn simd_instruction_set(&self) -> SimdInstructionSet {
2105 self.instruction_set
2106 }
2107
2108 fn latency_budget_us(&self) -> u64 {
2109 1 }
2111}
2112
2113#[cfg(test)]
2114mod metrics_cache_tests {
2115 use super::*;
2116
2117 #[test]
2118 fn test_metrics_cache_new() {
2119 let cache = MetricsCache::new();
2120 assert_eq!(cache.frame_id, 0);
2121 assert_eq!(cache.cpu.avg_usage, 0.0);
2122 }
2123
2124 #[test]
2125 fn test_metrics_cache_update_cpu() {
2126 let mut cache = MetricsCache::new();
2127 let cores = vec![10.0, 20.0, 30.0, 95.0];
2128 cache.update_cpu(&cores, [1.0, 2.0, 3.0], 4.5, 1);
2129
2130 assert!((cache.cpu.avg_usage - 38.75).abs() < 0.1);
2131 assert_eq!(cache.cpu.max_core_usage, 95.0);
2132 assert_eq!(cache.cpu.hot_cores, 1);
2133 assert_eq!(cache.cpu.freq_ghz, 4.5);
2134 assert_eq!(cache.frame_id, 1);
2135 }
2136
2137 #[test]
2138 fn test_metrics_cache_update_memory() {
2139 let mut cache = MetricsCache::new();
2140 cache.update_memory(
2141 50_000_000_000, 100_000_000_000, 20_000_000_000, 1_000_000_000, 10_000_000_000, 2.5, );
2148
2149 assert!((cache.memory.usage_percent - 50.0).abs() < 0.1);
2150 assert!((cache.memory.swap_percent - 10.0).abs() < 0.1);
2151 assert_eq!(cache.memory.zram_ratio, 2.5);
2152 }
2153
2154 #[test]
2155 fn test_metrics_cache_update_process() {
2156 let mut cache = MetricsCache::new();
2157 cache.update_process(
2158 1000, 5, 900, Some((1234, 50.0, "chrome".to_string())),
2162 Some((5678, 25.0, "firefox".to_string())),
2163 150.0, );
2165
2166 assert_eq!(cache.process.total_count, 1000);
2167 assert_eq!(cache.process.running_count, 5);
2168 assert!(cache.process.top_cpu.is_some());
2169 assert_eq!(cache.process.top_cpu.as_ref().unwrap().2, "chrome");
2170 }
2171
2172 #[test]
2173 fn test_metrics_cache_update_gpu() {
2174 let mut cache = MetricsCache::new();
2175 cache.update_gpu(
2176 "RTX 4090".to_string(),
2177 80.0, 50.0, 75.0, 300.0, );
2182
2183 assert_eq!(cache.gpu.name, "RTX 4090");
2184 assert_eq!(cache.gpu.thermal_state, GpuThermalState::Warm);
2185 }
2186
2187 #[test]
2188 fn test_metrics_cache_staleness() {
2189 let mut cache = MetricsCache::new();
2190 cache.mark_updated(1000);
2191
2192 assert!(!cache.is_stale(1000, 100));
2194 assert!(!cache.is_stale(1050, 100));
2196 assert!(cache.is_stale(1200, 100));
2198 }
2199
2200 #[test]
2201 fn test_metrics_cache_block_compute() {
2202 let mut block = MetricsCacheBlock::new();
2203 block
2204 .cache_mut()
2205 .update_cpu(&[50.0, 60.0], [1.0, 2.0, 3.0], 4.0, 1);
2206
2207 let output = block.compute(&());
2208 assert_eq!(output.frame_id, 1);
2209 assert!(output.cpu.avg_usage > 0.0);
2210 }
2211
2212 #[test]
2213 fn test_metrics_cache_block_latency() {
2214 let block = MetricsCacheBlock::new();
2215 assert_eq!(block.latency_budget_us(), 1);
2216 }
2217
2218 #[test]
2219 fn test_metrics_cache_empty_cores() {
2220 let mut cache = MetricsCache::new();
2221 cache.update_cpu(&[], [0.0, 0.0, 0.0], 0.0, 0);
2222 assert_eq!(cache.cpu.avg_usage, 0.0);
2224 }
2225}