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 Self::Neon
84 }
85
86 #[cfg(target_arch = "wasm32")]
87 {
88 #[cfg(target_feature = "simd128")]
90 return Self::WasmSimd128;
91 }
92
93 #[cfg(not(target_arch = "aarch64"))]
94 {
95 Self::Scalar
96 }
97 }
98
99 #[must_use]
101 pub const fn name(self) -> &'static str {
102 match self {
103 Self::Scalar => "Scalar",
104 Self::Sse4 => "SSE4.1",
105 Self::Avx2 => "AVX2",
106 Self::Avx512 => "AVX-512",
107 Self::Neon => "NEON",
108 Self::WasmSimd128 => "WASM SIMD128",
109 }
110 }
111}
112
113impl Default for SimdInstructionSet {
114 fn default() -> Self {
115 Self::detect()
116 }
117}
118
119pub trait ComputeBlock {
146 type Input;
148 type Output;
150
151 fn compute(&mut self, input: &Self::Input) -> Self::Output;
157
158 fn simd_supported(&self) -> bool {
160 self.simd_instruction_set() != SimdInstructionSet::Scalar
161 }
162
163 fn simd_instruction_set(&self) -> SimdInstructionSet {
165 SimdInstructionSet::detect()
166 }
167
168 fn latency_budget_us(&self) -> u64 {
170 1000 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
176pub enum ComputeBlockId {
177 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, }
220
221impl ComputeBlockId {
222 #[must_use]
224 pub const fn id_string(&self) -> &'static str {
225 match self {
226 Self::CpuSparklines => "CB-CPU-001",
227 Self::CpuLoadGauge => "CB-CPU-002",
228 Self::CpuLoadTrend => "CB-CPU-003",
229 Self::CpuFrequency => "CB-CPU-004",
230 Self::CpuBoostIndicator => "CB-CPU-005",
231 Self::CpuTemperature => "CB-CPU-006",
232 Self::CpuTopConsumers => "CB-CPU-007",
233 Self::MemSparklines => "CB-MEM-001",
234 Self::MemZramRatio => "CB-MEM-002",
235 Self::MemPressureGauge => "CB-MEM-003",
236 Self::MemSwapThrashing => "CB-MEM-004",
237 Self::MemCacheBreakdown => "CB-MEM-005",
238 Self::MemHugePages => "CB-MEM-006",
239 Self::ConnAge => "CB-CONN-001",
240 Self::ConnProc => "CB-CONN-002",
241 Self::ConnGeo => "CB-CONN-003",
242 Self::ConnLatency => "CB-CONN-004",
243 Self::ConnService => "CB-CONN-005",
244 Self::ConnHotIndicator => "CB-CONN-006",
245 Self::ConnSparkline => "CB-CONN-007",
246 Self::NetSparklines => "CB-NET-001",
247 Self::NetProtocolStats => "CB-NET-002",
248 Self::NetErrorRate => "CB-NET-003",
249 Self::NetDropRate => "CB-NET-004",
250 Self::NetLatencyGauge => "CB-NET-005",
251 Self::NetBandwidthUtil => "CB-NET-006",
252 Self::ProcTreeView => "CB-PROC-001",
253 Self::ProcSortIndicator => "CB-PROC-002",
254 Self::ProcFilter => "CB-PROC-003",
255 Self::ProcOomScore => "CB-PROC-004",
256 Self::ProcNiceValue => "CB-PROC-005",
257 Self::ProcThreadCount => "CB-PROC-006",
258 Self::ProcCgroup => "CB-PROC-007",
259 }
260 }
261
262 #[must_use]
264 pub const fn simd_vectorizable(&self) -> bool {
265 match self {
266 Self::CpuSparklines
268 | Self::CpuLoadTrend
269 | Self::CpuFrequency
270 | Self::CpuTemperature
271 | Self::CpuTopConsumers
272 | Self::MemSparklines
273 | Self::MemPressureGauge
274 | Self::MemSwapThrashing
275 | Self::ConnAge
276 | Self::ConnGeo
277 | Self::ConnLatency
278 | Self::ConnService
279 | Self::ConnHotIndicator
280 | Self::ConnSparkline
281 | Self::NetSparklines
282 | Self::NetProtocolStats
283 | Self::NetErrorRate
284 | Self::NetDropRate
285 | Self::NetBandwidthUtil
286 | Self::ProcOomScore
287 | Self::ProcNiceValue
288 | Self::ProcThreadCount => true,
289
290 Self::CpuLoadGauge
292 | Self::CpuBoostIndicator
293 | Self::MemZramRatio
294 | Self::MemCacheBreakdown
295 | Self::MemHugePages
296 | Self::ConnProc
297 | Self::NetLatencyGauge
298 | Self::ProcTreeView
299 | Self::ProcSortIndicator
300 | Self::ProcFilter
301 | Self::ProcCgroup => false,
302 }
303 }
304}
305
306#[derive(Debug, Clone)]
311#[allow(dead_code)]
312pub struct SparklineBlock {
313 history: Vec<f32>,
315 max_samples: usize,
317 simd_buffer: [f32; 8],
319 instruction_set: SimdInstructionSet,
321}
322
323impl Default for SparklineBlock {
324 fn default() -> Self {
325 Self::new(60)
326 }
327}
328
329impl SparklineBlock {
330 #[must_use]
332 pub fn new(max_samples: usize) -> Self {
333 debug_assert!(max_samples > 0, "max_samples must be positive");
334 Self {
335 history: Vec::with_capacity(max_samples),
336 max_samples,
337 simd_buffer: [0.0; 8],
338 instruction_set: SimdInstructionSet::detect(),
339 }
340 }
341
342 pub fn push(&mut self, value: f32) {
344 if self.history.len() >= self.max_samples {
345 self.history.remove(0);
346 }
347 self.history.push(value);
348 }
349
350 #[must_use]
352 pub fn history(&self) -> &[f32] {
353 &self.history
354 }
355
356 #[must_use]
358 pub fn render(&self, width: usize) -> Vec<char> {
359 if self.history.is_empty() {
360 return vec![' '; width];
361 }
362 contract_pre_render!();
363
364 let (min, max) = self.find_min_max();
366 let range = max - min;
367
368 let samples = self.sample_to_width(width);
370
371 #[allow(clippy::items_after_statements)]
373 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
374
375 samples
376 .iter()
377 .map(|&v| {
378 if range < f32::EPSILON {
379 BLOCKS[4] } else {
381 let normalized = ((v - min) / range).clamp(0.0, 1.0);
382 let idx = (normalized * 7.0) as usize;
383 BLOCKS[idx.min(7)]
384 }
385 })
386 .collect()
387 }
388
389 fn find_min_max(&self) -> (f32, f32) {
391 if self.history.is_empty() {
392 return (0.0, 1.0);
393 }
394
395 let min = self.history.iter().copied().fold(f32::INFINITY, f32::min);
398 let max = self
399 .history
400 .iter()
401 .copied()
402 .fold(f32::NEG_INFINITY, f32::max);
403
404 (min, max)
405 }
406
407 fn sample_to_width(&self, width: usize) -> Vec<f32> {
409 if self.history.len() <= width {
410 let mut result = vec![0.0; width - self.history.len()];
412 result.extend_from_slice(&self.history);
413 result
414 } else {
415 let step = self.history.len() as f32 / width as f32;
417 (0..width)
418 .map(|i| {
419 let idx = (i as f32 * step) as usize;
420 self.history[idx.min(self.history.len() - 1)]
421 })
422 .collect()
423 }
424 }
425}
426
427impl ComputeBlock for SparklineBlock {
428 type Input = f32;
429 type Output = Vec<char>;
430
431 fn compute(&mut self, input: &Self::Input) -> Self::Output {
432 self.push(*input);
433 self.render(self.max_samples.min(60))
434 }
435
436 fn simd_instruction_set(&self) -> SimdInstructionSet {
437 self.instruction_set
438 }
439
440 fn latency_budget_us(&self) -> u64 {
441 100 }
443}
444
445#[derive(Debug, Clone)]
449pub struct LoadTrendBlock {
450 history: Vec<f32>,
452 window_size: usize,
454}
455
456impl Default for LoadTrendBlock {
457 fn default() -> Self {
458 Self::new(5)
459 }
460}
461
462impl LoadTrendBlock {
463 #[must_use]
465 pub fn new(window_size: usize) -> Self {
466 debug_assert!(window_size > 0, "window_size must be positive");
467 Self {
468 history: Vec::with_capacity(window_size),
469 window_size,
470 }
471 }
472
473 #[must_use]
475 pub fn trend(&self) -> TrendDirection {
476 if self.history.len() < 2 {
477 return TrendDirection::Flat;
478 }
479
480 let recent = self.history.iter().rev().take(self.window_size);
481 let diffs: Vec<f32> = recent
482 .clone()
483 .zip(recent.skip(1))
484 .map(|(a, b)| a - b)
485 .collect();
486
487 if diffs.is_empty() {
488 return TrendDirection::Flat;
489 }
490
491 let avg_diff: f32 = diffs.iter().sum::<f32>() / diffs.len() as f32;
492
493 #[allow(clippy::items_after_statements)]
494 const THRESHOLD: f32 = 0.05;
495 if avg_diff > THRESHOLD {
496 TrendDirection::Up
497 } else if avg_diff < -THRESHOLD {
498 TrendDirection::Down
499 } else {
500 TrendDirection::Flat
501 }
502 }
503}
504
505impl ComputeBlock for LoadTrendBlock {
506 type Input = f32;
507 type Output = TrendDirection;
508
509 fn compute(&mut self, input: &Self::Input) -> Self::Output {
510 if self.history.len() >= self.window_size * 2 {
511 self.history.remove(0);
512 }
513 self.history.push(*input);
514 self.trend()
515 }
516
517 fn latency_budget_us(&self) -> u64 {
518 10 }
520}
521
522#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
524pub enum TrendDirection {
525 Up,
527 Down,
529 #[default]
531 Flat,
532}
533
534impl TrendDirection {
535 #[must_use]
537 pub const fn arrow(self) -> char {
538 match self {
539 Self::Up => '↑',
540 Self::Down => '↓',
541 Self::Flat => '→',
542 }
543 }
544}
545
546#[derive(Debug, Clone)]
555pub struct CpuFrequencyBlock {
556 frequencies: Vec<u32>,
558 max_frequencies: Vec<u32>,
560 instruction_set: SimdInstructionSet,
562}
563
564impl Default for CpuFrequencyBlock {
565 fn default() -> Self {
566 Self::new()
567 }
568}
569
570impl CpuFrequencyBlock {
571 #[must_use]
573 pub fn new() -> Self {
574 Self {
575 frequencies: Vec::new(),
576 max_frequencies: Vec::new(),
577 instruction_set: SimdInstructionSet::detect(),
578 }
579 }
580
581 pub fn set_frequencies(&mut self, freqs: Vec<u32>, max_freqs: Vec<u32>) {
583 self.frequencies = freqs;
584 self.max_frequencies = max_freqs;
585 }
586
587 #[must_use]
589 pub fn frequency_percentages(&self) -> Vec<f32> {
590 self.frequencies
591 .iter()
592 .zip(self.max_frequencies.iter())
593 .map(|(&cur, &max)| {
594 if max > 0 {
595 (cur as f32 / max as f32 * 100.0).clamp(0.0, 100.0)
596 } else {
597 0.0
598 }
599 })
600 .collect()
601 }
602
603 #[must_use]
605 pub fn scaling_indicators(&self) -> Vec<FrequencyScalingState> {
606 self.frequency_percentages()
607 .iter()
608 .map(|&pct| {
609 if pct >= 95.0 {
610 FrequencyScalingState::Turbo
611 } else if pct >= 75.0 {
612 FrequencyScalingState::High
613 } else if pct >= 50.0 {
614 FrequencyScalingState::Normal
615 } else if pct >= 25.0 {
616 FrequencyScalingState::Scaled
617 } else {
618 FrequencyScalingState::Idle
619 }
620 })
621 .collect()
622 }
623}
624
625impl ComputeBlock for CpuFrequencyBlock {
626 type Input = (Vec<u32>, Vec<u32>); type Output = Vec<FrequencyScalingState>;
628
629 fn compute(&mut self, input: &Self::Input) -> Self::Output {
630 self.set_frequencies(input.0.clone(), input.1.clone());
631 self.scaling_indicators()
632 }
633
634 fn simd_instruction_set(&self) -> SimdInstructionSet {
635 self.instruction_set
636 }
637
638 fn latency_budget_us(&self) -> u64 {
639 50 }
641}
642
643#[derive(Debug, Clone, Copy, PartialEq, Eq)]
645pub enum FrequencyScalingState {
646 Turbo,
648 High,
650 Normal,
652 Scaled,
654 Idle,
656}
657
658impl FrequencyScalingState {
659 #[must_use]
661 pub const fn indicator(self) -> char {
662 match self {
663 Self::Turbo => '⚡',
664 Self::High => '↑',
665 Self::Normal => '→',
666 Self::Scaled => '↓',
667 Self::Idle => '·',
668 }
669 }
670}
671
672#[derive(Debug, Clone)]
676pub struct CpuGovernorBlock {
677 governor: CpuGovernor,
679}
680
681impl Default for CpuGovernorBlock {
682 fn default() -> Self {
683 Self::new()
684 }
685}
686
687impl CpuGovernorBlock {
688 #[must_use]
690 pub fn new() -> Self {
691 Self {
692 governor: CpuGovernor::Unknown,
693 }
694 }
695
696 pub fn set_governor(&mut self, name: &str) {
698 self.governor = CpuGovernor::from_name(name);
699 }
700
701 #[must_use]
703 pub fn governor(&self) -> CpuGovernor {
704 self.governor
705 }
706}
707
708impl ComputeBlock for CpuGovernorBlock {
709 type Input = String;
710 type Output = CpuGovernor;
711
712 fn compute(&mut self, input: &Self::Input) -> Self::Output {
713 self.set_governor(input);
714 self.governor
715 }
716
717 fn latency_budget_us(&self) -> u64 {
718 10 }
720}
721
722#[derive(Debug, Clone, Copy, PartialEq, Eq)]
724pub enum CpuGovernor {
725 Performance,
727 Powersave,
729 Ondemand,
731 Conservative,
733 Schedutil,
735 Userspace,
737 Unknown,
739}
740
741impl CpuGovernor {
742 #[must_use]
744 pub fn from_name(name: &str) -> Self {
745 match name.trim().to_lowercase().as_str() {
746 "performance" => Self::Performance,
747 "powersave" => Self::Powersave,
748 "ondemand" => Self::Ondemand,
749 "conservative" => Self::Conservative,
750 "schedutil" => Self::Schedutil,
751 "userspace" => Self::Userspace,
752 _ => Self::Unknown,
753 }
754 }
755
756 #[must_use]
758 pub const fn as_str(&self) -> &'static str {
759 match self {
760 Self::Performance => "performance",
761 Self::Powersave => "powersave",
762 Self::Ondemand => "ondemand",
763 Self::Conservative => "conservative",
764 Self::Schedutil => "schedutil",
765 Self::Userspace => "userspace",
766 Self::Unknown => "unknown",
767 }
768 }
769
770 #[must_use]
772 pub const fn short_name(self) -> &'static str {
773 match self {
774 Self::Performance => "perf",
775 Self::Powersave => "psav",
776 Self::Ondemand => "odmd",
777 Self::Conservative => "cons",
778 Self::Schedutil => "schu",
779 Self::Userspace => "user",
780 Self::Unknown => "????",
781 }
782 }
783
784 #[must_use]
786 pub const fn icon(self) -> char {
787 match self {
788 Self::Performance => '🚀',
789 Self::Powersave => '🔋',
790 Self::Ondemand => '⚡',
791 Self::Conservative => '📊',
792 Self::Schedutil => '📅',
793 Self::Userspace => '👤',
794 Self::Unknown => '?',
795 }
796 }
797}
798
799#[derive(Debug, Clone)]
803pub struct MemPressureBlock {
804 avg10_some: f32,
806 avg60_some: f32,
808 avg300_some: f32,
810 avg10_full: f32,
812 instruction_set: SimdInstructionSet,
814}
815
816impl Default for MemPressureBlock {
817 fn default() -> Self {
818 Self::new()
819 }
820}
821
822impl MemPressureBlock {
823 #[must_use]
825 pub fn new() -> Self {
826 Self {
827 avg10_some: 0.0,
828 avg60_some: 0.0,
829 avg300_some: 0.0,
830 avg10_full: 0.0,
831 instruction_set: SimdInstructionSet::detect(),
832 }
833 }
834
835 pub fn set_pressure(
837 &mut self,
838 avg10_some: f32,
839 avg60_some: f32,
840 avg300_some: f32,
841 avg10_full: f32,
842 ) {
843 debug_assert!(avg10_some >= 0.0, "avg10_some must be non-negative");
844 debug_assert!(avg60_some >= 0.0, "avg60_some must be non-negative");
845 debug_assert!(avg300_some >= 0.0, "avg300_some must be non-negative");
846 debug_assert!(avg10_full >= 0.0, "avg10_full must be non-negative");
847 self.avg10_some = avg10_some;
848 self.avg60_some = avg60_some;
849 self.avg300_some = avg300_some;
850 self.avg10_full = avg10_full;
851 }
852
853 #[must_use]
855 pub fn pressure_level(&self) -> MemoryPressureLevel {
856 let pct = self.avg10_some;
857 if pct >= 50.0 {
858 MemoryPressureLevel::Critical
859 } else if pct >= 25.0 {
860 MemoryPressureLevel::High
861 } else if pct >= 10.0 {
862 MemoryPressureLevel::Medium
863 } else if pct >= 1.0 {
864 MemoryPressureLevel::Low
865 } else {
866 MemoryPressureLevel::None
867 }
868 }
869
870 #[must_use]
872 pub fn trend(&self) -> TrendDirection {
873 let diff = self.avg10_some - self.avg300_some;
874 if diff > 5.0 {
875 TrendDirection::Up
876 } else if diff < -5.0 {
877 TrendDirection::Down
878 } else {
879 TrendDirection::Flat
880 }
881 }
882}
883
884impl ComputeBlock for MemPressureBlock {
885 type Input = (f32, f32, f32, f32); type Output = MemoryPressureLevel;
887
888 fn compute(&mut self, input: &Self::Input) -> Self::Output {
889 self.set_pressure(input.0, input.1, input.2, input.3);
890 self.pressure_level()
891 }
892
893 fn simd_instruction_set(&self) -> SimdInstructionSet {
894 self.instruction_set
895 }
896
897 fn latency_budget_us(&self) -> u64 {
898 20 }
900}
901
902#[derive(Debug, Clone, Copy, PartialEq, Eq)]
904pub enum MemoryPressureLevel {
905 None,
907 Low,
909 Medium,
911 High,
913 Critical,
915}
916
917impl MemoryPressureLevel {
918 #[allow(clippy::match_same_arms)]
920 pub fn symbol(&self) -> char {
921 match self {
922 Self::None => ' ',
923 Self::Low => '○',
924 Self::Medium => '◐',
925 Self::High => '◕',
926 Self::Critical => '●',
927 }
928 }
929
930 #[must_use]
932 pub const fn severity(self) -> u8 {
933 match self {
934 Self::None => 0,
935 Self::Low => 1,
936 Self::Medium => 2,
937 Self::High => 3,
938 Self::Critical => 4,
939 }
940 }
941}
942
943#[derive(Debug, Clone)]
947pub struct HugePagesBlock {
948 total: u64,
950 free: u64,
952 reserved: u64,
954 page_size_kb: u64,
956}
957
958impl Default for HugePagesBlock {
959 fn default() -> Self {
960 Self::new()
961 }
962}
963
964impl HugePagesBlock {
965 #[must_use]
967 pub fn new() -> Self {
968 Self {
969 total: 0,
970 free: 0,
971 reserved: 0,
972 page_size_kb: 2048, }
974 }
975
976 pub fn set_values(&mut self, total: u64, free: u64, reserved: u64, page_size_kb: u64) {
978 debug_assert!(free <= total, "free must be <= total");
979 debug_assert!(page_size_kb > 0, "page_size_kb must be positive");
980 self.total = total;
981 self.free = free;
982 self.reserved = reserved;
983 self.page_size_kb = page_size_kb;
984 }
985
986 #[must_use]
988 pub fn usage_percent(&self) -> f32 {
989 if self.total == 0 {
990 0.0
991 } else {
992 ((self.total - self.free) as f32 / self.total as f32 * 100.0).clamp(0.0, 100.0)
993 }
994 }
995
996 #[must_use]
998 pub fn total_bytes(&self) -> u64 {
999 self.total * self.page_size_kb * 1024
1000 }
1001
1002 #[must_use]
1004 pub fn used_bytes(&self) -> u64 {
1005 (self.total - self.free) * self.page_size_kb * 1024
1006 }
1007}
1008
1009impl ComputeBlock for HugePagesBlock {
1010 type Input = (u64, u64, u64, u64); type Output = f32; fn compute(&mut self, input: &Self::Input) -> Self::Output {
1014 self.set_values(input.0, input.1, input.2, input.3);
1015 self.usage_percent()
1016 }
1017
1018 fn latency_budget_us(&self) -> u64 {
1019 10 }
1021}
1022
1023#[derive(Debug, Clone)]
1027pub struct GpuThermalBlock {
1028 temperature_c: f32,
1030 power_w: f32,
1032 power_limit_w: f32,
1034 temp_history: Vec<f32>,
1036 instruction_set: SimdInstructionSet,
1038}
1039
1040impl Default for GpuThermalBlock {
1041 fn default() -> Self {
1042 Self::new()
1043 }
1044}
1045
1046impl GpuThermalBlock {
1047 #[must_use]
1049 pub fn new() -> Self {
1050 Self {
1051 temperature_c: 0.0,
1052 power_w: 0.0,
1053 power_limit_w: 0.0,
1054 temp_history: Vec::with_capacity(60),
1055 instruction_set: SimdInstructionSet::detect(),
1056 }
1057 }
1058
1059 pub fn set_values(&mut self, temp_c: f32, power_w: f32, power_limit_w: f32) {
1061 debug_assert!(power_w >= 0.0, "power_w must be non-negative");
1062 debug_assert!(power_limit_w >= 0.0, "power_limit_w must be non-negative");
1063 self.temperature_c = temp_c;
1064 self.power_w = power_w;
1065 self.power_limit_w = power_limit_w;
1066
1067 if self.temp_history.len() >= 60 {
1069 self.temp_history.remove(0);
1070 }
1071 self.temp_history.push(temp_c);
1072 }
1073
1074 #[must_use]
1076 pub fn thermal_state(&self) -> GpuThermalState {
1077 if self.temperature_c >= 90.0 {
1078 GpuThermalState::Critical
1079 } else if self.temperature_c >= 80.0 {
1080 GpuThermalState::Hot
1081 } else if self.temperature_c >= 70.0 {
1082 GpuThermalState::Warm
1083 } else if self.temperature_c >= 50.0 {
1084 GpuThermalState::Normal
1085 } else {
1086 GpuThermalState::Cool
1087 }
1088 }
1089
1090 #[must_use]
1092 pub fn power_percent(&self) -> f32 {
1093 if self.power_limit_w > 0.0 {
1094 (self.power_w / self.power_limit_w * 100.0).clamp(0.0, 100.0)
1095 } else {
1096 0.0
1097 }
1098 }
1099
1100 #[must_use]
1102 pub fn trend(&self) -> TrendDirection {
1103 if self.temp_history.len() < 5 {
1104 return TrendDirection::Flat;
1105 }
1106
1107 let recent: f32 = self.temp_history.iter().rev().take(5).sum::<f32>() / 5.0;
1108 let older: f32 = self.temp_history.iter().rev().skip(5).take(5).sum::<f32>() / 5.0;
1109
1110 let diff = recent - older;
1111 if diff > 2.0 {
1112 TrendDirection::Up
1113 } else if diff < -2.0 {
1114 TrendDirection::Down
1115 } else {
1116 TrendDirection::Flat
1117 }
1118 }
1119}
1120
1121impl ComputeBlock for GpuThermalBlock {
1122 type Input = (f32, f32, f32); type Output = GpuThermalState;
1124
1125 fn compute(&mut self, input: &Self::Input) -> Self::Output {
1126 self.set_values(input.0, input.1, input.2);
1127 self.thermal_state()
1128 }
1129
1130 fn simd_instruction_set(&self) -> SimdInstructionSet {
1131 self.instruction_set
1132 }
1133
1134 fn latency_budget_us(&self) -> u64 {
1135 30 }
1137}
1138
1139#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1141pub enum GpuThermalState {
1142 #[default]
1144 Cool,
1145 Normal,
1147 Warm,
1149 Hot,
1151 Critical,
1153}
1154
1155impl GpuThermalState {
1156 #[must_use]
1158 pub const fn indicator(self) -> char {
1159 match self {
1160 Self::Cool => '❄',
1161 Self::Normal => '●',
1162 Self::Warm => '◐',
1163 Self::Hot => '◕',
1164 Self::Critical => '🔥',
1165 }
1166 }
1167
1168 #[must_use]
1170 pub const fn severity(self) -> u8 {
1171 match self {
1172 Self::Cool => 0,
1173 Self::Normal => 1,
1174 Self::Warm => 2,
1175 Self::Hot => 3,
1176 Self::Critical => 4,
1177 }
1178 }
1179}
1180
1181#[derive(Debug, Clone)]
1185pub struct GpuVramBlock {
1186 total_mb: u64,
1188 used_mb: u64,
1190 per_process: Vec<(u32, u64, String)>, }
1193
1194impl Default for GpuVramBlock {
1195 fn default() -> Self {
1196 Self::new()
1197 }
1198}
1199
1200impl GpuVramBlock {
1201 #[must_use]
1203 pub fn new() -> Self {
1204 Self {
1205 total_mb: 0,
1206 used_mb: 0,
1207 per_process: Vec::new(),
1208 }
1209 }
1210
1211 pub fn set_values(
1213 &mut self,
1214 total_mb: u64,
1215 used_mb: u64,
1216 per_process: Vec<(u32, u64, String)>,
1217 ) {
1218 self.total_mb = total_mb;
1219 self.used_mb = used_mb;
1220 self.per_process = per_process;
1221 }
1222
1223 #[must_use]
1225 pub fn usage_percent(&self) -> f32 {
1226 if self.total_mb == 0 {
1227 0.0
1228 } else {
1229 (self.used_mb as f32 / self.total_mb as f32 * 100.0).clamp(0.0, 100.0)
1230 }
1231 }
1232
1233 #[must_use]
1235 pub fn top_consumers(&self, n: usize) -> Vec<&(u32, u64, String)> {
1236 let mut sorted: Vec<_> = self.per_process.iter().collect();
1237 sorted.sort_by_key(|b| std::cmp::Reverse(b.1));
1238 sorted.into_iter().take(n).collect()
1239 }
1240}
1241
1242impl ComputeBlock for GpuVramBlock {
1243 type Input = (u64, u64, Vec<(u32, u64, String)>);
1244 type Output = f32; fn compute(&mut self, input: &Self::Input) -> Self::Output {
1247 self.set_values(input.0, input.1, input.2.clone());
1248 self.usage_percent()
1249 }
1250
1251 fn latency_budget_us(&self) -> u64 {
1252 100 }
1254}
1255
1256#[derive(Debug, Clone, Default)]
1287pub struct MetricsCache {
1288 pub cpu: CpuMetricsCache,
1290 pub memory: MemoryMetricsCache,
1292 pub process: ProcessMetricsCache,
1294 pub network: NetworkMetricsCache,
1296 pub gpu: GpuMetricsCache,
1298 pub frame_id: u64,
1300 pub updated_at_us: u64,
1302}
1303
1304#[derive(Debug, Clone, Default)]
1306pub struct CpuMetricsCache {
1307 pub avg_usage: f32,
1309 pub max_core_usage: f32,
1311 pub hot_cores: u32,
1313 pub load_avg: [f32; 3],
1315 pub freq_ghz: f32,
1317 pub trend: TrendDirection,
1319}
1320
1321#[derive(Debug, Clone, Default)]
1323pub struct MemoryMetricsCache {
1324 pub usage_percent: f32,
1326 pub used_bytes: u64,
1328 pub total_bytes: u64,
1330 pub cached_bytes: u64,
1332 pub swap_percent: f32,
1334 pub zram_ratio: f32,
1336 pub trend: TrendDirection,
1338}
1339
1340#[derive(Debug, Clone, Default)]
1342pub struct ProcessMetricsCache {
1343 pub total_count: u32,
1345 pub running_count: u32,
1347 pub sleeping_count: u32,
1349 pub top_cpu: Option<(u32, f32, String)>,
1351 pub top_mem: Option<(u32, f32, String)>,
1353 pub total_cpu_usage: f32,
1355}
1356
1357#[derive(Debug, Clone, Default)]
1359pub struct NetworkMetricsCache {
1360 pub interface: String,
1362 pub rx_bytes_sec: u64,
1364 pub tx_bytes_sec: u64,
1366 pub total_rx: u64,
1368 pub total_tx: u64,
1370 pub connection_count: u32,
1372}
1373
1374#[derive(Debug, Clone, Default)]
1376pub struct GpuMetricsCache {
1377 pub name: String,
1379 pub usage_percent: f32,
1381 pub vram_percent: f32,
1383 pub temp_c: f32,
1385 pub power_w: f32,
1387 pub thermal_state: GpuThermalState,
1389}
1390
1391impl MetricsCache {
1392 #[must_use]
1394 pub fn new() -> Self {
1395 Self::default()
1396 }
1397
1398 #[must_use]
1400 pub fn is_stale(&self, current_time_us: u64, max_age_us: u64) -> bool {
1401 current_time_us.saturating_sub(self.updated_at_us) > max_age_us
1402 }
1403
1404 pub fn update_cpu(
1406 &mut self,
1407 per_core: &[f64],
1408 load_avg: [f32; 3],
1409 freq_ghz: f32,
1410 frame_id: u64,
1411 ) {
1412 if per_core.is_empty() {
1413 return;
1414 }
1415
1416 let sum: f64 = per_core.iter().sum();
1418 let max: f64 = per_core.iter().copied().fold(0.0, f64::max);
1419 let hot_cores = per_core.iter().filter(|&&c| c > 90.0).count();
1420
1421 self.cpu.avg_usage = (sum / per_core.len() as f64) as f32;
1422 self.cpu.max_core_usage = max as f32;
1423 self.cpu.hot_cores = hot_cores as u32;
1424 self.cpu.load_avg = load_avg;
1425 self.cpu.freq_ghz = freq_ghz;
1426 self.frame_id = frame_id;
1427 }
1428
1429 pub fn update_memory(
1431 &mut self,
1432 used: u64,
1433 total: u64,
1434 cached: u64,
1435 swap_used: u64,
1436 swap_total: u64,
1437 zram_ratio: f32,
1438 ) {
1439 self.memory.used_bytes = used;
1440 self.memory.total_bytes = total;
1441 self.memory.cached_bytes = cached;
1442 self.memory.usage_percent = if total > 0 {
1443 used as f32 / total as f32 * 100.0
1444 } else {
1445 0.0
1446 };
1447 self.memory.swap_percent = if swap_total > 0 {
1448 swap_used as f32 / swap_total as f32 * 100.0
1449 } else {
1450 0.0
1451 };
1452 self.memory.zram_ratio = zram_ratio;
1453 }
1454
1455 pub fn update_process(
1457 &mut self,
1458 total: u32,
1459 running: u32,
1460 sleeping: u32,
1461 top_cpu: Option<(u32, f32, String)>,
1462 top_mem: Option<(u32, f32, String)>,
1463 total_cpu: f32,
1464 ) {
1465 self.process.total_count = total;
1466 self.process.running_count = running;
1467 self.process.sleeping_count = sleeping;
1468 self.process.top_cpu = top_cpu;
1469 self.process.top_mem = top_mem;
1470 self.process.total_cpu_usage = total_cpu;
1471 }
1472
1473 pub fn update_network(
1475 &mut self,
1476 interface: String,
1477 rx_rate: u64,
1478 tx_rate: u64,
1479 total_rx: u64,
1480 total_tx: u64,
1481 conn_count: u32,
1482 ) {
1483 self.network.interface = interface;
1484 self.network.rx_bytes_sec = rx_rate;
1485 self.network.tx_bytes_sec = tx_rate;
1486 self.network.total_rx = total_rx;
1487 self.network.total_tx = total_tx;
1488 self.network.connection_count = conn_count;
1489 }
1490
1491 pub fn update_gpu(&mut self, name: String, usage: f32, vram: f32, temp: f32, power: f32) {
1493 self.gpu.name = name;
1494 self.gpu.usage_percent = usage;
1495 self.gpu.vram_percent = vram;
1496 self.gpu.temp_c = temp;
1497 self.gpu.power_w = power;
1498 self.gpu.thermal_state = if temp >= 90.0 {
1499 GpuThermalState::Critical
1500 } else if temp >= 80.0 {
1501 GpuThermalState::Hot
1502 } else if temp >= 70.0 {
1503 GpuThermalState::Warm
1504 } else if temp >= 50.0 {
1505 GpuThermalState::Normal
1506 } else {
1507 GpuThermalState::Cool
1508 };
1509 }
1510
1511 pub fn mark_updated(&mut self, timestamp_us: u64) {
1513 self.updated_at_us = timestamp_us;
1514 }
1515}
1516
1517#[derive(Debug, Clone, Default)]
1519pub struct MetricsCacheBlock {
1520 cache: MetricsCache,
1521 instruction_set: SimdInstructionSet,
1522}
1523
1524impl MetricsCacheBlock {
1525 #[must_use]
1527 pub fn new() -> Self {
1528 Self {
1529 cache: MetricsCache::new(),
1530 instruction_set: SimdInstructionSet::detect(),
1531 }
1532 }
1533
1534 #[must_use]
1536 pub fn cache(&self) -> &MetricsCache {
1537 &self.cache
1538 }
1539
1540 pub fn cache_mut(&mut self) -> &mut MetricsCache {
1542 &mut self.cache
1543 }
1544}
1545
1546impl ComputeBlock for MetricsCacheBlock {
1547 type Input = (); type Output = MetricsCache;
1549
1550 fn compute(&mut self, _input: &Self::Input) -> Self::Output {
1551 self.cache.clone()
1552 }
1553
1554 fn simd_instruction_set(&self) -> SimdInstructionSet {
1555 self.instruction_set
1556 }
1557
1558 fn latency_budget_us(&self) -> u64 {
1559 1 }
1561}
1562
1563#[cfg(test)]
1564#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
1565#[path = "compute_block_tests.rs"]
1566mod tests;