Skip to main content

presentar_terminal/
compute_block.rs

1//! `ComputeBlock`: SIMD-optimized panel element trait
2//!
3//! Implements the `ComputeBlock` architecture from SPEC-024 Section 21.6.
4//! All panel elements (sparklines, gauges, etc.) implement this trait
5//! to enable SIMD optimization where available.
6//!
7//! ## Architecture
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────┐
11//! │  ComputeBlock Trait                                     │
12//! │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐  │
13//! │  │ Input Data  │→ │ SIMD Kernel │→ │ Rendered Output │  │
14//! │  │ (f32 array) │  │ (AVX2/NEON) │  │ (block chars)   │  │
15//! │  └─────────────┘  └─────────────┘  └─────────────────┘  │
16//! └─────────────────────────────────────────────────────────┘
17//! ```
18//!
19//! ## SIMD Instruction Sets Supported
20//!
21//! | Platform | Instruction Set | Vector Width |
22//! |----------|-----------------|--------------|
23//! | `x86_64`   | AVX2            | 256-bit (8×f32) |
24//! | `x86_64`   | SSE4.1          | 128-bit (4×f32) |
25//! | aarch64  | NEON            | 128-bit (4×f32) |
26//! | wasm32   | SIMD128         | 128-bit (4×f32) |
27//!
28//! ## Peer-Reviewed Foundation
29//!
30//! - Intel Intrinsics Guide (2024): AVX2 intrinsics for f32x8
31//! - Fog, A. (2023): SIMD optimization patterns
32//! - Hennessy & Patterson (2017): Memory hierarchy optimization
33
34/// SIMD instruction set identifier
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum SimdInstructionSet {
37    /// No SIMD available (scalar fallback)
38    Scalar,
39    /// `x86_64` SSE4.1 (128-bit, 4×f32)
40    Sse4,
41    /// `x86_64` AVX2 (256-bit, 8×f32)
42    Avx2,
43    /// `x86_64` AVX-512 (512-bit, 16×f32)
44    Avx512,
45    /// ARM NEON (128-bit, 4×f32)
46    Neon,
47    /// WebAssembly SIMD128 (128-bit, 4×f32)
48    WasmSimd128,
49}
50
51impl SimdInstructionSet {
52    /// Get the vector width in f32 elements
53    #[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    /// Detect the best available instruction set at runtime
64    #[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            // NEON is always available on aarch64
83            return Self::Neon;
84        }
85
86        #[cfg(target_arch = "wasm32")]
87        {
88            // WASM SIMD is compile-time feature
89            #[cfg(target_feature = "simd128")]
90            return Self::WasmSimd128;
91        }
92
93        Self::Scalar
94    }
95
96    /// Get the instruction set name as a static string
97    #[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
116/// `ComputeBlock` trait for SIMD-optimized panel elements
117///
118/// All panel elements that benefit from SIMD optimization implement
119/// this trait. The trait provides a common interface for:
120/// - Computing output from input data
121/// - Querying SIMD support
122/// - Measuring compute latency
123///
124/// ## Example
125///
126/// ```ignore
127/// struct SparklineBlock {
128///     history: Vec<f32>,
129/// }
130///
131/// impl ComputeBlock for SparklineBlock {
132///     type Input = f32;
133///     type Output = Vec<char>;
134///
135///     fn compute(&mut self, input: &Self::Input) -> Self::Output {
136///         self.history.push(*input);
137///         // SIMD-optimized normalization and character mapping
138///         self.render_blocks()
139///     }
140/// }
141/// ```
142pub trait ComputeBlock {
143    /// Input type for this compute block
144    type Input;
145    /// Output type produced by this compute block
146    type Output;
147
148    /// Process input data and produce output
149    ///
150    /// Implementations should use SIMD where available for optimal
151    /// performance. The `simd_instruction_set()` method indicates
152    /// which instruction set is being used.
153    fn compute(&mut self, input: &Self::Input) -> Self::Output;
154
155    /// Query if this block supports SIMD on the current CPU
156    fn simd_supported(&self) -> bool {
157        self.simd_instruction_set() != SimdInstructionSet::Scalar
158    }
159
160    /// Get the SIMD instruction set used by this block
161    fn simd_instruction_set(&self) -> SimdInstructionSet {
162        SimdInstructionSet::detect()
163    }
164
165    /// Get the compute latency budget in microseconds
166    fn latency_budget_us(&self) -> u64 {
167        1000 // Default 1ms budget
168    }
169}
170
171/// `ComputeBlock` ID as specified in SPEC-024 Section 21
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173pub enum ComputeBlockId {
174    // CPU Panel (CB-CPU-*)
175    CpuSparklines,     // CB-CPU-001
176    CpuLoadGauge,      // CB-CPU-002
177    CpuLoadTrend,      // CB-CPU-003
178    CpuFrequency,      // CB-CPU-004
179    CpuBoostIndicator, // CB-CPU-005
180    CpuTemperature,    // CB-CPU-006
181    CpuTopConsumers,   // CB-CPU-007
182
183    // Memory Panel (CB-MEM-*)
184    MemSparklines,     // CB-MEM-001
185    MemZramRatio,      // CB-MEM-002
186    MemPressureGauge,  // CB-MEM-003
187    MemSwapThrashing,  // CB-MEM-004
188    MemCacheBreakdown, // CB-MEM-005
189    MemHugePages,      // CB-MEM-006
190
191    // Connections Panel (CB-CONN-*)
192    ConnAge,          // CB-CONN-001
193    ConnProc,         // CB-CONN-002
194    ConnGeo,          // CB-CONN-003
195    ConnLatency,      // CB-CONN-004
196    ConnService,      // CB-CONN-005
197    ConnHotIndicator, // CB-CONN-006
198    ConnSparkline,    // CB-CONN-007
199
200    // Network Panel (CB-NET-*)
201    NetSparklines,    // CB-NET-001
202    NetProtocolStats, // CB-NET-002
203    NetErrorRate,     // CB-NET-003
204    NetDropRate,      // CB-NET-004
205    NetLatencyGauge,  // CB-NET-005
206    NetBandwidthUtil, // CB-NET-006
207
208    // Process Panel (CB-PROC-*)
209    ProcTreeView,      // CB-PROC-001
210    ProcSortIndicator, // CB-PROC-002
211    ProcFilter,        // CB-PROC-003
212    ProcOomScore,      // CB-PROC-004
213    ProcNiceValue,     // CB-PROC-005
214    ProcThreadCount,   // CB-PROC-006
215    ProcCgroup,        // CB-PROC-007
216}
217
218impl ComputeBlockId {
219    /// Get the string ID (e.g., "CB-CPU-001")
220    #[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    /// Check if this block is SIMD-vectorizable
260    #[must_use]
261    pub const fn simd_vectorizable(&self) -> bool {
262        match self {
263            // YES - can use SIMD
264            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            // NO - scalar only
288            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/// Sparkline `ComputeBlock` (CB-CPU-001, CB-MEM-001, CB-NET-001, CB-CONN-007)
304///
305/// SIMD-optimized sparkline rendering using 8-level block characters.
306/// Uses AVX2 for min/max/normalization when available.
307#[derive(Debug, Clone)]
308#[allow(dead_code)]
309pub struct SparklineBlock {
310    /// History buffer (60 samples = 60 seconds at 1Hz)
311    history: Vec<f32>,
312    /// Maximum history length
313    max_samples: usize,
314    /// SIMD buffer for aligned operations
315    simd_buffer: [f32; 8],
316    /// Detected instruction set
317    instruction_set: SimdInstructionSet,
318}
319
320impl Default for SparklineBlock {
321    fn default() -> Self {
322        Self::new(60)
323    }
324}
325
326impl SparklineBlock {
327    /// Create a new sparkline block with given history length
328    #[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    /// Add a sample to the history
340    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    /// Get the current history
348    #[must_use]
349    pub fn history(&self) -> &[f32] {
350        &self.history
351    }
352
353    /// Render the sparkline as block characters
354    #[must_use]
355    pub fn render(&self, width: usize) -> Vec<char> {
356        if self.history.is_empty() {
357            return vec![' '; width];
358        }
359
360        // SIMD-optimized min/max finding
361        let (min, max) = self.find_min_max();
362        let range = max - min;
363
364        // Sample history to fit width
365        let samples = self.sample_to_width(width);
366
367        // Map to block characters
368        #[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] // Mid-level if no variation
376                } 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    /// Find min/max using SIMD when available
386    fn find_min_max(&self) -> (f32, f32) {
387        if self.history.is_empty() {
388            return (0.0, 1.0);
389        }
390
391        // Scalar implementation - SIMD intrinsics for AVX2 can be added
392        // in a future optimization pass if profiling shows this as a hotspot
393        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    /// Sample history to fit target width
404    fn sample_to_width(&self, width: usize) -> Vec<f32> {
405        if self.history.len() <= width {
406            // Pad with zeros if history is shorter
407            let mut result = vec![0.0; width - self.history.len()];
408            result.extend_from_slice(&self.history);
409            result
410        } else {
411            // Downsample using linear interpolation
412            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 // 100μs budget for sparkline rendering
438    }
439}
440
441/// Load Trend `ComputeBlock` (CB-CPU-003)
442///
443/// Computes the derivative of load average to show trend direction.
444#[derive(Debug, Clone)]
445pub struct LoadTrendBlock {
446    /// Previous load values for derivative calculation
447    history: Vec<f32>,
448    /// Smoothing window size
449    window_size: usize,
450}
451
452impl Default for LoadTrendBlock {
453    fn default() -> Self {
454        Self::new(5)
455    }
456}
457
458impl LoadTrendBlock {
459    /// Create a new load trend block
460    #[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    /// Get the trend direction
470    #[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 // Very fast operation
515    }
516}
517
518/// Trend direction for load/usage indicators
519#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
520pub enum TrendDirection {
521    /// Trending up (↑)
522    Up,
523    /// Trending down (↓)
524    Down,
525    /// Stable/flat (→)
526    #[default]
527    Flat,
528}
529
530impl TrendDirection {
531    /// Get the arrow character for this trend
532    #[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        // Should detect something (at minimum Scalar)
550        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        // Should only keep last 5
598        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        // First should be lowest, last should be highest
611        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        // Just verify it doesn't panic
633        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// =============================================================================
694// Additional ComputeBlocks (SPEC-024 Part VI: Grammar of Graphics)
695// =============================================================================
696
697/// CPU Frequency `ComputeBlock` (CB-CPU-004)
698///
699/// Tracks per-core CPU frequencies and detects frequency scaling state.
700/// Per-core frequency data from `/sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq`.
701#[derive(Debug, Clone)]
702pub struct CpuFrequencyBlock {
703    /// Per-core frequencies in MHz
704    frequencies: Vec<u32>,
705    /// Per-core max frequencies in MHz (for percentage calculation)
706    max_frequencies: Vec<u32>,
707    /// Detected instruction set
708    instruction_set: SimdInstructionSet,
709}
710
711impl Default for CpuFrequencyBlock {
712    fn default() -> Self {
713        Self::new()
714    }
715}
716
717impl CpuFrequencyBlock {
718    /// Create a new CPU frequency block
719    #[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    /// Set per-core frequencies
729    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    /// Get frequencies as percentages of max
735    #[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    /// Get scaling state indicator for each core
751    #[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>); // (cur_freqs, max_freqs)
774    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 // 50μs budget for frequency processing
787    }
788}
789
790/// Frequency scaling state indicators
791#[derive(Debug, Clone, Copy, PartialEq, Eq)]
792pub enum FrequencyScalingState {
793    /// Turbo/boost mode active (⚡)
794    Turbo,
795    /// High frequency (↑)
796    High,
797    /// Normal frequency (→)
798    Normal,
799    /// Scaled down (↓)
800    Scaled,
801    /// Idle/very low (·)
802    Idle,
803}
804
805impl FrequencyScalingState {
806    /// Get the indicator character
807    #[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/// CPU Governor `ComputeBlock` (CB-CPU-008)
820///
821/// Tracks CPU governor state from `/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor`.
822#[derive(Debug, Clone)]
823pub struct CpuGovernorBlock {
824    /// Current governor name
825    governor: CpuGovernor,
826}
827
828impl Default for CpuGovernorBlock {
829    fn default() -> Self {
830        Self::new()
831    }
832}
833
834impl CpuGovernorBlock {
835    /// Create a new CPU governor block
836    #[must_use]
837    pub fn new() -> Self {
838        Self {
839            governor: CpuGovernor::Unknown,
840        }
841    }
842
843    /// Set governor from string
844    pub fn set_governor(&mut self, name: &str) {
845        self.governor = CpuGovernor::from_name(name);
846    }
847
848    /// Get current governor
849    #[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 // Very fast string parsing
866    }
867}
868
869/// CPU Governor types
870#[derive(Debug, Clone, Copy, PartialEq, Eq)]
871pub enum CpuGovernor {
872    /// Performance - max frequency
873    Performance,
874    /// Powersave - min frequency
875    Powersave,
876    /// Ondemand - dynamic scaling
877    Ondemand,
878    /// Conservative - gradual scaling
879    Conservative,
880    /// Schedutil - scheduler-based
881    Schedutil,
882    /// Userspace - user-controlled
883    Userspace,
884    /// Unknown governor
885    Unknown,
886}
887
888impl CpuGovernor {
889    /// Parse governor from name
890    #[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    /// Get governor name as string
904    #[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    /// Get short display name
918    #[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    /// Get icon for governor
932    #[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/// Memory Pressure `ComputeBlock` (CB-MEM-003)
947///
948/// Tracks memory pressure from `/proc/pressure/memory`.
949#[derive(Debug, Clone)]
950pub struct MemPressureBlock {
951    /// Average pressure over 10 seconds (some)
952    avg10_some: f32,
953    /// Average pressure over 60 seconds (some)
954    avg60_some: f32,
955    /// Average pressure over 300 seconds (some)
956    avg300_some: f32,
957    /// Average pressure over 10 seconds (full)
958    avg10_full: f32,
959    /// Instruction set
960    instruction_set: SimdInstructionSet,
961}
962
963impl Default for MemPressureBlock {
964    fn default() -> Self {
965        Self::new()
966    }
967}
968
969impl MemPressureBlock {
970    /// Create a new memory pressure block
971    #[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    /// Set pressure values
983    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    /// Get pressure level indicator
1001    #[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    /// Get trend from 300s to 10s averages
1018    #[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); // (avg10_some, avg60_some, avg300_some, avg10_full)
1033    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 // Simple comparisons
1046    }
1047}
1048
1049/// Memory pressure level
1050#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1051pub enum MemoryPressureLevel {
1052    /// No pressure
1053    None,
1054    /// Low pressure (1-10%)
1055    Low,
1056    /// Medium pressure (10-25%)
1057    Medium,
1058    /// High pressure (25-50%)
1059    High,
1060    /// Critical pressure (>50%)
1061    Critical,
1062}
1063
1064impl MemoryPressureLevel {
1065    /// Get the display character for this pressure level
1066    #[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    /// Get color index (0=green, 4=red)
1078    #[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/// Huge Pages `ComputeBlock` (CB-MEM-006)
1091///
1092/// Tracks huge page usage from `/proc/meminfo`.
1093#[derive(Debug, Clone)]
1094pub struct HugePagesBlock {
1095    /// Total huge pages
1096    total: u64,
1097    /// Free huge pages
1098    free: u64,
1099    /// Reserved huge pages
1100    reserved: u64,
1101    /// Huge page size in KB
1102    page_size_kb: u64,
1103}
1104
1105impl Default for HugePagesBlock {
1106    fn default() -> Self {
1107        Self::new()
1108    }
1109}
1110
1111impl HugePagesBlock {
1112    /// Create a new huge pages block
1113    #[must_use]
1114    pub fn new() -> Self {
1115        Self {
1116            total: 0,
1117            free: 0,
1118            reserved: 0,
1119            page_size_kb: 2048, // Default 2MB huge pages
1120        }
1121    }
1122
1123    /// Set huge page values
1124    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    /// Get usage percentage
1134    #[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    /// Get total size in bytes
1144    #[must_use]
1145    pub fn total_bytes(&self) -> u64 {
1146        self.total * self.page_size_kb * 1024
1147    }
1148
1149    /// Get used size in bytes
1150    #[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); // (total, free, reserved, page_size_kb)
1158    type Output = f32; // Usage percentage
1159
1160    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 // Simple arithmetic
1167    }
1168}
1169
1170/// GPU Thermal `ComputeBlock` (CB-GPU-001)
1171///
1172/// Tracks GPU temperature and power draw.
1173#[derive(Debug, Clone)]
1174pub struct GpuThermalBlock {
1175    /// Temperature in Celsius
1176    temperature_c: f32,
1177    /// Power draw in Watts
1178    power_w: f32,
1179    /// Power limit in Watts
1180    power_limit_w: f32,
1181    /// History for trend
1182    temp_history: Vec<f32>,
1183    /// Instruction set
1184    instruction_set: SimdInstructionSet,
1185}
1186
1187impl Default for GpuThermalBlock {
1188    fn default() -> Self {
1189        Self::new()
1190    }
1191}
1192
1193impl GpuThermalBlock {
1194    /// Create a new GPU thermal block
1195    #[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    /// Set thermal values
1207    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        // Update history
1215        if self.temp_history.len() >= 60 {
1216            self.temp_history.remove(0);
1217        }
1218        self.temp_history.push(temp_c);
1219    }
1220
1221    /// Get thermal state
1222    #[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    /// Get power usage percentage
1238    #[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    /// Get temperature trend
1248    #[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); // (temp_c, power_w, power_limit_w)
1270    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 // Simple comparisons + history update
1283    }
1284}
1285
1286/// GPU thermal state
1287#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1288pub enum GpuThermalState {
1289    /// Cool (<50°C)
1290    #[default]
1291    Cool,
1292    /// Normal (50-70°C)
1293    Normal,
1294    /// Warm (70-80°C)
1295    Warm,
1296    /// Hot (80-90°C)
1297    Hot,
1298    /// Critical (>90°C)
1299    Critical,
1300}
1301
1302impl GpuThermalState {
1303    /// Get indicator character
1304    #[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    /// Get severity (0=cool, 4=critical)
1316    #[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/// GPU VRAM `ComputeBlock` (CB-GPU-002)
1329///
1330/// Tracks VRAM usage per process.
1331#[derive(Debug, Clone)]
1332pub struct GpuVramBlock {
1333    /// Total VRAM in MB
1334    total_mb: u64,
1335    /// Used VRAM in MB
1336    used_mb: u64,
1337    /// Per-process VRAM usage (PID -> MB)
1338    per_process: Vec<(u32, u64, String)>, // (pid, mb, name)
1339}
1340
1341impl Default for GpuVramBlock {
1342    fn default() -> Self {
1343        Self::new()
1344    }
1345}
1346
1347impl GpuVramBlock {
1348    /// Create a new VRAM block
1349    #[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    /// Set VRAM values
1359    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    /// Get usage percentage
1371    #[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    /// Get top N consumers by VRAM
1381    #[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; // Usage percentage
1392
1393    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 // Sorting may be needed
1400    }
1401}
1402
1403// Additional tests for new blocks
1404#[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        // Test all compute block IDs have valid strings
1649        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        // Test SIMD vectorizable blocks
1687        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        // Test non-SIMD blocks
1700        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        // All same value should render mid-level blocks
1725        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        // Render at width 5 - should pad with zeros
1737        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        // Render at width 5 - should downsample
1748        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        // Push more than window_size * 2
1762        for i in 0..20 {
1763            block.compute(&(i as f32));
1764        }
1765        // History should be limited
1766        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        // Only 1 sample, should be flat
1774        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        // Empty history should return defaults
1781        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// =============================================================================
1807// MetricsCacheBlock: O(1) Cached Metrics for ptop Performance
1808// =============================================================================
1809
1810/// Cached metrics snapshot for O(1) panel access.
1811///
1812/// This struct provides pre-computed, cached views of system metrics
1813/// to avoid redundant calculations during rendering. Per the spec:
1814/// "All metrics must be O(1) cached views, not O(n) per-frame refreshes."
1815///
1816/// # Architecture
1817///
1818/// ```text
1819/// ┌─────────────────────────────────────────────────────────────────┐
1820/// │  collect_metrics() [O(n)]  →  MetricsCache  →  render() [O(1)]  │
1821/// │                                                                  │
1822/// │  ┌─────────────┐     ┌─────────────┐     ┌─────────────────┐    │
1823/// │  │ /proc scan  │ →   │ SIMD Reduce │ →   │ Cached Summary  │    │
1824/// │  │ (2600 PIDs) │     │ (AVX2/NEON) │     │ (top 50, sums)  │    │
1825/// │  └─────────────┘     └─────────────┘     └─────────────────┘    │
1826/// └─────────────────────────────────────────────────────────────────┘
1827/// ```
1828///
1829/// # Performance Targets
1830///
1831/// | Operation | Target | Notes |
1832/// |-----------|--------|-------|
1833/// | Cache update | <100ms | Once per collect_metrics() |
1834/// | Cache read | <1μs | O(1) field access |
1835/// | Memory overhead | <1KB | Just aggregates, not full data |
1836#[derive(Debug, Clone, Default)]
1837pub struct MetricsCache {
1838    /// Cached CPU aggregate
1839    pub cpu: CpuMetricsCache,
1840    /// Cached memory aggregate
1841    pub memory: MemoryMetricsCache,
1842    /// Cached process aggregate
1843    pub process: ProcessMetricsCache,
1844    /// Cached network aggregate
1845    pub network: NetworkMetricsCache,
1846    /// Cached GPU aggregate
1847    pub gpu: GpuMetricsCache,
1848    /// Frame ID when cache was last updated
1849    pub frame_id: u64,
1850    /// Timestamp of last update (for cache invalidation)
1851    pub updated_at_us: u64,
1852}
1853
1854/// Cached CPU metrics
1855#[derive(Debug, Clone, Default)]
1856pub struct CpuMetricsCache {
1857    /// Average CPU usage across all cores (0-100)
1858    pub avg_usage: f32,
1859    /// Maximum core usage (for load display)
1860    pub max_core_usage: f32,
1861    /// Number of cores at >90% usage
1862    pub hot_cores: u32,
1863    /// Load average (1m, 5m, 15m)
1864    pub load_avg: [f32; 3],
1865    /// Current frequency (GHz)
1866    pub freq_ghz: f32,
1867    /// Trend direction
1868    pub trend: TrendDirection,
1869}
1870
1871/// Cached memory metrics
1872#[derive(Debug, Clone, Default)]
1873pub struct MemoryMetricsCache {
1874    /// Usage percentage (0-100)
1875    pub usage_percent: f32,
1876    /// Used bytes
1877    pub used_bytes: u64,
1878    /// Total bytes
1879    pub total_bytes: u64,
1880    /// Cached bytes
1881    pub cached_bytes: u64,
1882    /// Swap usage percentage
1883    pub swap_percent: f32,
1884    /// ZRAM compression ratio
1885    pub zram_ratio: f32,
1886    /// Trend direction
1887    pub trend: TrendDirection,
1888}
1889
1890/// Cached process metrics
1891#[derive(Debug, Clone, Default)]
1892pub struct ProcessMetricsCache {
1893    /// Total process count
1894    pub total_count: u32,
1895    /// Running process count
1896    pub running_count: u32,
1897    /// Sleeping process count
1898    pub sleeping_count: u32,
1899    /// Top CPU consumer (pid, cpu%, name)
1900    pub top_cpu: Option<(u32, f32, String)>,
1901    /// Top memory consumer (pid, mem%, name)
1902    pub top_mem: Option<(u32, f32, String)>,
1903    /// Sum of all CPU usage (for overhead display)
1904    pub total_cpu_usage: f32,
1905}
1906
1907/// Cached network metrics
1908#[derive(Debug, Clone, Default)]
1909pub struct NetworkMetricsCache {
1910    /// Primary interface name
1911    pub interface: String,
1912    /// RX rate (bytes/sec)
1913    pub rx_bytes_sec: u64,
1914    /// TX rate (bytes/sec)
1915    pub tx_bytes_sec: u64,
1916    /// Total RX bytes
1917    pub total_rx: u64,
1918    /// Total TX bytes
1919    pub total_tx: u64,
1920    /// Active connection count
1921    pub connection_count: u32,
1922}
1923
1924/// Cached GPU metrics
1925#[derive(Debug, Clone, Default)]
1926pub struct GpuMetricsCache {
1927    /// GPU name
1928    pub name: String,
1929    /// GPU usage percentage
1930    pub usage_percent: f32,
1931    /// VRAM usage percentage
1932    pub vram_percent: f32,
1933    /// Temperature in Celsius
1934    pub temp_c: f32,
1935    /// Power draw in Watts
1936    pub power_w: f32,
1937    /// Thermal state
1938    pub thermal_state: GpuThermalState,
1939}
1940
1941impl MetricsCache {
1942    /// Create a new empty cache
1943    #[must_use]
1944    pub fn new() -> Self {
1945        Self::default()
1946    }
1947
1948    /// Check if cache is stale (older than `max_age_us`)
1949    #[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    /// Update CPU cache from raw data
1955    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        // SIMD-friendly reduction (compiler can vectorize)
1967        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    /// Update memory cache from raw data
1980    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    /// Update process cache from raw data
2006    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    /// Update network cache from raw data
2024    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    /// Update GPU cache from raw data
2042    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    /// Set timestamp for cache freshness tracking
2062    pub fn mark_updated(&mut self, timestamp_us: u64) {
2063        self.updated_at_us = timestamp_us;
2064    }
2065}
2066
2067/// `ComputeBlock` wrapper for `MetricsCache` that provides O(1) access
2068#[derive(Debug, Clone, Default)]
2069pub struct MetricsCacheBlock {
2070    cache: MetricsCache,
2071    instruction_set: SimdInstructionSet,
2072}
2073
2074impl MetricsCacheBlock {
2075    /// Create a new metrics cache block
2076    #[must_use]
2077    pub fn new() -> Self {
2078        Self {
2079            cache: MetricsCache::new(),
2080            instruction_set: SimdInstructionSet::detect(),
2081        }
2082    }
2083
2084    /// Get immutable reference to the cache
2085    #[must_use]
2086    pub fn cache(&self) -> &MetricsCache {
2087        &self.cache
2088    }
2089
2090    /// Get mutable reference to the cache for updates
2091    pub fn cache_mut(&mut self) -> &mut MetricsCache {
2092        &mut self.cache
2093    }
2094}
2095
2096impl ComputeBlock for MetricsCacheBlock {
2097    type Input = (); // No input - cache is updated separately
2098    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 // O(1) access - should be <1μs
2110    }
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,  // 50GB used
2142            100_000_000_000, // 100GB total
2143            20_000_000_000,  // 20GB cached
2144            1_000_000_000,   // 1GB swap used
2145            10_000_000_000,  // 10GB swap total
2146            2.5,             // ZRAM ratio
2147        );
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, // total
2159            5,    // running
2160            900,  // sleeping
2161            Some((1234, 50.0, "chrome".to_string())),
2162            Some((5678, 25.0, "firefox".to_string())),
2163            150.0, // total CPU
2164        );
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,  // usage
2178            50.0,  // vram
2179            75.0,  // temp
2180            300.0, // power
2181        );
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        // Not stale at same time
2193        assert!(!cache.is_stale(1000, 100));
2194        // Not stale within window
2195        assert!(!cache.is_stale(1050, 100));
2196        // Stale after window
2197        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        // Should not panic, just leave defaults
2223        assert_eq!(cache.cpu.avg_usage, 0.0);
2224    }
2225}