## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/trueno/src/brick/exec_graph/node.rs

    Line
    Count
    Source
    1
    //! Execution Graph Node Types and Profiling Primitives
    2
    //!
    3
    //! This module contains all type definitions for execution path tracking:
    4
    //!
    5
    //! - **PAR-073**: BrickSample, BrickBottleneck - foundational profiling primitives
    6
    //! - **PAR-200**: BrickId, BrickCategory, SyncMode - O(1) hot path brick identification
    7
    //! - **PAR-201**: ExecutionNode, EdgeType, etc. - execution hierarchy types
    8
    9
    use std::collections::HashMap;
    10
    use std::fmt;
    11
    12
    // ============================================================================
    13
    // BrickProfiler: FOUNDATIONAL Real-Time Per-Brick Timing (PAR-073)
    14
    // ============================================================================
    15
    16
    /// Individual brick timing sample.
    17
    /// Pure Rust timing using `std::time::Instant`.
    18
    #[derive(Debug, Clone, Copy)]
    19
    pub struct BrickSample {
    20
        /// Brick name hash (for fast lookup)
    21
        pub brick_id: u64,
    22
        /// Elapsed time in nanoseconds
    23
        pub elapsed_ns: u64,
    24
        /// Number of elements processed
    25
        pub elements: u64,
    26
    }
    27
    28
    /// Bottleneck classification for roofline analysis (PMAT-451)
    29
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
    30
    pub enum BrickBottleneck {
    31
        /// Not classified
    32
        #[default]
    33
        Unknown,
    34
        /// Limited by memory bandwidth
    35
        Memory,
    36
        /// Limited by compute throughput
    37
        Compute,
    38
    }
    39
    40
    impl fmt::Display for BrickBottleneck {
    41
    0
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    42
    0
            match self {
    43
    0
                BrickBottleneck::Unknown => write!(f, "unknown"),
    44
    0
                BrickBottleneck::Memory => write!(f, "memory"),
    45
    0
                BrickBottleneck::Compute => write!(f, "compute"),
    46
            }
    47
    0
        }
    48
    }
    49
    50
    // ============================================================================
    51
    // PAR-200: BrickProfiler v2 - O(1) Hot Path with BrickId Enum
    52
    // ============================================================================
    53
    54
    /// Well-known brick types for O(1) lookup on hot path.
    55
    ///
    56
    /// PAR-200: Eliminates string allocation and HashMap hashing during profiling.
    57
    /// Use `BrickId::Custom` with string fallback for unknown brick types.
    58
    ///
    59
    /// # Example
    60
    /// ```rust
    61
    /// use trueno::brick::BrickId;
    62
    ///
    63
    /// let brick = BrickId::RmsNorm;
    64
    /// assert_eq!(brick.category(), trueno::brick::BrickCategory::Norm);
    65
    /// assert_eq!(brick.name(), "RmsNorm");
    66
    /// ```
    67
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    68
    #[repr(u8)]
    69
    pub enum BrickId {
    70
        // Normalization (0-1)
    71
        /// RMS normalization layer
    72
        RmsNorm = 0,
    73
        /// Layer normalization
    74
        LayerNorm = 1,
    75
    76
        // Attention (2-7)
    77
        /// Q/K/V projection (combined or separate)
    78
        QkvProjection = 2,
    79
        /// Rotary position embedding
    80
        RopeEmbedding = 3,
    81
        /// Attention score computation (Q @ K^T)
    82
        AttentionScore = 4,
    83
        /// Attention softmax
    84
        AttentionSoftmax = 5,
    85
        /// Attention output (scores @ V)
    86
        AttentionOutput = 6,
    87
        /// Output projection after attention
    88
        OutputProjection = 7,
    89
    90
        // FFN (8-11)
    91
        /// Gate projection (for gated FFN)
    92
        GateProjection = 8,
    93
        /// Up projection
    94
        UpProjection = 9,
    95
        /// SiLU/GELU/ReLU activation
    96
        Activation = 10,
    97
        /// Down projection
    98
        DownProjection = 11,
    99
    100
        // Other (12-14)
    101
        /// Token embedding lookup
    102
        Embedding = 12,
    103
        /// Language model head (logits)
    104
        LmHead = 13,
    105
        /// Token sampling
    106
        Sampling = 14,
    107
    }
    108
    109
    impl BrickId {
    110
        /// Number of well-known brick types.
    111
        pub const COUNT: usize = 15;
    112
    113
        /// Get the category for hierarchical aggregation.
    114
        #[inline]
    115
    0
        pub fn category(self) -> BrickCategory {
    116
    0
            match self {
    117
    0
                Self::RmsNorm | Self::LayerNorm => BrickCategory::Norm,
    118
                Self::QkvProjection
    119
                | Self::RopeEmbedding
    120
                | Self::AttentionScore
    121
                | Self::AttentionSoftmax
    122
                | Self::AttentionOutput
    123
    0
                | Self::OutputProjection => BrickCategory::Attention,
    124
                Self::GateProjection | Self::UpProjection | Self::Activation | Self::DownProjection => {
    125
    0
                    BrickCategory::Ffn
    126
                }
    127
    0
                Self::Embedding | Self::LmHead | Self::Sampling => BrickCategory::Other,
    128
            }
    129
    0
        }
    130
    131
        /// Get the string name of this brick.
    132
        #[inline]
    133
    0
        pub const fn name(self) -> &'static str {
    134
    0
            match self {
    135
    0
                Self::RmsNorm => "RmsNorm",
    136
    0
                Self::LayerNorm => "LayerNorm",
    137
    0
                Self::QkvProjection => "QkvProjection",
    138
    0
                Self::RopeEmbedding => "RopeEmbedding",
    139
    0
                Self::AttentionScore => "AttentionScore",
    140
    0
                Self::AttentionSoftmax => "AttentionSoftmax",
    141
    0
                Self::AttentionOutput => "AttentionOutput",
    142
    0
                Self::OutputProjection => "OutputProjection",
    143
    0
                Self::GateProjection => "GateProjection",
    144
    0
                Self::UpProjection => "UpProjection",
    145
    0
                Self::Activation => "Activation",
    146
    0
                Self::DownProjection => "DownProjection",
    147
    0
                Self::Embedding => "Embedding",
    148
    0
                Self::LmHead => "LmHead",
    149
    0
                Self::Sampling => "Sampling",
    150
            }
    151
    0
        }
    152
    153
        /// Try to parse a string into a BrickId.
    154
        #[allow(clippy::should_implement_trait)]
    155
    0
        pub fn from_str(s: &str) -> Option<Self> {
    156
    0
            match s {
    157
    0
                "RmsNorm" => Some(Self::RmsNorm),
    158
    0
                "LayerNorm" => Some(Self::LayerNorm),
    159
    0
                "QkvProjection" | "Qkv" => Some(Self::QkvProjection),
    160
    0
                "RopeEmbedding" | "Rope" | "RoPE" => Some(Self::RopeEmbedding),
    161
    0
                "AttentionScore" => Some(Self::AttentionScore),
    162
    0
                "AttentionSoftmax" | "Softmax" => Some(Self::AttentionSoftmax),
    163
    0
                "AttentionOutput" => Some(Self::AttentionOutput),
    164
    0
                "OutputProjection" | "OutProj" => Some(Self::OutputProjection),
    165
    0
                "GateProjection" | "Gate" => Some(Self::GateProjection),
    166
    0
                "UpProjection" | "Up" => Some(Self::UpProjection),
    167
    0
                "Activation" | "SiLU" | "GELU" | "ReLU" => Some(Self::Activation),
    168
    0
                "DownProjection" | "Down" => Some(Self::DownProjection),
    169
    0
                "Embedding" | "Embed" => Some(Self::Embedding),
    170
    0
                "LmHead" | "Head" => Some(Self::LmHead),
    171
    0
                "Sampling" | "Sample" => Some(Self::Sampling),
    172
    0
                _ => None,
    173
            }
    174
    0
        }
    175
    }
    176
    177
    impl fmt::Display for BrickId {
    178
    0
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    179
    0
            write!(f, "{}", self.name())
    180
    0
        }
    181
    }
    182
    183
    /// Category for hierarchical aggregation of brick statistics.
    184
    ///
    185
    /// PAR-200: Groups related bricks for high-level performance analysis.
    186
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
    187
    #[repr(u8)]
    188
    pub enum BrickCategory {
    189
        /// Normalization layers (RmsNorm, LayerNorm)
    190
        Norm = 0,
    191
        /// Attention mechanism (QKV, RoPE, scores, softmax, output)
    192
        Attention = 1,
    193
        /// Feed-forward network (gate, up, activation, down)
    194
        Ffn = 2,
    195
        /// Other operations (embedding, lm_head, sampling)
    196
        #[default]
    197
        Other = 3,
    198
    }
    199
    200
    impl BrickCategory {
    201
        /// Number of categories.
    202
        pub const COUNT: usize = 4;
    203
    204
        /// Get the string name of this category.
    205
        #[inline]
    206
    0
        pub const fn name(self) -> &'static str {
    207
    0
            match self {
    208
    0
                Self::Norm => "Norm",
    209
    0
                Self::Attention => "Attention",
    210
    0
                Self::Ffn => "FFN",
    211
    0
                Self::Other => "Other",
    212
            }
    213
    0
        }
    214
    }
    215
    216
    impl fmt::Display for BrickCategory {
    217
    0
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    218
    0
            write!(f, "{}", self.name())
    219
    0
        }
    220
    }
    221
    222
    /// Synchronization mode for GPU profiling.
    223
    ///
    224
    /// PAR-200: Controls the trade-off between accuracy and overhead.
    225
    ///
    226
    /// # Performance Characteristics
    227
    ///
    228
    /// | Mode | Overhead | Accuracy | Use Case |
    229
    /// |------|----------|----------|----------|
    230
    /// | `Immediate` | ~200% | Exact per-kernel | Debugging |
    231
    /// | `PerLayer` | ~20% | Per-layer exact | Development |
    232
    /// | `Deferred` | ~5% | Approximate | Production |
    233
    /// | `None` | 0% | N/A | Disabled |
    234
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
    235
    pub enum SyncMode {
    236
        /// Sync after each kernel (accurate but slow).
    237
        /// Best for debugging and detailed optimization.
    238
        Immediate,
    239
        /// Sync once per transformer layer.
    240
        /// Good balance for development.
    241
        PerLayer,
    242
        /// Sync once per forward pass (fast, approximate).
    243
        /// Best for production profiling.
    244
        #[default]
    245
        Deferred,
    246
        /// No synchronization (profiling disabled or CPU-only).
    247
        None,
    248
    }
    249
    250
    // ============================================================================
    251
    // PAR-201: Execution Path Graph Types
    252
    // ============================================================================
    253
    254
    /// Node ID in the execution graph.
    255
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    256
    pub struct ExecutionNodeId(pub u32);
    257
    258
    /// Execution graph node types.
    259
    ///
    260
    /// PAR-201: Represents different levels of the execution hierarchy.
    261
    #[derive(Debug, Clone)]
    262
    pub enum ExecutionNode {
    263
        /// High-level brick (BrickId from v2)
    264
        Brick {
    265
            id: BrickId,
    266
            timing_ns: u64,
    267
            elements: u64,
    268
        },
    269
        /// GPU kernel launch
    270
        Kernel {
    271
            name: String,
    272
            /// FNV-1a hash of PTX source for identity
    273
            ptx_hash: u64,
    274
            /// Grid dimensions (blocks)
    275
            grid: (u32, u32, u32),
    276
            /// Block dimensions (threads)
    277
            block: (u32, u32, u32),
    278
            /// Shared memory bytes
    279
            shared_mem: u32,
    280
            /// Kernel execution time in nanoseconds (Phase 9: for CPA)
    281
            timing_ns: Option<u64>,
    282
            /// Arithmetic intensity (FLOPs/byte) for roofline analysis (Phase 9)
    283
            arithmetic_intensity: Option<f32>,
    284
            /// Achieved throughput in TFLOP/s (Phase 9)
    285
            achieved_tflops: Option<f32>,
    286
        },
    287
        /// Memory transfer operation (Phase 9: data movement topology)
    288
        Transfer {
    289
            /// Source location description
    290
            src: String,
    291
            /// Destination location description
    292
            dst: String,
    293
            /// Bytes transferred
    294
            bytes: u64,
    295
            /// Transfer direction
    296
            direction: TransferDirection,
    297
            /// Transfer time in nanoseconds
    298
            timing_ns: Option<u64>,
    299
        },
    300
        /// Rust function (from DWARF or manual annotation)
    301
        Function {
    302
            name: String,
    303
            file: Option<String>,
    304
            line: Option<u32>,
    305
        },
    306
        /// Transformer layer grouping
    307
        Layer { index: u32 },
    308
        /// Phase 11 (E.9.4): Async task metrics for poll efficiency tracking
    309
        AsyncTask {
    310
            /// Task name for identification
    311
            name: String,
    312
            /// Number of times poll() was called
    313
            poll_count: u64,
    314
            /// Number of times poll() returned Pending
    315
            yield_count: u64,
    316
            /// Total time spent in poll() (nanoseconds)
    317
            total_poll_ns: u64,
    318
        },
    319
    }
    320
    321
    impl ExecutionNode {
    322
        /// Get the display name of this node.
    323
    0
        pub fn name(&self) -> String {
    324
    0
            match self {
    325
    0
                Self::Brick { id, .. } => id.name().to_string(),
    326
    0
                Self::Kernel { name, .. } => name.clone(),
    327
    0
                Self::Function { name, .. } => name.clone(),
    328
    0
                Self::Layer { index } => format!("Layer{}", index),
    329
                Self::Transfer {
    330
    0
                    src,
    331
    0
                    dst,
    332
    0
                    direction,
    333
                    ..
    334
                } => {
    335
    0
                    let dir = match direction {
    336
    0
                        TransferDirection::H2D => "H2D",
    337
    0
                        TransferDirection::D2H => "D2H",
    338
    0
                        TransferDirection::D2D => "D2D",
    339
                    };
    340
    0
                    format!("{}:{}->{}", dir, src, dst)
    341
                }
    342
    0
                Self::AsyncTask { name, .. } => name.clone(),
    343
            }
    344
    0
        }
    345
    346
        /// Check if this is a kernel node.
    347
    0
        pub fn is_kernel(&self) -> bool {
    348
    0
            matches!(self, Self::Kernel { .. })
    349
    0
        }
    350
    351
        /// Check if this is a brick node.
    352
    0
        pub fn is_brick(&self) -> bool {
    353
    0
            matches!(self, Self::Brick { .. })
    354
    0
        }
    355
    356
        /// Check if this is a transfer node.
    357
    0
        pub fn is_transfer(&self) -> bool {
    358
    0
            matches!(self, Self::Transfer { .. })
    359
    0
        }
    360
    361
        /// Get timing if available (bricks, kernels, and transfers).
    362
    0
        pub fn timing_ns(&self) -> Option<u64> {
    363
    0
            match self {
    364
    0
                Self::Brick { timing_ns, .. } => Some(*timing_ns),
    365
    0
                Self::Kernel { timing_ns, .. } => *timing_ns,
    366
    0
                Self::Transfer { timing_ns, .. } => *timing_ns,
    367
    0
                _ => None,
    368
            }
    369
    0
        }
    370
    371
        /// Get PTX hash if available (kernels only).
    372
    0
        pub fn ptx_hash(&self) -> Option<u64> {
    373
    0
            match self {
    374
    0
                Self::Kernel { ptx_hash, .. } => Some(*ptx_hash),
    375
    0
                _ => None,
    376
            }
    377
    0
        }
    378
    379
        /// Get arithmetic intensity if available (kernels only, Phase 9).
    380
    0
        pub fn arithmetic_intensity(&self) -> Option<f32> {
    381
    0
            match self {
    382
                Self::Kernel {
    383
    0
                    arithmetic_intensity,
    384
                    ..
    385
    0
                } => *arithmetic_intensity,
    386
    0
                _ => None,
    387
            }
    388
    0
        }
    389
    390
        /// Get achieved TFLOP/s if available (kernels only, Phase 9).
    391
    0
        pub fn achieved_tflops(&self) -> Option<f32> {
    392
    0
            match self {
    393
                Self::Kernel {
    394
    0
                    achieved_tflops, ..
    395
    0
                } => *achieved_tflops,
    396
    0
                _ => None,
    397
            }
    398
    0
        }
    399
    400
        /// Get transfer bytes if available (transfers only, Phase 9).
    401
    0
        pub fn transfer_bytes(&self) -> Option<u64> {
    402
    0
            match self {
    403
    0
                Self::Transfer { bytes, .. } => Some(*bytes),
    404
    0
                _ => None,
    405
            }
    406
    0
        }
    407
    }
    408
    409
    /// Edge types in execution graph.
    410
    ///
    411
    /// PAR-201: Describes relationships between execution nodes.
    412
    /// Phase 9 (E.7.12): Added DependsOn and Transfer for advanced profiling.
    413
    #[derive(Debug, Clone, PartialEq)]
    414
    pub enum EdgeType {
    415
        /// Function calls function
    416
        Calls,
    417
        /// Brick contains sub-operations
    418
        Contains,
    419
        /// Function launches GPU kernel
    420
        Launches,
    421
        /// Temporal sequence (A happens before B)
    422
        Sequence,
    423
        /// Dependency edge for critical path analysis (CUDA events, stream sync)
    424
        /// PAR-201 Phase 9: CPA requires tracking true dependencies vs containment
    425
        DependsOn,
    426
        /// Data transfer edge with byte count (H2D/D2H/D2D)
    427
        /// PAR-201 Phase 9: For data movement topology and ping-pong detection
    428
        Transfer {
    429
            /// Bytes transferred
    430
            bytes: u64,
    431
            /// Transfer direction
    432
            direction: TransferDirection,
    433
        },
    434
    }
    435
    436
    /// Direction of memory transfer.
    437
    ///
    438
    /// PAR-201 Phase 9: Used with EdgeType::Transfer for data movement analysis.
    439
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    440
    pub enum TransferDirection {
    441
        /// Host to Device
    442
        H2D,
    443
        /// Device to Host
    444
        D2H,
    445
        /// Device to Device
    446
        D2D,
    447
    }
    448
    449
    /// An edge in the execution graph.
    450
    #[derive(Debug, Clone)]
    451
    pub struct ExecutionEdge {
    452
        /// Source node ID
    453
        pub src: ExecutionNodeId,
    454
        /// Destination node ID
    455
        pub dst: ExecutionNodeId,
    456
        /// Edge type
    457
        pub edge_type: EdgeType,
    458
        /// Optional weight (e.g., call count, timing)
    459
        pub weight: f32,
    460
    }
    461
    462
    // ============================================================================
    463
    // PTX Registry and Statistics Types
    464
    // ============================================================================
    465
    466
    /// PTX kernel registry for execution graph correlation.
    467
    ///
    468
    /// PAR-201: Maps PTX hashes to source code for debugging and analysis.
    469
    #[derive(Debug, Default)]
    470
    pub struct PtxRegistry {
    471
        /// Hash → (kernel_name, ptx_source, file_path)
    472
        kernels: HashMap<u64, (String, String, Option<std::path::PathBuf>)>,
    473
    }
    474
    475
    impl PtxRegistry {
    476
        /// Create a new empty registry.
    477
    0
        pub fn new() -> Self {
    478
    0
            Self::default()
    479
    0
        }
    480
    481
        /// Register PTX source code.
    482
        ///
    483
        /// # Arguments
    484
        /// - `name`: Kernel name (e.g., "batched_q4k_gemv")
    485
        /// - `ptx`: PTX source code
    486
        /// - `path`: Optional file path for source correlation
    487
    0
        pub fn register(&mut self, name: &str, ptx: &str, path: Option<&std::path::Path>) {
    488
    0
            let hash = Self::hash_ptx(ptx);
    489
    0
            self.kernels.insert(
    490
    0
                hash,
    491
                (
    492
    0
                    name.to_string(),
    493
    0
                    ptx.to_string(),
    494
    0
                    path.map(|p| p.to_path_buf()),
    495
                ),
    496
            );
    497
    0
        }
    498
    499
        /// Compute FNV-1a hash of PTX source.
    500
        #[inline]
    501
    0
        pub fn hash_ptx(ptx: &str) -> u64 {
    502
            // FNV-1a hash
    503
    0
            let mut hash: u64 = 0xcbf29ce484222325;
    504
    0
            for byte in ptx.bytes() {
    505
    0
                hash ^= byte as u64;
    506
    0
                hash = hash.wrapping_mul(0x100000001b3);
    507
    0
            }
    508
    0
            hash
    509
    0
        }
    510
    511
        /// Lookup PTX source by hash.
    512
    0
        pub fn lookup(&self, hash: u64) -> Option<&str> {
    513
    0
            self.kernels.get(&hash).map(|(_, ptx, _)| ptx.as_str())
    514
    0
        }
    515
    516
        /// Lookup kernel name by hash.
    517
    0
        pub fn lookup_name(&self, hash: u64) -> Option<&str> {
    518
    0
            self.kernels.get(&hash).map(|(name, _, _)| name.as_str())
    519
    0
        }
    520
    521
        /// Lookup file path by hash.
    522
    0
        pub fn lookup_path(&self, hash: u64) -> Option<&std::path::Path> {
    523
    0
            self.kernels
    524
    0
                .get(&hash)
    525
    0
                .and_then(|(_, _, path)| path.as_deref())
    526
    0
        }
    527
    528
        /// Get all registered hashes.
    529
    0
        pub fn hashes(&self) -> impl Iterator<Item = u64> + '_ {
    530
    0
            self.kernels.keys().copied()
    531
    0
        }
    532
    533
        /// Number of registered kernels.
    534
    0
        pub fn len(&self) -> usize {
    535
    0
            self.kernels.len()
    536
    0
        }
    537
    538
        /// Check if registry is empty.
    539
    0
        pub fn is_empty(&self) -> bool {
    540
    0
            self.kernels.is_empty()
    541
    0
        }
    542
    }
    543
    544
    /// Aggregated statistics for a brick category.
    545
    #[derive(Debug, Clone, Copy, Default)]
    546
    pub struct CategoryStats {
    547
        /// Total elapsed time (nanoseconds)
    548
        pub total_ns: u64,
    549
        /// Total elements processed
    550
        pub total_elements: u64,
    551
        /// Total samples
    552
        pub count: u64,
    553
    }
    554
    555
    impl CategoryStats {
    556
        /// Average time per sample in microseconds.
    557
        #[inline]
    558
    0
        pub fn avg_us(&self) -> f64 {
    559
    0
            if self.count == 0 {
    560
    0
                0.0
    561
            } else {
    562
    0
                self.total_ns as f64 / self.count as f64 / 1000.0
    563
            }
    564
    0
        }
    565
    566
        /// Throughput in elements per second.
    567
        #[inline]
    568
    0
        pub fn throughput(&self) -> f64 {
    569
    0
            if self.total_ns == 0 {
    570
    0
                0.0
    571
            } else {
    572
    0
                self.total_elements as f64 / (self.total_ns as f64 / 1_000_000_000.0)
    573
            }
    574
    0
        }
    575
    576
        /// Percentage of total time (given total_ns across all categories).
    577
        #[inline]
    578
    0
        pub fn percentage(&self, total: u64) -> f64 {
    579
    0
            if total == 0 {
    580
    0
                0.0
    581
            } else {
    582
    0
                100.0 * self.total_ns as f64 / total as f64
    583
            }
    584
    0
        }
    585
    }
    586
    587
    /// Accumulated per-brick statistics.
    588
    #[derive(Debug, Clone, Default)]
    589
    pub struct BrickStats {
    590
        /// Brick name
    591
        pub name: String,
    592
        /// Total samples collected
    593
        pub count: u64,
    594
        /// Total elapsed time (nanoseconds)
    595
        pub total_ns: u64,
    596
        /// Min elapsed time (nanoseconds)
    597
        pub min_ns: u64,
    598
        /// Max elapsed time (nanoseconds)
    599
        pub max_ns: u64,
    600
        /// Total elements processed
    601
        pub total_elements: u64,
    602
        /// PMAT-451: Total bytes processed (for throughput calculation)
    603
        pub total_bytes: u64,
    604
        /// PMAT-451: Total compressed bytes (for compression ratio)
    605
        pub total_compressed_bytes: u64,
    606
        /// PMAT-451: Bottleneck classification
    607
        pub bottleneck: BrickBottleneck,
    608
        /// Phase 11 (E.9.2): Total CPU cycles (from RDTSCP/CNTVCT)
    609
        pub total_cycles: u64,
    610
        /// Phase 11: Minimum CPU cycles observed
    611
        pub min_cycles: u64,
    612
        /// Phase 11: Maximum CPU cycles observed
    613
        pub max_cycles: u64,
    614
    }
    615
    616
    impl BrickStats {
    617
        /// Create new stats for a brick.
    618
    0
        pub fn new(name: &str) -> Self {
    619
    0
            Self {
    620
    0
                name: name.to_string(),
    621
    0
                count: 0,
    622
    0
                total_ns: 0,
    623
    0
                min_ns: u64::MAX,
    624
    0
                max_ns: 0,
    625
    0
                total_elements: 0,
    626
    0
                total_bytes: 0,
    627
    0
                total_compressed_bytes: 0,
    628
    0
                bottleneck: BrickBottleneck::Unknown,
    629
    0
                total_cycles: 0,
    630
    0
                min_cycles: u64::MAX,
    631
    0
                max_cycles: 0,
    632
    0
            }
    633
    0
        }
    634
    635
        /// Add a sample to statistics.
    636
    0
        pub fn add_sample(&mut self, elapsed_ns: u64, elements: u64) {
    637
    0
            self.count += 1;
    638
    0
            self.total_ns += elapsed_ns;
    639
    0
            self.min_ns = self.min_ns.min(elapsed_ns);
    640
    0
            self.max_ns = self.max_ns.max(elapsed_ns);
    641
    0
            self.total_elements += elements;
    642
    0
        }
    643
    644
        /// Phase 11 (E.9.2): Add a sample with CPU cycle count.
    645
        ///
    646
        /// Use this for frequency-invariant performance analysis.
    647
        /// Cycles are immune to CPU frequency scaling (turbo boost).
    648
    0
        pub fn add_sample_with_cycles(&mut self, elapsed_ns: u64, elements: u64, cycles: u64) {
    649
    0
            self.add_sample(elapsed_ns, elements);
    650
    0
            self.total_cycles += cycles;
    651
    0
            self.min_cycles = self.min_cycles.min(cycles);
    652
    0
            self.max_cycles = self.max_cycles.max(cycles);
    653
    0
        }
    654
    655
        /// Phase 11: Cycles per element (frequency-invariant throughput metric).
    656
        ///
    657
        /// Lower is better. This metric is immune to CPU frequency scaling.
    658
        #[must_use]
    659
    0
        pub fn cycles_per_element(&self) -> f64 {
    660
    0
            if self.total_elements == 0 {
    661
    0
                0.0
    662
            } else {
    663
    0
                self.total_cycles as f64 / self.total_elements as f64
    664
            }
    665
    0
        }
    666
    667
        /// Phase 11: Average cycles per sample.
    668
        #[must_use]
    669
    0
        pub fn avg_cycles(&self) -> f64 {
    670
    0
            if self.count == 0 {
    671
    0
                0.0
    672
            } else {
    673
    0
                self.total_cycles as f64 / self.count as f64
    674
            }
    675
    0
        }
    676
    677
        /// Phase 11: Estimated IPC (Instructions Per Cycle).
    678
        ///
    679
        /// Approximation assuming ~1 instruction per element for simple ops.
    680
        /// - Low IPC (<1.0): Memory stalls (cache misses, memory latency)
    681
        /// - High IPC (>2.0): Compute bound (efficient execution)
    682
        #[must_use]
    683
    0
        pub fn estimated_ipc(&self) -> f64 {
    684
    0
            if self.total_cycles == 0 {
    685
    0
                0.0
    686
            } else {
    687
                // Rough approximation: assume 1 instruction per element
    688
    0
                self.total_elements as f64 / self.total_cycles as f64
    689
            }
    690
    0
        }
    691
    692
        /// Phase 11: Diagnose bottleneck based on cycles vs time ratio.
    693
        ///
    694
        /// High cycles + low time = likely cache misses
    695
        /// Low cycles + high time = likely CPU throttling or context switches
    696
        #[must_use]
    697
    0
        pub fn diagnose_from_cycles(&self) -> &'static str {
    698
    0
            if self.total_cycles == 0 || self.total_ns == 0 {
    699
    0
                return "insufficient data";
    700
    0
            }
    701
    702
    0
            let ipc = self.estimated_ipc();
    703
    0
            let ns_per_cycle = self.total_ns as f64 / self.total_cycles as f64;
    704
    705
            // Typical CPU runs at ~3GHz, so 1 cycle ≈ 0.33ns
    706
            // If ns_per_cycle >> 0.33, we're seeing stalls or throttling
    707
    0
            if ipc < 0.5 {
    708
    0
                "memory-bound (low IPC, likely cache misses)"
    709
    0
            } else if ipc > 2.0 {
    710
    0
                "compute-bound (efficient)"
    711
    0
            } else if ns_per_cycle > 1.0 {
    712
    0
                "throttled or context-switched"
    713
            } else {
    714
    0
                "balanced"
    715
            }
    716
    0
        }
    717
    718
        /// PMAT-451: Add a sample with byte metrics for compression workloads.
    719
        ///
    720
        /// # Arguments
    721
        /// - `elapsed_ns`: Time taken in nanoseconds
    722
        /// - `elements`: Number of elements processed (e.g., pages)
    723
        /// - `input_bytes`: Original uncompressed size
    724
        /// - `output_bytes`: Compressed output size
    725
    0
        pub fn add_sample_with_bytes(
    726
    0
            &mut self,
    727
    0
            elapsed_ns: u64,
    728
    0
            elements: u64,
    729
    0
            input_bytes: u64,
    730
    0
            output_bytes: u64,
    731
    0
        ) {
    732
    0
            self.add_sample(elapsed_ns, elements);
    733
    0
            self.total_bytes += input_bytes;
    734
    0
            self.total_compressed_bytes += output_bytes;
    735
    0
        }
    736
    737
        /// PMAT-451: Calculate compression ratio (input_size / output_size).
    738
        /// Returns 1.0 if no compression data available.
    739
        #[must_use]
    740
    0
        pub fn compression_ratio(&self) -> f64 {
    741
    0
            if self.total_compressed_bytes == 0 {
    742
    0
                1.0
    743
            } else {
    744
    0
                self.total_bytes as f64 / self.total_compressed_bytes as f64
    745
            }
    746
    0
        }
    747
    748
        /// PMAT-451: Calculate throughput in GB/s.
    749
        /// Based on total input bytes processed.
    750
        #[must_use]
    751
    0
        pub fn throughput_gbps(&self) -> f64 {
    752
    0
            if self.total_ns == 0 {
    753
    0
                0.0
    754
            } else {
    755
    0
                let bytes_per_ns = self.total_bytes as f64 / self.total_ns as f64;
    756
    0
                bytes_per_ns * 1e9 / 1e9 // Convert to GB/s (ns to sec, bytes to GB)
    757
            }
    758
    0
        }
    759
    760
        /// PMAT-451: Set bottleneck classification.
    761
    0
        pub fn set_bottleneck(&mut self, bottleneck: BrickBottleneck) {
    762
    0
            self.bottleneck = bottleneck;
    763
    0
        }
    764
    765
        /// PMAT-451: Get bottleneck classification.
    766
        #[must_use]
    767
    0
        pub fn get_bottleneck(&self) -> BrickBottleneck {
    768
    0
            self.bottleneck
    769
    0
        }
    770
    771
        /// Average time in microseconds.
    772
        #[must_use]
    773
    0
        pub fn avg_us(&self) -> f64 {
    774
    0
            if self.count == 0 {
    775
    0
                0.0
    776
            } else {
    777
    0
                self.total_ns as f64 / self.count as f64 / 1000.0
    778
            }
    779
    0
        }
    780
    781
        /// Throughput in elements/second.
    782
        #[must_use]
    783
    0
        pub fn throughput(&self) -> f64 {
    784
    0
            if self.total_ns == 0 {
    785
    0
                0.0
    786
            } else {
    787
    0
                self.total_elements as f64 / (self.total_ns as f64 / 1_000_000_000.0)
    788
            }
    789
    0
        }
    790
    791
        /// Throughput in tokens/second (alias for throughput).
    792
        #[must_use]
    793
    0
        pub fn tokens_per_sec(&self) -> f64 {
    794
    0
            self.throughput()
    795
    0
        }
    796
    797
        /// Minimum time in microseconds.
    798
        #[must_use]
    799
    0
        pub fn min_us(&self) -> f64 {
    800
    0
            if self.min_ns == u64::MAX {
    801
    0
                0.0
    802
            } else {
    803
    0
                self.min_ns as f64 / 1000.0
    804
            }
    805
    0
        }
    806
    807
        /// Maximum time in microseconds.
    808
        #[must_use]
    809
    0
        pub fn max_us(&self) -> f64 {
    810
    0
            self.max_ns as f64 / 1000.0
    811
    0
        }
    812
    }