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            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        #[cfg(not(target_arch = "aarch64"))]
94        {
95            Self::Scalar
96        }
97    }
98
99    /// Get the instruction set name as a static string
100    #[must_use]
101    pub const fn name(self) -> &'static str {
102        match self {
103            Self::Scalar => "Scalar",
104            Self::Sse4 => "SSE4.1",
105            Self::Avx2 => "AVX2",
106            Self::Avx512 => "AVX-512",
107            Self::Neon => "NEON",
108            Self::WasmSimd128 => "WASM SIMD128",
109        }
110    }
111}
112
113impl Default for SimdInstructionSet {
114    fn default() -> Self {
115        Self::detect()
116    }
117}
118
119/// `ComputeBlock` trait for SIMD-optimized panel elements
120///
121/// All panel elements that benefit from SIMD optimization implement
122/// this trait. The trait provides a common interface for:
123/// - Computing output from input data
124/// - Querying SIMD support
125/// - Measuring compute latency
126///
127/// ## Example
128///
129/// ```ignore
130/// struct SparklineBlock {
131///     history: Vec<f32>,
132/// }
133///
134/// impl ComputeBlock for SparklineBlock {
135///     type Input = f32;
136///     type Output = Vec<char>;
137///
138///     fn compute(&mut self, input: &Self::Input) -> Self::Output {
139///         self.history.push(*input);
140///         // SIMD-optimized normalization and character mapping
141///         self.render_blocks()
142///     }
143/// }
144/// ```
145pub trait ComputeBlock {
146    /// Input type for this compute block
147    type Input;
148    /// Output type produced by this compute block
149    type Output;
150
151    /// Process input data and produce output
152    ///
153    /// Implementations should use SIMD where available for optimal
154    /// performance. The `simd_instruction_set()` method indicates
155    /// which instruction set is being used.
156    fn compute(&mut self, input: &Self::Input) -> Self::Output;
157
158    /// Query if this block supports SIMD on the current CPU
159    fn simd_supported(&self) -> bool {
160        self.simd_instruction_set() != SimdInstructionSet::Scalar
161    }
162
163    /// Get the SIMD instruction set used by this block
164    fn simd_instruction_set(&self) -> SimdInstructionSet {
165        SimdInstructionSet::detect()
166    }
167
168    /// Get the compute latency budget in microseconds
169    fn latency_budget_us(&self) -> u64 {
170        1000 // Default 1ms budget
171    }
172}
173
174/// `ComputeBlock` ID as specified in SPEC-024 Section 21
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
176pub enum ComputeBlockId {
177    // CPU Panel (CB-CPU-*)
178    CpuSparklines,     // CB-CPU-001
179    CpuLoadGauge,      // CB-CPU-002
180    CpuLoadTrend,      // CB-CPU-003
181    CpuFrequency,      // CB-CPU-004
182    CpuBoostIndicator, // CB-CPU-005
183    CpuTemperature,    // CB-CPU-006
184    CpuTopConsumers,   // CB-CPU-007
185
186    // Memory Panel (CB-MEM-*)
187    MemSparklines,     // CB-MEM-001
188    MemZramRatio,      // CB-MEM-002
189    MemPressureGauge,  // CB-MEM-003
190    MemSwapThrashing,  // CB-MEM-004
191    MemCacheBreakdown, // CB-MEM-005
192    MemHugePages,      // CB-MEM-006
193
194    // Connections Panel (CB-CONN-*)
195    ConnAge,          // CB-CONN-001
196    ConnProc,         // CB-CONN-002
197    ConnGeo,          // CB-CONN-003
198    ConnLatency,      // CB-CONN-004
199    ConnService,      // CB-CONN-005
200    ConnHotIndicator, // CB-CONN-006
201    ConnSparkline,    // CB-CONN-007
202
203    // Network Panel (CB-NET-*)
204    NetSparklines,    // CB-NET-001
205    NetProtocolStats, // CB-NET-002
206    NetErrorRate,     // CB-NET-003
207    NetDropRate,      // CB-NET-004
208    NetLatencyGauge,  // CB-NET-005
209    NetBandwidthUtil, // CB-NET-006
210
211    // Process Panel (CB-PROC-*)
212    ProcTreeView,      // CB-PROC-001
213    ProcSortIndicator, // CB-PROC-002
214    ProcFilter,        // CB-PROC-003
215    ProcOomScore,      // CB-PROC-004
216    ProcNiceValue,     // CB-PROC-005
217    ProcThreadCount,   // CB-PROC-006
218    ProcCgroup,        // CB-PROC-007
219}
220
221impl ComputeBlockId {
222    /// Get the string ID (e.g., "CB-CPU-001")
223    #[must_use]
224    pub const fn id_string(&self) -> &'static str {
225        match self {
226            Self::CpuSparklines => "CB-CPU-001",
227            Self::CpuLoadGauge => "CB-CPU-002",
228            Self::CpuLoadTrend => "CB-CPU-003",
229            Self::CpuFrequency => "CB-CPU-004",
230            Self::CpuBoostIndicator => "CB-CPU-005",
231            Self::CpuTemperature => "CB-CPU-006",
232            Self::CpuTopConsumers => "CB-CPU-007",
233            Self::MemSparklines => "CB-MEM-001",
234            Self::MemZramRatio => "CB-MEM-002",
235            Self::MemPressureGauge => "CB-MEM-003",
236            Self::MemSwapThrashing => "CB-MEM-004",
237            Self::MemCacheBreakdown => "CB-MEM-005",
238            Self::MemHugePages => "CB-MEM-006",
239            Self::ConnAge => "CB-CONN-001",
240            Self::ConnProc => "CB-CONN-002",
241            Self::ConnGeo => "CB-CONN-003",
242            Self::ConnLatency => "CB-CONN-004",
243            Self::ConnService => "CB-CONN-005",
244            Self::ConnHotIndicator => "CB-CONN-006",
245            Self::ConnSparkline => "CB-CONN-007",
246            Self::NetSparklines => "CB-NET-001",
247            Self::NetProtocolStats => "CB-NET-002",
248            Self::NetErrorRate => "CB-NET-003",
249            Self::NetDropRate => "CB-NET-004",
250            Self::NetLatencyGauge => "CB-NET-005",
251            Self::NetBandwidthUtil => "CB-NET-006",
252            Self::ProcTreeView => "CB-PROC-001",
253            Self::ProcSortIndicator => "CB-PROC-002",
254            Self::ProcFilter => "CB-PROC-003",
255            Self::ProcOomScore => "CB-PROC-004",
256            Self::ProcNiceValue => "CB-PROC-005",
257            Self::ProcThreadCount => "CB-PROC-006",
258            Self::ProcCgroup => "CB-PROC-007",
259        }
260    }
261
262    /// Check if this block is SIMD-vectorizable
263    #[must_use]
264    pub const fn simd_vectorizable(&self) -> bool {
265        match self {
266            // YES - can use SIMD
267            Self::CpuSparklines
268            | Self::CpuLoadTrend
269            | Self::CpuFrequency
270            | Self::CpuTemperature
271            | Self::CpuTopConsumers
272            | Self::MemSparklines
273            | Self::MemPressureGauge
274            | Self::MemSwapThrashing
275            | Self::ConnAge
276            | Self::ConnGeo
277            | Self::ConnLatency
278            | Self::ConnService
279            | Self::ConnHotIndicator
280            | Self::ConnSparkline
281            | Self::NetSparklines
282            | Self::NetProtocolStats
283            | Self::NetErrorRate
284            | Self::NetDropRate
285            | Self::NetBandwidthUtil
286            | Self::ProcOomScore
287            | Self::ProcNiceValue
288            | Self::ProcThreadCount => true,
289
290            // NO - scalar only
291            Self::CpuLoadGauge
292            | Self::CpuBoostIndicator
293            | Self::MemZramRatio
294            | Self::MemCacheBreakdown
295            | Self::MemHugePages
296            | Self::ConnProc
297            | Self::NetLatencyGauge
298            | Self::ProcTreeView
299            | Self::ProcSortIndicator
300            | Self::ProcFilter
301            | Self::ProcCgroup => false,
302        }
303    }
304}
305
306/// Sparkline `ComputeBlock` (CB-CPU-001, CB-MEM-001, CB-NET-001, CB-CONN-007)
307///
308/// SIMD-optimized sparkline rendering using 8-level block characters.
309/// Uses AVX2 for min/max/normalization when available.
310#[derive(Debug, Clone)]
311#[allow(dead_code)]
312pub struct SparklineBlock {
313    /// History buffer (60 samples = 60 seconds at 1Hz)
314    history: Vec<f32>,
315    /// Maximum history length
316    max_samples: usize,
317    /// SIMD buffer for aligned operations
318    simd_buffer: [f32; 8],
319    /// Detected instruction set
320    instruction_set: SimdInstructionSet,
321}
322
323impl Default for SparklineBlock {
324    fn default() -> Self {
325        Self::new(60)
326    }
327}
328
329impl SparklineBlock {
330    /// Create a new sparkline block with given history length
331    #[must_use]
332    pub fn new(max_samples: usize) -> Self {
333        debug_assert!(max_samples > 0, "max_samples must be positive");
334        Self {
335            history: Vec::with_capacity(max_samples),
336            max_samples,
337            simd_buffer: [0.0; 8],
338            instruction_set: SimdInstructionSet::detect(),
339        }
340    }
341
342    /// Add a sample to the history
343    pub fn push(&mut self, value: f32) {
344        if self.history.len() >= self.max_samples {
345            self.history.remove(0);
346        }
347        self.history.push(value);
348    }
349
350    /// Get the current history
351    #[must_use]
352    pub fn history(&self) -> &[f32] {
353        &self.history
354    }
355
356    /// Render the sparkline as block characters
357    #[must_use]
358    pub fn render(&self, width: usize) -> Vec<char> {
359        if self.history.is_empty() {
360            return vec![' '; width];
361        }
362        contract_pre_render!();
363
364        // SIMD-optimized min/max finding
365        let (min, max) = self.find_min_max();
366        let range = max - min;
367
368        // Sample history to fit width
369        let samples = self.sample_to_width(width);
370
371        // Map to block characters
372        #[allow(clippy::items_after_statements)]
373        const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
374
375        samples
376            .iter()
377            .map(|&v| {
378                if range < f32::EPSILON {
379                    BLOCKS[4] // Mid-level if no variation
380                } else {
381                    let normalized = ((v - min) / range).clamp(0.0, 1.0);
382                    let idx = (normalized * 7.0) as usize;
383                    BLOCKS[idx.min(7)]
384                }
385            })
386            .collect()
387    }
388
389    /// Find min/max using SIMD when available
390    fn find_min_max(&self) -> (f32, f32) {
391        if self.history.is_empty() {
392            return (0.0, 1.0);
393        }
394
395        // Scalar implementation - SIMD intrinsics for AVX2 can be added
396        // in a future optimization pass if profiling shows this as a hotspot
397        let min = self.history.iter().copied().fold(f32::INFINITY, f32::min);
398        let max = self
399            .history
400            .iter()
401            .copied()
402            .fold(f32::NEG_INFINITY, f32::max);
403
404        (min, max)
405    }
406
407    /// Sample history to fit target width
408    fn sample_to_width(&self, width: usize) -> Vec<f32> {
409        if self.history.len() <= width {
410            // Pad with zeros if history is shorter
411            let mut result = vec![0.0; width - self.history.len()];
412            result.extend_from_slice(&self.history);
413            result
414        } else {
415            // Downsample using linear interpolation
416            let step = self.history.len() as f32 / width as f32;
417            (0..width)
418                .map(|i| {
419                    let idx = (i as f32 * step) as usize;
420                    self.history[idx.min(self.history.len() - 1)]
421                })
422                .collect()
423        }
424    }
425}
426
427impl ComputeBlock for SparklineBlock {
428    type Input = f32;
429    type Output = Vec<char>;
430
431    fn compute(&mut self, input: &Self::Input) -> Self::Output {
432        self.push(*input);
433        self.render(self.max_samples.min(60))
434    }
435
436    fn simd_instruction_set(&self) -> SimdInstructionSet {
437        self.instruction_set
438    }
439
440    fn latency_budget_us(&self) -> u64 {
441        100 // 100μs budget for sparkline rendering
442    }
443}
444
445/// Load Trend `ComputeBlock` (CB-CPU-003)
446///
447/// Computes the derivative of load average to show trend direction.
448#[derive(Debug, Clone)]
449pub struct LoadTrendBlock {
450    /// Previous load values for derivative calculation
451    history: Vec<f32>,
452    /// Smoothing window size
453    window_size: usize,
454}
455
456impl Default for LoadTrendBlock {
457    fn default() -> Self {
458        Self::new(5)
459    }
460}
461
462impl LoadTrendBlock {
463    /// Create a new load trend block
464    #[must_use]
465    pub fn new(window_size: usize) -> Self {
466        debug_assert!(window_size > 0, "window_size must be positive");
467        Self {
468            history: Vec::with_capacity(window_size),
469            window_size,
470        }
471    }
472
473    /// Get the trend direction
474    #[must_use]
475    pub fn trend(&self) -> TrendDirection {
476        if self.history.len() < 2 {
477            return TrendDirection::Flat;
478        }
479
480        let recent = self.history.iter().rev().take(self.window_size);
481        let diffs: Vec<f32> = recent
482            .clone()
483            .zip(recent.skip(1))
484            .map(|(a, b)| a - b)
485            .collect();
486
487        if diffs.is_empty() {
488            return TrendDirection::Flat;
489        }
490
491        let avg_diff: f32 = diffs.iter().sum::<f32>() / diffs.len() as f32;
492
493        #[allow(clippy::items_after_statements)]
494        const THRESHOLD: f32 = 0.05;
495        if avg_diff > THRESHOLD {
496            TrendDirection::Up
497        } else if avg_diff < -THRESHOLD {
498            TrendDirection::Down
499        } else {
500            TrendDirection::Flat
501        }
502    }
503}
504
505impl ComputeBlock for LoadTrendBlock {
506    type Input = f32;
507    type Output = TrendDirection;
508
509    fn compute(&mut self, input: &Self::Input) -> Self::Output {
510        if self.history.len() >= self.window_size * 2 {
511            self.history.remove(0);
512        }
513        self.history.push(*input);
514        self.trend()
515    }
516
517    fn latency_budget_us(&self) -> u64 {
518        10 // Very fast operation
519    }
520}
521
522/// Trend direction for load/usage indicators
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
524pub enum TrendDirection {
525    /// Trending up (↑)
526    Up,
527    /// Trending down (↓)
528    Down,
529    /// Stable/flat (→)
530    #[default]
531    Flat,
532}
533
534impl TrendDirection {
535    /// Get the arrow character for this trend
536    #[must_use]
537    pub const fn arrow(self) -> char {
538        match self {
539            Self::Up => '↑',
540            Self::Down => '↓',
541            Self::Flat => '→',
542        }
543    }
544}
545
546// =============================================================================
547// Additional ComputeBlocks (SPEC-024 Part VI: Grammar of Graphics)
548// =============================================================================
549
550/// CPU Frequency `ComputeBlock` (CB-CPU-004)
551///
552/// Tracks per-core CPU frequencies and detects frequency scaling state.
553/// Per-core frequency data from `/sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq`.
554#[derive(Debug, Clone)]
555pub struct CpuFrequencyBlock {
556    /// Per-core frequencies in MHz
557    frequencies: Vec<u32>,
558    /// Per-core max frequencies in MHz (for percentage calculation)
559    max_frequencies: Vec<u32>,
560    /// Detected instruction set
561    instruction_set: SimdInstructionSet,
562}
563
564impl Default for CpuFrequencyBlock {
565    fn default() -> Self {
566        Self::new()
567    }
568}
569
570impl CpuFrequencyBlock {
571    /// Create a new CPU frequency block
572    #[must_use]
573    pub fn new() -> Self {
574        Self {
575            frequencies: Vec::new(),
576            max_frequencies: Vec::new(),
577            instruction_set: SimdInstructionSet::detect(),
578        }
579    }
580
581    /// Set per-core frequencies
582    pub fn set_frequencies(&mut self, freqs: Vec<u32>, max_freqs: Vec<u32>) {
583        self.frequencies = freqs;
584        self.max_frequencies = max_freqs;
585    }
586
587    /// Get frequencies as percentages of max
588    #[must_use]
589    pub fn frequency_percentages(&self) -> Vec<f32> {
590        self.frequencies
591            .iter()
592            .zip(self.max_frequencies.iter())
593            .map(|(&cur, &max)| {
594                if max > 0 {
595                    (cur as f32 / max as f32 * 100.0).clamp(0.0, 100.0)
596                } else {
597                    0.0
598                }
599            })
600            .collect()
601    }
602
603    /// Get scaling state indicator for each core
604    #[must_use]
605    pub fn scaling_indicators(&self) -> Vec<FrequencyScalingState> {
606        self.frequency_percentages()
607            .iter()
608            .map(|&pct| {
609                if pct >= 95.0 {
610                    FrequencyScalingState::Turbo
611                } else if pct >= 75.0 {
612                    FrequencyScalingState::High
613                } else if pct >= 50.0 {
614                    FrequencyScalingState::Normal
615                } else if pct >= 25.0 {
616                    FrequencyScalingState::Scaled
617                } else {
618                    FrequencyScalingState::Idle
619                }
620            })
621            .collect()
622    }
623}
624
625impl ComputeBlock for CpuFrequencyBlock {
626    type Input = (Vec<u32>, Vec<u32>); // (cur_freqs, max_freqs)
627    type Output = Vec<FrequencyScalingState>;
628
629    fn compute(&mut self, input: &Self::Input) -> Self::Output {
630        self.set_frequencies(input.0.clone(), input.1.clone());
631        self.scaling_indicators()
632    }
633
634    fn simd_instruction_set(&self) -> SimdInstructionSet {
635        self.instruction_set
636    }
637
638    fn latency_budget_us(&self) -> u64 {
639        50 // 50μs budget for frequency processing
640    }
641}
642
643/// Frequency scaling state indicators
644#[derive(Debug, Clone, Copy, PartialEq, Eq)]
645pub enum FrequencyScalingState {
646    /// Turbo/boost mode active (⚡)
647    Turbo,
648    /// High frequency (↑)
649    High,
650    /// Normal frequency (→)
651    Normal,
652    /// Scaled down (↓)
653    Scaled,
654    /// Idle/very low (·)
655    Idle,
656}
657
658impl FrequencyScalingState {
659    /// Get the indicator character
660    #[must_use]
661    pub const fn indicator(self) -> char {
662        match self {
663            Self::Turbo => '⚡',
664            Self::High => '↑',
665            Self::Normal => '→',
666            Self::Scaled => '↓',
667            Self::Idle => '·',
668        }
669    }
670}
671
672/// CPU Governor `ComputeBlock` (CB-CPU-008)
673///
674/// Tracks CPU governor state from `/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor`.
675#[derive(Debug, Clone)]
676pub struct CpuGovernorBlock {
677    /// Current governor name
678    governor: CpuGovernor,
679}
680
681impl Default for CpuGovernorBlock {
682    fn default() -> Self {
683        Self::new()
684    }
685}
686
687impl CpuGovernorBlock {
688    /// Create a new CPU governor block
689    #[must_use]
690    pub fn new() -> Self {
691        Self {
692            governor: CpuGovernor::Unknown,
693        }
694    }
695
696    /// Set governor from string
697    pub fn set_governor(&mut self, name: &str) {
698        self.governor = CpuGovernor::from_name(name);
699    }
700
701    /// Get current governor
702    #[must_use]
703    pub fn governor(&self) -> CpuGovernor {
704        self.governor
705    }
706}
707
708impl ComputeBlock for CpuGovernorBlock {
709    type Input = String;
710    type Output = CpuGovernor;
711
712    fn compute(&mut self, input: &Self::Input) -> Self::Output {
713        self.set_governor(input);
714        self.governor
715    }
716
717    fn latency_budget_us(&self) -> u64 {
718        10 // Very fast string parsing
719    }
720}
721
722/// CPU Governor types
723#[derive(Debug, Clone, Copy, PartialEq, Eq)]
724pub enum CpuGovernor {
725    /// Performance - max frequency
726    Performance,
727    /// Powersave - min frequency
728    Powersave,
729    /// Ondemand - dynamic scaling
730    Ondemand,
731    /// Conservative - gradual scaling
732    Conservative,
733    /// Schedutil - scheduler-based
734    Schedutil,
735    /// Userspace - user-controlled
736    Userspace,
737    /// Unknown governor
738    Unknown,
739}
740
741impl CpuGovernor {
742    /// Parse governor from name
743    #[must_use]
744    pub fn from_name(name: &str) -> Self {
745        match name.trim().to_lowercase().as_str() {
746            "performance" => Self::Performance,
747            "powersave" => Self::Powersave,
748            "ondemand" => Self::Ondemand,
749            "conservative" => Self::Conservative,
750            "schedutil" => Self::Schedutil,
751            "userspace" => Self::Userspace,
752            _ => Self::Unknown,
753        }
754    }
755
756    /// Get governor name as string
757    #[must_use]
758    pub const fn as_str(&self) -> &'static str {
759        match self {
760            Self::Performance => "performance",
761            Self::Powersave => "powersave",
762            Self::Ondemand => "ondemand",
763            Self::Conservative => "conservative",
764            Self::Schedutil => "schedutil",
765            Self::Userspace => "userspace",
766            Self::Unknown => "unknown",
767        }
768    }
769
770    /// Get short display name
771    #[must_use]
772    pub const fn short_name(self) -> &'static str {
773        match self {
774            Self::Performance => "perf",
775            Self::Powersave => "psav",
776            Self::Ondemand => "odmd",
777            Self::Conservative => "cons",
778            Self::Schedutil => "schu",
779            Self::Userspace => "user",
780            Self::Unknown => "????",
781        }
782    }
783
784    /// Get icon for governor
785    #[must_use]
786    pub const fn icon(self) -> char {
787        match self {
788            Self::Performance => '🚀',
789            Self::Powersave => '🔋',
790            Self::Ondemand => '⚡',
791            Self::Conservative => '📊',
792            Self::Schedutil => '📅',
793            Self::Userspace => '👤',
794            Self::Unknown => '?',
795        }
796    }
797}
798
799/// Memory Pressure `ComputeBlock` (CB-MEM-003)
800///
801/// Tracks memory pressure from `/proc/pressure/memory`.
802#[derive(Debug, Clone)]
803pub struct MemPressureBlock {
804    /// Average pressure over 10 seconds (some)
805    avg10_some: f32,
806    /// Average pressure over 60 seconds (some)
807    avg60_some: f32,
808    /// Average pressure over 300 seconds (some)
809    avg300_some: f32,
810    /// Average pressure over 10 seconds (full)
811    avg10_full: f32,
812    /// Instruction set
813    instruction_set: SimdInstructionSet,
814}
815
816impl Default for MemPressureBlock {
817    fn default() -> Self {
818        Self::new()
819    }
820}
821
822impl MemPressureBlock {
823    /// Create a new memory pressure block
824    #[must_use]
825    pub fn new() -> Self {
826        Self {
827            avg10_some: 0.0,
828            avg60_some: 0.0,
829            avg300_some: 0.0,
830            avg10_full: 0.0,
831            instruction_set: SimdInstructionSet::detect(),
832        }
833    }
834
835    /// Set pressure values
836    pub fn set_pressure(
837        &mut self,
838        avg10_some: f32,
839        avg60_some: f32,
840        avg300_some: f32,
841        avg10_full: f32,
842    ) {
843        debug_assert!(avg10_some >= 0.0, "avg10_some must be non-negative");
844        debug_assert!(avg60_some >= 0.0, "avg60_some must be non-negative");
845        debug_assert!(avg300_some >= 0.0, "avg300_some must be non-negative");
846        debug_assert!(avg10_full >= 0.0, "avg10_full must be non-negative");
847        self.avg10_some = avg10_some;
848        self.avg60_some = avg60_some;
849        self.avg300_some = avg300_some;
850        self.avg10_full = avg10_full;
851    }
852
853    /// Get pressure level indicator
854    #[must_use]
855    pub fn pressure_level(&self) -> MemoryPressureLevel {
856        let pct = self.avg10_some;
857        if pct >= 50.0 {
858            MemoryPressureLevel::Critical
859        } else if pct >= 25.0 {
860            MemoryPressureLevel::High
861        } else if pct >= 10.0 {
862            MemoryPressureLevel::Medium
863        } else if pct >= 1.0 {
864            MemoryPressureLevel::Low
865        } else {
866            MemoryPressureLevel::None
867        }
868    }
869
870    /// Get trend from 300s to 10s averages
871    #[must_use]
872    pub fn trend(&self) -> TrendDirection {
873        let diff = self.avg10_some - self.avg300_some;
874        if diff > 5.0 {
875            TrendDirection::Up
876        } else if diff < -5.0 {
877            TrendDirection::Down
878        } else {
879            TrendDirection::Flat
880        }
881    }
882}
883
884impl ComputeBlock for MemPressureBlock {
885    type Input = (f32, f32, f32, f32); // (avg10_some, avg60_some, avg300_some, avg10_full)
886    type Output = MemoryPressureLevel;
887
888    fn compute(&mut self, input: &Self::Input) -> Self::Output {
889        self.set_pressure(input.0, input.1, input.2, input.3);
890        self.pressure_level()
891    }
892
893    fn simd_instruction_set(&self) -> SimdInstructionSet {
894        self.instruction_set
895    }
896
897    fn latency_budget_us(&self) -> u64 {
898        20 // Simple comparisons
899    }
900}
901
902/// Memory pressure level
903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
904pub enum MemoryPressureLevel {
905    /// No pressure
906    None,
907    /// Low pressure (1-10%)
908    Low,
909    /// Medium pressure (10-25%)
910    Medium,
911    /// High pressure (25-50%)
912    High,
913    /// Critical pressure (>50%)
914    Critical,
915}
916
917impl MemoryPressureLevel {
918    /// Get the display character for this pressure level
919    #[allow(clippy::match_same_arms)]
920    pub fn symbol(&self) -> char {
921        match self {
922            Self::None => ' ',
923            Self::Low => '○',
924            Self::Medium => '◐',
925            Self::High => '◕',
926            Self::Critical => '●',
927        }
928    }
929
930    /// Get color index (0=green, 4=red)
931    #[must_use]
932    pub const fn severity(self) -> u8 {
933        match self {
934            Self::None => 0,
935            Self::Low => 1,
936            Self::Medium => 2,
937            Self::High => 3,
938            Self::Critical => 4,
939        }
940    }
941}
942
943/// Huge Pages `ComputeBlock` (CB-MEM-006)
944///
945/// Tracks huge page usage from `/proc/meminfo`.
946#[derive(Debug, Clone)]
947pub struct HugePagesBlock {
948    /// Total huge pages
949    total: u64,
950    /// Free huge pages
951    free: u64,
952    /// Reserved huge pages
953    reserved: u64,
954    /// Huge page size in KB
955    page_size_kb: u64,
956}
957
958impl Default for HugePagesBlock {
959    fn default() -> Self {
960        Self::new()
961    }
962}
963
964impl HugePagesBlock {
965    /// Create a new huge pages block
966    #[must_use]
967    pub fn new() -> Self {
968        Self {
969            total: 0,
970            free: 0,
971            reserved: 0,
972            page_size_kb: 2048, // Default 2MB huge pages
973        }
974    }
975
976    /// Set huge page values
977    pub fn set_values(&mut self, total: u64, free: u64, reserved: u64, page_size_kb: u64) {
978        debug_assert!(free <= total, "free must be <= total");
979        debug_assert!(page_size_kb > 0, "page_size_kb must be positive");
980        self.total = total;
981        self.free = free;
982        self.reserved = reserved;
983        self.page_size_kb = page_size_kb;
984    }
985
986    /// Get usage percentage
987    #[must_use]
988    pub fn usage_percent(&self) -> f32 {
989        if self.total == 0 {
990            0.0
991        } else {
992            ((self.total - self.free) as f32 / self.total as f32 * 100.0).clamp(0.0, 100.0)
993        }
994    }
995
996    /// Get total size in bytes
997    #[must_use]
998    pub fn total_bytes(&self) -> u64 {
999        self.total * self.page_size_kb * 1024
1000    }
1001
1002    /// Get used size in bytes
1003    #[must_use]
1004    pub fn used_bytes(&self) -> u64 {
1005        (self.total - self.free) * self.page_size_kb * 1024
1006    }
1007}
1008
1009impl ComputeBlock for HugePagesBlock {
1010    type Input = (u64, u64, u64, u64); // (total, free, reserved, page_size_kb)
1011    type Output = f32; // Usage percentage
1012
1013    fn compute(&mut self, input: &Self::Input) -> Self::Output {
1014        self.set_values(input.0, input.1, input.2, input.3);
1015        self.usage_percent()
1016    }
1017
1018    fn latency_budget_us(&self) -> u64 {
1019        10 // Simple arithmetic
1020    }
1021}
1022
1023/// GPU Thermal `ComputeBlock` (CB-GPU-001)
1024///
1025/// Tracks GPU temperature and power draw.
1026#[derive(Debug, Clone)]
1027pub struct GpuThermalBlock {
1028    /// Temperature in Celsius
1029    temperature_c: f32,
1030    /// Power draw in Watts
1031    power_w: f32,
1032    /// Power limit in Watts
1033    power_limit_w: f32,
1034    /// History for trend
1035    temp_history: Vec<f32>,
1036    /// Instruction set
1037    instruction_set: SimdInstructionSet,
1038}
1039
1040impl Default for GpuThermalBlock {
1041    fn default() -> Self {
1042        Self::new()
1043    }
1044}
1045
1046impl GpuThermalBlock {
1047    /// Create a new GPU thermal block
1048    #[must_use]
1049    pub fn new() -> Self {
1050        Self {
1051            temperature_c: 0.0,
1052            power_w: 0.0,
1053            power_limit_w: 0.0,
1054            temp_history: Vec::with_capacity(60),
1055            instruction_set: SimdInstructionSet::detect(),
1056        }
1057    }
1058
1059    /// Set thermal values
1060    pub fn set_values(&mut self, temp_c: f32, power_w: f32, power_limit_w: f32) {
1061        debug_assert!(power_w >= 0.0, "power_w must be non-negative");
1062        debug_assert!(power_limit_w >= 0.0, "power_limit_w must be non-negative");
1063        self.temperature_c = temp_c;
1064        self.power_w = power_w;
1065        self.power_limit_w = power_limit_w;
1066
1067        // Update history
1068        if self.temp_history.len() >= 60 {
1069            self.temp_history.remove(0);
1070        }
1071        self.temp_history.push(temp_c);
1072    }
1073
1074    /// Get thermal state
1075    #[must_use]
1076    pub fn thermal_state(&self) -> GpuThermalState {
1077        if self.temperature_c >= 90.0 {
1078            GpuThermalState::Critical
1079        } else if self.temperature_c >= 80.0 {
1080            GpuThermalState::Hot
1081        } else if self.temperature_c >= 70.0 {
1082            GpuThermalState::Warm
1083        } else if self.temperature_c >= 50.0 {
1084            GpuThermalState::Normal
1085        } else {
1086            GpuThermalState::Cool
1087        }
1088    }
1089
1090    /// Get power usage percentage
1091    #[must_use]
1092    pub fn power_percent(&self) -> f32 {
1093        if self.power_limit_w > 0.0 {
1094            (self.power_w / self.power_limit_w * 100.0).clamp(0.0, 100.0)
1095        } else {
1096            0.0
1097        }
1098    }
1099
1100    /// Get temperature trend
1101    #[must_use]
1102    pub fn trend(&self) -> TrendDirection {
1103        if self.temp_history.len() < 5 {
1104            return TrendDirection::Flat;
1105        }
1106
1107        let recent: f32 = self.temp_history.iter().rev().take(5).sum::<f32>() / 5.0;
1108        let older: f32 = self.temp_history.iter().rev().skip(5).take(5).sum::<f32>() / 5.0;
1109
1110        let diff = recent - older;
1111        if diff > 2.0 {
1112            TrendDirection::Up
1113        } else if diff < -2.0 {
1114            TrendDirection::Down
1115        } else {
1116            TrendDirection::Flat
1117        }
1118    }
1119}
1120
1121impl ComputeBlock for GpuThermalBlock {
1122    type Input = (f32, f32, f32); // (temp_c, power_w, power_limit_w)
1123    type Output = GpuThermalState;
1124
1125    fn compute(&mut self, input: &Self::Input) -> Self::Output {
1126        self.set_values(input.0, input.1, input.2);
1127        self.thermal_state()
1128    }
1129
1130    fn simd_instruction_set(&self) -> SimdInstructionSet {
1131        self.instruction_set
1132    }
1133
1134    fn latency_budget_us(&self) -> u64 {
1135        30 // Simple comparisons + history update
1136    }
1137}
1138
1139/// GPU thermal state
1140#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1141pub enum GpuThermalState {
1142    /// Cool (<50°C)
1143    #[default]
1144    Cool,
1145    /// Normal (50-70°C)
1146    Normal,
1147    /// Warm (70-80°C)
1148    Warm,
1149    /// Hot (80-90°C)
1150    Hot,
1151    /// Critical (>90°C)
1152    Critical,
1153}
1154
1155impl GpuThermalState {
1156    /// Get indicator character
1157    #[must_use]
1158    pub const fn indicator(self) -> char {
1159        match self {
1160            Self::Cool => '❄',
1161            Self::Normal => '●',
1162            Self::Warm => '◐',
1163            Self::Hot => '◕',
1164            Self::Critical => '🔥',
1165        }
1166    }
1167
1168    /// Get severity (0=cool, 4=critical)
1169    #[must_use]
1170    pub const fn severity(self) -> u8 {
1171        match self {
1172            Self::Cool => 0,
1173            Self::Normal => 1,
1174            Self::Warm => 2,
1175            Self::Hot => 3,
1176            Self::Critical => 4,
1177        }
1178    }
1179}
1180
1181/// GPU VRAM `ComputeBlock` (CB-GPU-002)
1182///
1183/// Tracks VRAM usage per process.
1184#[derive(Debug, Clone)]
1185pub struct GpuVramBlock {
1186    /// Total VRAM in MB
1187    total_mb: u64,
1188    /// Used VRAM in MB
1189    used_mb: u64,
1190    /// Per-process VRAM usage (PID -> MB)
1191    per_process: Vec<(u32, u64, String)>, // (pid, mb, name)
1192}
1193
1194impl Default for GpuVramBlock {
1195    fn default() -> Self {
1196        Self::new()
1197    }
1198}
1199
1200impl GpuVramBlock {
1201    /// Create a new VRAM block
1202    #[must_use]
1203    pub fn new() -> Self {
1204        Self {
1205            total_mb: 0,
1206            used_mb: 0,
1207            per_process: Vec::new(),
1208        }
1209    }
1210
1211    /// Set VRAM values
1212    pub fn set_values(
1213        &mut self,
1214        total_mb: u64,
1215        used_mb: u64,
1216        per_process: Vec<(u32, u64, String)>,
1217    ) {
1218        self.total_mb = total_mb;
1219        self.used_mb = used_mb;
1220        self.per_process = per_process;
1221    }
1222
1223    /// Get usage percentage
1224    #[must_use]
1225    pub fn usage_percent(&self) -> f32 {
1226        if self.total_mb == 0 {
1227            0.0
1228        } else {
1229            (self.used_mb as f32 / self.total_mb as f32 * 100.0).clamp(0.0, 100.0)
1230        }
1231    }
1232
1233    /// Get top N consumers by VRAM
1234    #[must_use]
1235    pub fn top_consumers(&self, n: usize) -> Vec<&(u32, u64, String)> {
1236        let mut sorted: Vec<_> = self.per_process.iter().collect();
1237        sorted.sort_by_key(|b| std::cmp::Reverse(b.1));
1238        sorted.into_iter().take(n).collect()
1239    }
1240}
1241
1242impl ComputeBlock for GpuVramBlock {
1243    type Input = (u64, u64, Vec<(u32, u64, String)>);
1244    type Output = f32; // Usage percentage
1245
1246    fn compute(&mut self, input: &Self::Input) -> Self::Output {
1247        self.set_values(input.0, input.1, input.2.clone());
1248        self.usage_percent()
1249    }
1250
1251    fn latency_budget_us(&self) -> u64 {
1252        100 // Sorting may be needed
1253    }
1254}
1255
1256// =============================================================================
1257// MetricsCacheBlock: O(1) Cached Metrics for ptop Performance
1258// =============================================================================
1259
1260/// Cached metrics snapshot for O(1) panel access.
1261///
1262/// This struct provides pre-computed, cached views of system metrics
1263/// to avoid redundant calculations during rendering. Per the spec:
1264/// "All metrics must be O(1) cached views, not O(n) per-frame refreshes."
1265///
1266/// # Architecture
1267///
1268/// ```text
1269/// ┌─────────────────────────────────────────────────────────────────┐
1270/// │  collect_metrics() [O(n)]  →  MetricsCache  →  render() [O(1)]  │
1271/// │                                                                  │
1272/// │  ┌─────────────┐     ┌─────────────┐     ┌─────────────────┐    │
1273/// │  │ /proc scan  │ →   │ SIMD Reduce │ →   │ Cached Summary  │    │
1274/// │  │ (2600 PIDs) │     │ (AVX2/NEON) │     │ (top 50, sums)  │    │
1275/// │  └─────────────┘     └─────────────┘     └─────────────────┘    │
1276/// └─────────────────────────────────────────────────────────────────┘
1277/// ```
1278///
1279/// # Performance Targets
1280///
1281/// | Operation | Target | Notes |
1282/// |-----------|--------|-------|
1283/// | Cache update | <100ms | Once per collect_metrics() |
1284/// | Cache read | <1μs | O(1) field access |
1285/// | Memory overhead | <1KB | Just aggregates, not full data |
1286#[derive(Debug, Clone, Default)]
1287pub struct MetricsCache {
1288    /// Cached CPU aggregate
1289    pub cpu: CpuMetricsCache,
1290    /// Cached memory aggregate
1291    pub memory: MemoryMetricsCache,
1292    /// Cached process aggregate
1293    pub process: ProcessMetricsCache,
1294    /// Cached network aggregate
1295    pub network: NetworkMetricsCache,
1296    /// Cached GPU aggregate
1297    pub gpu: GpuMetricsCache,
1298    /// Frame ID when cache was last updated
1299    pub frame_id: u64,
1300    /// Timestamp of last update (for cache invalidation)
1301    pub updated_at_us: u64,
1302}
1303
1304/// Cached CPU metrics
1305#[derive(Debug, Clone, Default)]
1306pub struct CpuMetricsCache {
1307    /// Average CPU usage across all cores (0-100)
1308    pub avg_usage: f32,
1309    /// Maximum core usage (for load display)
1310    pub max_core_usage: f32,
1311    /// Number of cores at >90% usage
1312    pub hot_cores: u32,
1313    /// Load average (1m, 5m, 15m)
1314    pub load_avg: [f32; 3],
1315    /// Current frequency (GHz)
1316    pub freq_ghz: f32,
1317    /// Trend direction
1318    pub trend: TrendDirection,
1319}
1320
1321/// Cached memory metrics
1322#[derive(Debug, Clone, Default)]
1323pub struct MemoryMetricsCache {
1324    /// Usage percentage (0-100)
1325    pub usage_percent: f32,
1326    /// Used bytes
1327    pub used_bytes: u64,
1328    /// Total bytes
1329    pub total_bytes: u64,
1330    /// Cached bytes
1331    pub cached_bytes: u64,
1332    /// Swap usage percentage
1333    pub swap_percent: f32,
1334    /// ZRAM compression ratio
1335    pub zram_ratio: f32,
1336    /// Trend direction
1337    pub trend: TrendDirection,
1338}
1339
1340/// Cached process metrics
1341#[derive(Debug, Clone, Default)]
1342pub struct ProcessMetricsCache {
1343    /// Total process count
1344    pub total_count: u32,
1345    /// Running process count
1346    pub running_count: u32,
1347    /// Sleeping process count
1348    pub sleeping_count: u32,
1349    /// Top CPU consumer (pid, cpu%, name)
1350    pub top_cpu: Option<(u32, f32, String)>,
1351    /// Top memory consumer (pid, mem%, name)
1352    pub top_mem: Option<(u32, f32, String)>,
1353    /// Sum of all CPU usage (for overhead display)
1354    pub total_cpu_usage: f32,
1355}
1356
1357/// Cached network metrics
1358#[derive(Debug, Clone, Default)]
1359pub struct NetworkMetricsCache {
1360    /// Primary interface name
1361    pub interface: String,
1362    /// RX rate (bytes/sec)
1363    pub rx_bytes_sec: u64,
1364    /// TX rate (bytes/sec)
1365    pub tx_bytes_sec: u64,
1366    /// Total RX bytes
1367    pub total_rx: u64,
1368    /// Total TX bytes
1369    pub total_tx: u64,
1370    /// Active connection count
1371    pub connection_count: u32,
1372}
1373
1374/// Cached GPU metrics
1375#[derive(Debug, Clone, Default)]
1376pub struct GpuMetricsCache {
1377    /// GPU name
1378    pub name: String,
1379    /// GPU usage percentage
1380    pub usage_percent: f32,
1381    /// VRAM usage percentage
1382    pub vram_percent: f32,
1383    /// Temperature in Celsius
1384    pub temp_c: f32,
1385    /// Power draw in Watts
1386    pub power_w: f32,
1387    /// Thermal state
1388    pub thermal_state: GpuThermalState,
1389}
1390
1391impl MetricsCache {
1392    /// Create a new empty cache
1393    #[must_use]
1394    pub fn new() -> Self {
1395        Self::default()
1396    }
1397
1398    /// Check if cache is stale (older than `max_age_us`)
1399    #[must_use]
1400    pub fn is_stale(&self, current_time_us: u64, max_age_us: u64) -> bool {
1401        current_time_us.saturating_sub(self.updated_at_us) > max_age_us
1402    }
1403
1404    /// Update CPU cache from raw data
1405    pub fn update_cpu(
1406        &mut self,
1407        per_core: &[f64],
1408        load_avg: [f32; 3],
1409        freq_ghz: f32,
1410        frame_id: u64,
1411    ) {
1412        if per_core.is_empty() {
1413            return;
1414        }
1415
1416        // SIMD-friendly reduction (compiler can vectorize)
1417        let sum: f64 = per_core.iter().sum();
1418        let max: f64 = per_core.iter().copied().fold(0.0, f64::max);
1419        let hot_cores = per_core.iter().filter(|&&c| c > 90.0).count();
1420
1421        self.cpu.avg_usage = (sum / per_core.len() as f64) as f32;
1422        self.cpu.max_core_usage = max as f32;
1423        self.cpu.hot_cores = hot_cores as u32;
1424        self.cpu.load_avg = load_avg;
1425        self.cpu.freq_ghz = freq_ghz;
1426        self.frame_id = frame_id;
1427    }
1428
1429    /// Update memory cache from raw data
1430    pub fn update_memory(
1431        &mut self,
1432        used: u64,
1433        total: u64,
1434        cached: u64,
1435        swap_used: u64,
1436        swap_total: u64,
1437        zram_ratio: f32,
1438    ) {
1439        self.memory.used_bytes = used;
1440        self.memory.total_bytes = total;
1441        self.memory.cached_bytes = cached;
1442        self.memory.usage_percent = if total > 0 {
1443            used as f32 / total as f32 * 100.0
1444        } else {
1445            0.0
1446        };
1447        self.memory.swap_percent = if swap_total > 0 {
1448            swap_used as f32 / swap_total as f32 * 100.0
1449        } else {
1450            0.0
1451        };
1452        self.memory.zram_ratio = zram_ratio;
1453    }
1454
1455    /// Update process cache from raw data
1456    pub fn update_process(
1457        &mut self,
1458        total: u32,
1459        running: u32,
1460        sleeping: u32,
1461        top_cpu: Option<(u32, f32, String)>,
1462        top_mem: Option<(u32, f32, String)>,
1463        total_cpu: f32,
1464    ) {
1465        self.process.total_count = total;
1466        self.process.running_count = running;
1467        self.process.sleeping_count = sleeping;
1468        self.process.top_cpu = top_cpu;
1469        self.process.top_mem = top_mem;
1470        self.process.total_cpu_usage = total_cpu;
1471    }
1472
1473    /// Update network cache from raw data
1474    pub fn update_network(
1475        &mut self,
1476        interface: String,
1477        rx_rate: u64,
1478        tx_rate: u64,
1479        total_rx: u64,
1480        total_tx: u64,
1481        conn_count: u32,
1482    ) {
1483        self.network.interface = interface;
1484        self.network.rx_bytes_sec = rx_rate;
1485        self.network.tx_bytes_sec = tx_rate;
1486        self.network.total_rx = total_rx;
1487        self.network.total_tx = total_tx;
1488        self.network.connection_count = conn_count;
1489    }
1490
1491    /// Update GPU cache from raw data
1492    pub fn update_gpu(&mut self, name: String, usage: f32, vram: f32, temp: f32, power: f32) {
1493        self.gpu.name = name;
1494        self.gpu.usage_percent = usage;
1495        self.gpu.vram_percent = vram;
1496        self.gpu.temp_c = temp;
1497        self.gpu.power_w = power;
1498        self.gpu.thermal_state = if temp >= 90.0 {
1499            GpuThermalState::Critical
1500        } else if temp >= 80.0 {
1501            GpuThermalState::Hot
1502        } else if temp >= 70.0 {
1503            GpuThermalState::Warm
1504        } else if temp >= 50.0 {
1505            GpuThermalState::Normal
1506        } else {
1507            GpuThermalState::Cool
1508        };
1509    }
1510
1511    /// Set timestamp for cache freshness tracking
1512    pub fn mark_updated(&mut self, timestamp_us: u64) {
1513        self.updated_at_us = timestamp_us;
1514    }
1515}
1516
1517/// `ComputeBlock` wrapper for `MetricsCache` that provides O(1) access
1518#[derive(Debug, Clone, Default)]
1519pub struct MetricsCacheBlock {
1520    cache: MetricsCache,
1521    instruction_set: SimdInstructionSet,
1522}
1523
1524impl MetricsCacheBlock {
1525    /// Create a new metrics cache block
1526    #[must_use]
1527    pub fn new() -> Self {
1528        Self {
1529            cache: MetricsCache::new(),
1530            instruction_set: SimdInstructionSet::detect(),
1531        }
1532    }
1533
1534    /// Get immutable reference to the cache
1535    #[must_use]
1536    pub fn cache(&self) -> &MetricsCache {
1537        &self.cache
1538    }
1539
1540    /// Get mutable reference to the cache for updates
1541    pub fn cache_mut(&mut self) -> &mut MetricsCache {
1542        &mut self.cache
1543    }
1544}
1545
1546impl ComputeBlock for MetricsCacheBlock {
1547    type Input = (); // No input - cache is updated separately
1548    type Output = MetricsCache;
1549
1550    fn compute(&mut self, _input: &Self::Input) -> Self::Output {
1551        self.cache.clone()
1552    }
1553
1554    fn simd_instruction_set(&self) -> SimdInstructionSet {
1555        self.instruction_set
1556    }
1557
1558    fn latency_budget_us(&self) -> u64 {
1559        1 // O(1) access - should be <1μs
1560    }
1561}
1562
1563#[cfg(test)]
1564#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
1565#[path = "compute_block_tests.rs"]
1566mod tests;