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 contract_pre_render!();
360
361 let (min, max) = self.find_min_max();
363 let range = max - min;
364
365 let samples = self.sample_to_width(width);
367
368 #[allow(clippy::items_after_statements)]
370 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
371
372 samples
373 .iter()
374 .map(|&v| {
375 if range < f32::EPSILON {
376 BLOCKS[4] } else {
378 let normalized = ((v - min) / range).clamp(0.0, 1.0);
379 let idx = (normalized * 7.0) as usize;
380 BLOCKS[idx.min(7)]
381 }
382 })
383 .collect()
384 }
385
386 fn find_min_max(&self) -> (f32, f32) {
388 if self.history.is_empty() {
389 return (0.0, 1.0);
390 }
391
392 let min = self.history.iter().copied().fold(f32::INFINITY, f32::min);
395 let max = self
396 .history
397 .iter()
398 .copied()
399 .fold(f32::NEG_INFINITY, f32::max);
400
401 (min, max)
402 }
403
404 fn sample_to_width(&self, width: usize) -> Vec<f32> {
406 if self.history.len() <= width {
407 let mut result = vec![0.0; width - self.history.len()];
409 result.extend_from_slice(&self.history);
410 result
411 } else {
412 let step = self.history.len() as f32 / width as f32;
414 (0..width)
415 .map(|i| {
416 let idx = (i as f32 * step) as usize;
417 self.history[idx.min(self.history.len() - 1)]
418 })
419 .collect()
420 }
421 }
422}
423
424impl ComputeBlock for SparklineBlock {
425 type Input = f32;
426 type Output = Vec<char>;
427
428 fn compute(&mut self, input: &Self::Input) -> Self::Output {
429 self.push(*input);
430 self.render(self.max_samples.min(60))
431 }
432
433 fn simd_instruction_set(&self) -> SimdInstructionSet {
434 self.instruction_set
435 }
436
437 fn latency_budget_us(&self) -> u64 {
438 100 }
440}
441
442#[derive(Debug, Clone)]
446pub struct LoadTrendBlock {
447 history: Vec<f32>,
449 window_size: usize,
451}
452
453impl Default for LoadTrendBlock {
454 fn default() -> Self {
455 Self::new(5)
456 }
457}
458
459impl LoadTrendBlock {
460 #[must_use]
462 pub fn new(window_size: usize) -> Self {
463 debug_assert!(window_size > 0, "window_size must be positive");
464 Self {
465 history: Vec::with_capacity(window_size),
466 window_size,
467 }
468 }
469
470 #[must_use]
472 pub fn trend(&self) -> TrendDirection {
473 if self.history.len() < 2 {
474 return TrendDirection::Flat;
475 }
476
477 let recent = self.history.iter().rev().take(self.window_size);
478 let diffs: Vec<f32> = recent
479 .clone()
480 .zip(recent.skip(1))
481 .map(|(a, b)| a - b)
482 .collect();
483
484 if diffs.is_empty() {
485 return TrendDirection::Flat;
486 }
487
488 let avg_diff: f32 = diffs.iter().sum::<f32>() / diffs.len() as f32;
489
490 #[allow(clippy::items_after_statements)]
491 const THRESHOLD: f32 = 0.05;
492 if avg_diff > THRESHOLD {
493 TrendDirection::Up
494 } else if avg_diff < -THRESHOLD {
495 TrendDirection::Down
496 } else {
497 TrendDirection::Flat
498 }
499 }
500}
501
502impl ComputeBlock for LoadTrendBlock {
503 type Input = f32;
504 type Output = TrendDirection;
505
506 fn compute(&mut self, input: &Self::Input) -> Self::Output {
507 if self.history.len() >= self.window_size * 2 {
508 self.history.remove(0);
509 }
510 self.history.push(*input);
511 self.trend()
512 }
513
514 fn latency_budget_us(&self) -> u64 {
515 10 }
517}
518
519#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
521pub enum TrendDirection {
522 Up,
524 Down,
526 #[default]
528 Flat,
529}
530
531impl TrendDirection {
532 #[must_use]
534 pub const fn arrow(self) -> char {
535 match self {
536 Self::Up => '↑',
537 Self::Down => '↓',
538 Self::Flat => '→',
539 }
540 }
541}
542
543#[derive(Debug, Clone)]
552pub struct CpuFrequencyBlock {
553 frequencies: Vec<u32>,
555 max_frequencies: Vec<u32>,
557 instruction_set: SimdInstructionSet,
559}
560
561impl Default for CpuFrequencyBlock {
562 fn default() -> Self {
563 Self::new()
564 }
565}
566
567impl CpuFrequencyBlock {
568 #[must_use]
570 pub fn new() -> Self {
571 Self {
572 frequencies: Vec::new(),
573 max_frequencies: Vec::new(),
574 instruction_set: SimdInstructionSet::detect(),
575 }
576 }
577
578 pub fn set_frequencies(&mut self, freqs: Vec<u32>, max_freqs: Vec<u32>) {
580 self.frequencies = freqs;
581 self.max_frequencies = max_freqs;
582 }
583
584 #[must_use]
586 pub fn frequency_percentages(&self) -> Vec<f32> {
587 self.frequencies
588 .iter()
589 .zip(self.max_frequencies.iter())
590 .map(|(&cur, &max)| {
591 if max > 0 {
592 (cur as f32 / max as f32 * 100.0).clamp(0.0, 100.0)
593 } else {
594 0.0
595 }
596 })
597 .collect()
598 }
599
600 #[must_use]
602 pub fn scaling_indicators(&self) -> Vec<FrequencyScalingState> {
603 self.frequency_percentages()
604 .iter()
605 .map(|&pct| {
606 if pct >= 95.0 {
607 FrequencyScalingState::Turbo
608 } else if pct >= 75.0 {
609 FrequencyScalingState::High
610 } else if pct >= 50.0 {
611 FrequencyScalingState::Normal
612 } else if pct >= 25.0 {
613 FrequencyScalingState::Scaled
614 } else {
615 FrequencyScalingState::Idle
616 }
617 })
618 .collect()
619 }
620}
621
622impl ComputeBlock for CpuFrequencyBlock {
623 type Input = (Vec<u32>, Vec<u32>); type Output = Vec<FrequencyScalingState>;
625
626 fn compute(&mut self, input: &Self::Input) -> Self::Output {
627 self.set_frequencies(input.0.clone(), input.1.clone());
628 self.scaling_indicators()
629 }
630
631 fn simd_instruction_set(&self) -> SimdInstructionSet {
632 self.instruction_set
633 }
634
635 fn latency_budget_us(&self) -> u64 {
636 50 }
638}
639
640#[derive(Debug, Clone, Copy, PartialEq, Eq)]
642pub enum FrequencyScalingState {
643 Turbo,
645 High,
647 Normal,
649 Scaled,
651 Idle,
653}
654
655impl FrequencyScalingState {
656 #[must_use]
658 pub const fn indicator(self) -> char {
659 match self {
660 Self::Turbo => '⚡',
661 Self::High => '↑',
662 Self::Normal => '→',
663 Self::Scaled => '↓',
664 Self::Idle => '·',
665 }
666 }
667}
668
669#[derive(Debug, Clone)]
673pub struct CpuGovernorBlock {
674 governor: CpuGovernor,
676}
677
678impl Default for CpuGovernorBlock {
679 fn default() -> Self {
680 Self::new()
681 }
682}
683
684impl CpuGovernorBlock {
685 #[must_use]
687 pub fn new() -> Self {
688 Self {
689 governor: CpuGovernor::Unknown,
690 }
691 }
692
693 pub fn set_governor(&mut self, name: &str) {
695 self.governor = CpuGovernor::from_name(name);
696 }
697
698 #[must_use]
700 pub fn governor(&self) -> CpuGovernor {
701 self.governor
702 }
703}
704
705impl ComputeBlock for CpuGovernorBlock {
706 type Input = String;
707 type Output = CpuGovernor;
708
709 fn compute(&mut self, input: &Self::Input) -> Self::Output {
710 self.set_governor(input);
711 self.governor
712 }
713
714 fn latency_budget_us(&self) -> u64 {
715 10 }
717}
718
719#[derive(Debug, Clone, Copy, PartialEq, Eq)]
721pub enum CpuGovernor {
722 Performance,
724 Powersave,
726 Ondemand,
728 Conservative,
730 Schedutil,
732 Userspace,
734 Unknown,
736}
737
738impl CpuGovernor {
739 #[must_use]
741 pub fn from_name(name: &str) -> Self {
742 match name.trim().to_lowercase().as_str() {
743 "performance" => Self::Performance,
744 "powersave" => Self::Powersave,
745 "ondemand" => Self::Ondemand,
746 "conservative" => Self::Conservative,
747 "schedutil" => Self::Schedutil,
748 "userspace" => Self::Userspace,
749 _ => Self::Unknown,
750 }
751 }
752
753 #[must_use]
755 pub const fn as_str(&self) -> &'static str {
756 match self {
757 Self::Performance => "performance",
758 Self::Powersave => "powersave",
759 Self::Ondemand => "ondemand",
760 Self::Conservative => "conservative",
761 Self::Schedutil => "schedutil",
762 Self::Userspace => "userspace",
763 Self::Unknown => "unknown",
764 }
765 }
766
767 #[must_use]
769 pub const fn short_name(self) -> &'static str {
770 match self {
771 Self::Performance => "perf",
772 Self::Powersave => "psav",
773 Self::Ondemand => "odmd",
774 Self::Conservative => "cons",
775 Self::Schedutil => "schu",
776 Self::Userspace => "user",
777 Self::Unknown => "????",
778 }
779 }
780
781 #[must_use]
783 pub const fn icon(self) -> char {
784 match self {
785 Self::Performance => '🚀',
786 Self::Powersave => '🔋',
787 Self::Ondemand => '⚡',
788 Self::Conservative => '📊',
789 Self::Schedutil => '📅',
790 Self::Userspace => '👤',
791 Self::Unknown => '?',
792 }
793 }
794}
795
796#[derive(Debug, Clone)]
800pub struct MemPressureBlock {
801 avg10_some: f32,
803 avg60_some: f32,
805 avg300_some: f32,
807 avg10_full: f32,
809 instruction_set: SimdInstructionSet,
811}
812
813impl Default for MemPressureBlock {
814 fn default() -> Self {
815 Self::new()
816 }
817}
818
819impl MemPressureBlock {
820 #[must_use]
822 pub fn new() -> Self {
823 Self {
824 avg10_some: 0.0,
825 avg60_some: 0.0,
826 avg300_some: 0.0,
827 avg10_full: 0.0,
828 instruction_set: SimdInstructionSet::detect(),
829 }
830 }
831
832 pub fn set_pressure(
834 &mut self,
835 avg10_some: f32,
836 avg60_some: f32,
837 avg300_some: f32,
838 avg10_full: f32,
839 ) {
840 debug_assert!(avg10_some >= 0.0, "avg10_some must be non-negative");
841 debug_assert!(avg60_some >= 0.0, "avg60_some must be non-negative");
842 debug_assert!(avg300_some >= 0.0, "avg300_some must be non-negative");
843 debug_assert!(avg10_full >= 0.0, "avg10_full must be non-negative");
844 self.avg10_some = avg10_some;
845 self.avg60_some = avg60_some;
846 self.avg300_some = avg300_some;
847 self.avg10_full = avg10_full;
848 }
849
850 #[must_use]
852 pub fn pressure_level(&self) -> MemoryPressureLevel {
853 let pct = self.avg10_some;
854 if pct >= 50.0 {
855 MemoryPressureLevel::Critical
856 } else if pct >= 25.0 {
857 MemoryPressureLevel::High
858 } else if pct >= 10.0 {
859 MemoryPressureLevel::Medium
860 } else if pct >= 1.0 {
861 MemoryPressureLevel::Low
862 } else {
863 MemoryPressureLevel::None
864 }
865 }
866
867 #[must_use]
869 pub fn trend(&self) -> TrendDirection {
870 let diff = self.avg10_some - self.avg300_some;
871 if diff > 5.0 {
872 TrendDirection::Up
873 } else if diff < -5.0 {
874 TrendDirection::Down
875 } else {
876 TrendDirection::Flat
877 }
878 }
879}
880
881impl ComputeBlock for MemPressureBlock {
882 type Input = (f32, f32, f32, f32); type Output = MemoryPressureLevel;
884
885 fn compute(&mut self, input: &Self::Input) -> Self::Output {
886 self.set_pressure(input.0, input.1, input.2, input.3);
887 self.pressure_level()
888 }
889
890 fn simd_instruction_set(&self) -> SimdInstructionSet {
891 self.instruction_set
892 }
893
894 fn latency_budget_us(&self) -> u64 {
895 20 }
897}
898
899#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901pub enum MemoryPressureLevel {
902 None,
904 Low,
906 Medium,
908 High,
910 Critical,
912}
913
914impl MemoryPressureLevel {
915 #[allow(clippy::match_same_arms)]
917 pub fn symbol(&self) -> char {
918 match self {
919 Self::None => ' ',
920 Self::Low => '○',
921 Self::Medium => '◐',
922 Self::High => '◕',
923 Self::Critical => '●',
924 }
925 }
926
927 #[must_use]
929 pub const fn severity(self) -> u8 {
930 match self {
931 Self::None => 0,
932 Self::Low => 1,
933 Self::Medium => 2,
934 Self::High => 3,
935 Self::Critical => 4,
936 }
937 }
938}
939
940#[derive(Debug, Clone)]
944pub struct HugePagesBlock {
945 total: u64,
947 free: u64,
949 reserved: u64,
951 page_size_kb: u64,
953}
954
955impl Default for HugePagesBlock {
956 fn default() -> Self {
957 Self::new()
958 }
959}
960
961impl HugePagesBlock {
962 #[must_use]
964 pub fn new() -> Self {
965 Self {
966 total: 0,
967 free: 0,
968 reserved: 0,
969 page_size_kb: 2048, }
971 }
972
973 pub fn set_values(&mut self, total: u64, free: u64, reserved: u64, page_size_kb: u64) {
975 debug_assert!(free <= total, "free must be <= total");
976 debug_assert!(page_size_kb > 0, "page_size_kb must be positive");
977 self.total = total;
978 self.free = free;
979 self.reserved = reserved;
980 self.page_size_kb = page_size_kb;
981 }
982
983 #[must_use]
985 pub fn usage_percent(&self) -> f32 {
986 if self.total == 0 {
987 0.0
988 } else {
989 ((self.total - self.free) as f32 / self.total as f32 * 100.0).clamp(0.0, 100.0)
990 }
991 }
992
993 #[must_use]
995 pub fn total_bytes(&self) -> u64 {
996 self.total * self.page_size_kb * 1024
997 }
998
999 #[must_use]
1001 pub fn used_bytes(&self) -> u64 {
1002 (self.total - self.free) * self.page_size_kb * 1024
1003 }
1004}
1005
1006impl ComputeBlock for HugePagesBlock {
1007 type Input = (u64, u64, u64, u64); type Output = f32; fn compute(&mut self, input: &Self::Input) -> Self::Output {
1011 self.set_values(input.0, input.1, input.2, input.3);
1012 self.usage_percent()
1013 }
1014
1015 fn latency_budget_us(&self) -> u64 {
1016 10 }
1018}
1019
1020#[derive(Debug, Clone)]
1024pub struct GpuThermalBlock {
1025 temperature_c: f32,
1027 power_w: f32,
1029 power_limit_w: f32,
1031 temp_history: Vec<f32>,
1033 instruction_set: SimdInstructionSet,
1035}
1036
1037impl Default for GpuThermalBlock {
1038 fn default() -> Self {
1039 Self::new()
1040 }
1041}
1042
1043impl GpuThermalBlock {
1044 #[must_use]
1046 pub fn new() -> Self {
1047 Self {
1048 temperature_c: 0.0,
1049 power_w: 0.0,
1050 power_limit_w: 0.0,
1051 temp_history: Vec::with_capacity(60),
1052 instruction_set: SimdInstructionSet::detect(),
1053 }
1054 }
1055
1056 pub fn set_values(&mut self, temp_c: f32, power_w: f32, power_limit_w: f32) {
1058 debug_assert!(power_w >= 0.0, "power_w must be non-negative");
1059 debug_assert!(power_limit_w >= 0.0, "power_limit_w must be non-negative");
1060 self.temperature_c = temp_c;
1061 self.power_w = power_w;
1062 self.power_limit_w = power_limit_w;
1063
1064 if self.temp_history.len() >= 60 {
1066 self.temp_history.remove(0);
1067 }
1068 self.temp_history.push(temp_c);
1069 }
1070
1071 #[must_use]
1073 pub fn thermal_state(&self) -> GpuThermalState {
1074 if self.temperature_c >= 90.0 {
1075 GpuThermalState::Critical
1076 } else if self.temperature_c >= 80.0 {
1077 GpuThermalState::Hot
1078 } else if self.temperature_c >= 70.0 {
1079 GpuThermalState::Warm
1080 } else if self.temperature_c >= 50.0 {
1081 GpuThermalState::Normal
1082 } else {
1083 GpuThermalState::Cool
1084 }
1085 }
1086
1087 #[must_use]
1089 pub fn power_percent(&self) -> f32 {
1090 if self.power_limit_w > 0.0 {
1091 (self.power_w / self.power_limit_w * 100.0).clamp(0.0, 100.0)
1092 } else {
1093 0.0
1094 }
1095 }
1096
1097 #[must_use]
1099 pub fn trend(&self) -> TrendDirection {
1100 if self.temp_history.len() < 5 {
1101 return TrendDirection::Flat;
1102 }
1103
1104 let recent: f32 = self.temp_history.iter().rev().take(5).sum::<f32>() / 5.0;
1105 let older: f32 = self.temp_history.iter().rev().skip(5).take(5).sum::<f32>() / 5.0;
1106
1107 let diff = recent - older;
1108 if diff > 2.0 {
1109 TrendDirection::Up
1110 } else if diff < -2.0 {
1111 TrendDirection::Down
1112 } else {
1113 TrendDirection::Flat
1114 }
1115 }
1116}
1117
1118impl ComputeBlock for GpuThermalBlock {
1119 type Input = (f32, f32, f32); type Output = GpuThermalState;
1121
1122 fn compute(&mut self, input: &Self::Input) -> Self::Output {
1123 self.set_values(input.0, input.1, input.2);
1124 self.thermal_state()
1125 }
1126
1127 fn simd_instruction_set(&self) -> SimdInstructionSet {
1128 self.instruction_set
1129 }
1130
1131 fn latency_budget_us(&self) -> u64 {
1132 30 }
1134}
1135
1136#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1138pub enum GpuThermalState {
1139 #[default]
1141 Cool,
1142 Normal,
1144 Warm,
1146 Hot,
1148 Critical,
1150}
1151
1152impl GpuThermalState {
1153 #[must_use]
1155 pub const fn indicator(self) -> char {
1156 match self {
1157 Self::Cool => '❄',
1158 Self::Normal => '●',
1159 Self::Warm => '◐',
1160 Self::Hot => '◕',
1161 Self::Critical => '🔥',
1162 }
1163 }
1164
1165 #[must_use]
1167 pub const fn severity(self) -> u8 {
1168 match self {
1169 Self::Cool => 0,
1170 Self::Normal => 1,
1171 Self::Warm => 2,
1172 Self::Hot => 3,
1173 Self::Critical => 4,
1174 }
1175 }
1176}
1177
1178#[derive(Debug, Clone)]
1182pub struct GpuVramBlock {
1183 total_mb: u64,
1185 used_mb: u64,
1187 per_process: Vec<(u32, u64, String)>, }
1190
1191impl Default for GpuVramBlock {
1192 fn default() -> Self {
1193 Self::new()
1194 }
1195}
1196
1197impl GpuVramBlock {
1198 #[must_use]
1200 pub fn new() -> Self {
1201 Self {
1202 total_mb: 0,
1203 used_mb: 0,
1204 per_process: Vec::new(),
1205 }
1206 }
1207
1208 pub fn set_values(
1210 &mut self,
1211 total_mb: u64,
1212 used_mb: u64,
1213 per_process: Vec<(u32, u64, String)>,
1214 ) {
1215 self.total_mb = total_mb;
1216 self.used_mb = used_mb;
1217 self.per_process = per_process;
1218 }
1219
1220 #[must_use]
1222 pub fn usage_percent(&self) -> f32 {
1223 if self.total_mb == 0 {
1224 0.0
1225 } else {
1226 (self.used_mb as f32 / self.total_mb as f32 * 100.0).clamp(0.0, 100.0)
1227 }
1228 }
1229
1230 #[must_use]
1232 pub fn top_consumers(&self, n: usize) -> Vec<&(u32, u64, String)> {
1233 let mut sorted: Vec<_> = self.per_process.iter().collect();
1234 sorted.sort_by(|a, b| b.1.cmp(&a.1));
1235 sorted.into_iter().take(n).collect()
1236 }
1237}
1238
1239impl ComputeBlock for GpuVramBlock {
1240 type Input = (u64, u64, Vec<(u32, u64, String)>);
1241 type Output = f32; fn compute(&mut self, input: &Self::Input) -> Self::Output {
1244 self.set_values(input.0, input.1, input.2.clone());
1245 self.usage_percent()
1246 }
1247
1248 fn latency_budget_us(&self) -> u64 {
1249 100 }
1251}
1252
1253#[derive(Debug, Clone, Default)]
1284pub struct MetricsCache {
1285 pub cpu: CpuMetricsCache,
1287 pub memory: MemoryMetricsCache,
1289 pub process: ProcessMetricsCache,
1291 pub network: NetworkMetricsCache,
1293 pub gpu: GpuMetricsCache,
1295 pub frame_id: u64,
1297 pub updated_at_us: u64,
1299}
1300
1301#[derive(Debug, Clone, Default)]
1303pub struct CpuMetricsCache {
1304 pub avg_usage: f32,
1306 pub max_core_usage: f32,
1308 pub hot_cores: u32,
1310 pub load_avg: [f32; 3],
1312 pub freq_ghz: f32,
1314 pub trend: TrendDirection,
1316}
1317
1318#[derive(Debug, Clone, Default)]
1320pub struct MemoryMetricsCache {
1321 pub usage_percent: f32,
1323 pub used_bytes: u64,
1325 pub total_bytes: u64,
1327 pub cached_bytes: u64,
1329 pub swap_percent: f32,
1331 pub zram_ratio: f32,
1333 pub trend: TrendDirection,
1335}
1336
1337#[derive(Debug, Clone, Default)]
1339pub struct ProcessMetricsCache {
1340 pub total_count: u32,
1342 pub running_count: u32,
1344 pub sleeping_count: u32,
1346 pub top_cpu: Option<(u32, f32, String)>,
1348 pub top_mem: Option<(u32, f32, String)>,
1350 pub total_cpu_usage: f32,
1352}
1353
1354#[derive(Debug, Clone, Default)]
1356pub struct NetworkMetricsCache {
1357 pub interface: String,
1359 pub rx_bytes_sec: u64,
1361 pub tx_bytes_sec: u64,
1363 pub total_rx: u64,
1365 pub total_tx: u64,
1367 pub connection_count: u32,
1369}
1370
1371#[derive(Debug, Clone, Default)]
1373pub struct GpuMetricsCache {
1374 pub name: String,
1376 pub usage_percent: f32,
1378 pub vram_percent: f32,
1380 pub temp_c: f32,
1382 pub power_w: f32,
1384 pub thermal_state: GpuThermalState,
1386}
1387
1388impl MetricsCache {
1389 #[must_use]
1391 pub fn new() -> Self {
1392 Self::default()
1393 }
1394
1395 #[must_use]
1397 pub fn is_stale(&self, current_time_us: u64, max_age_us: u64) -> bool {
1398 current_time_us.saturating_sub(self.updated_at_us) > max_age_us
1399 }
1400
1401 pub fn update_cpu(
1403 &mut self,
1404 per_core: &[f64],
1405 load_avg: [f32; 3],
1406 freq_ghz: f32,
1407 frame_id: u64,
1408 ) {
1409 if per_core.is_empty() {
1410 return;
1411 }
1412
1413 let sum: f64 = per_core.iter().sum();
1415 let max: f64 = per_core.iter().copied().fold(0.0, f64::max);
1416 let hot_cores = per_core.iter().filter(|&&c| c > 90.0).count();
1417
1418 self.cpu.avg_usage = (sum / per_core.len() as f64) as f32;
1419 self.cpu.max_core_usage = max as f32;
1420 self.cpu.hot_cores = hot_cores as u32;
1421 self.cpu.load_avg = load_avg;
1422 self.cpu.freq_ghz = freq_ghz;
1423 self.frame_id = frame_id;
1424 }
1425
1426 pub fn update_memory(
1428 &mut self,
1429 used: u64,
1430 total: u64,
1431 cached: u64,
1432 swap_used: u64,
1433 swap_total: u64,
1434 zram_ratio: f32,
1435 ) {
1436 self.memory.used_bytes = used;
1437 self.memory.total_bytes = total;
1438 self.memory.cached_bytes = cached;
1439 self.memory.usage_percent = if total > 0 {
1440 used as f32 / total as f32 * 100.0
1441 } else {
1442 0.0
1443 };
1444 self.memory.swap_percent = if swap_total > 0 {
1445 swap_used as f32 / swap_total as f32 * 100.0
1446 } else {
1447 0.0
1448 };
1449 self.memory.zram_ratio = zram_ratio;
1450 }
1451
1452 pub fn update_process(
1454 &mut self,
1455 total: u32,
1456 running: u32,
1457 sleeping: u32,
1458 top_cpu: Option<(u32, f32, String)>,
1459 top_mem: Option<(u32, f32, String)>,
1460 total_cpu: f32,
1461 ) {
1462 self.process.total_count = total;
1463 self.process.running_count = running;
1464 self.process.sleeping_count = sleeping;
1465 self.process.top_cpu = top_cpu;
1466 self.process.top_mem = top_mem;
1467 self.process.total_cpu_usage = total_cpu;
1468 }
1469
1470 pub fn update_network(
1472 &mut self,
1473 interface: String,
1474 rx_rate: u64,
1475 tx_rate: u64,
1476 total_rx: u64,
1477 total_tx: u64,
1478 conn_count: u32,
1479 ) {
1480 self.network.interface = interface;
1481 self.network.rx_bytes_sec = rx_rate;
1482 self.network.tx_bytes_sec = tx_rate;
1483 self.network.total_rx = total_rx;
1484 self.network.total_tx = total_tx;
1485 self.network.connection_count = conn_count;
1486 }
1487
1488 pub fn update_gpu(&mut self, name: String, usage: f32, vram: f32, temp: f32, power: f32) {
1490 self.gpu.name = name;
1491 self.gpu.usage_percent = usage;
1492 self.gpu.vram_percent = vram;
1493 self.gpu.temp_c = temp;
1494 self.gpu.power_w = power;
1495 self.gpu.thermal_state = if temp >= 90.0 {
1496 GpuThermalState::Critical
1497 } else if temp >= 80.0 {
1498 GpuThermalState::Hot
1499 } else if temp >= 70.0 {
1500 GpuThermalState::Warm
1501 } else if temp >= 50.0 {
1502 GpuThermalState::Normal
1503 } else {
1504 GpuThermalState::Cool
1505 };
1506 }
1507
1508 pub fn mark_updated(&mut self, timestamp_us: u64) {
1510 self.updated_at_us = timestamp_us;
1511 }
1512}
1513
1514#[derive(Debug, Clone, Default)]
1516pub struct MetricsCacheBlock {
1517 cache: MetricsCache,
1518 instruction_set: SimdInstructionSet,
1519}
1520
1521impl MetricsCacheBlock {
1522 #[must_use]
1524 pub fn new() -> Self {
1525 Self {
1526 cache: MetricsCache::new(),
1527 instruction_set: SimdInstructionSet::detect(),
1528 }
1529 }
1530
1531 #[must_use]
1533 pub fn cache(&self) -> &MetricsCache {
1534 &self.cache
1535 }
1536
1537 pub fn cache_mut(&mut self) -> &mut MetricsCache {
1539 &mut self.cache
1540 }
1541}
1542
1543impl ComputeBlock for MetricsCacheBlock {
1544 type Input = (); type Output = MetricsCache;
1546
1547 fn compute(&mut self, _input: &Self::Input) -> Self::Output {
1548 self.cache.clone()
1549 }
1550
1551 fn simd_instruction_set(&self) -> SimdInstructionSet {
1552 self.instruction_set
1553 }
1554
1555 fn latency_budget_us(&self) -> u64 {
1556 1 }
1558}
1559
1560#[cfg(test)]
1561#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
1562#[path = "compute_block_tests.rs"]
1563mod tests;