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
}