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