## 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/monitor.rs

    Line
    Count
    Source
    1
    //! GPU Monitoring, Tracing, and Visualization (TRUENO-SPEC-010)
    2
    //!
    3
    //! This module provides comprehensive GPU monitoring capabilities:
    4
    //! - Device discovery and information
    5
    //! - Real-time metrics collection
    6
    //! - Cross-platform support (wgpu + optional CUDA)
    7
    //!
    8
    //! # Design Philosophy
    9
    //!
    10
    //! **Dual-Backend Architecture**: Supports both wgpu (cross-platform) and
    11
    //! trueno-gpu (native CUDA) for maximum flexibility.
    12
    //!
    13
    //! # References
    14
    //!
    15
    //! - Nickolls et al. (2008): GPU parallel computing model
    16
    //! - Gregg (2016): Flame graph visualization
    17
    //! - btop: NVML/ROCm SMI patterns
    18
    //!
    19
    //! # Example
    20
    //!
    21
    //! ```rust,ignore
    22
    //! use trueno::monitor::{GpuDeviceInfo, GpuMonitor};
    23
    //!
    24
    //! // Query device information
    25
    //! let info = GpuDeviceInfo::query()?;
    26
    //! println!("GPU: {}", info.name);
    27
    //! println!("VRAM: {} MB", info.vram_total / (1024 * 1024));
    28
    //!
    29
    //! // Start background monitoring
    30
    //! let monitor = GpuMonitor::new(0)?;
    31
    //! let metrics = monitor.collect()?;
    32
    //! println!("VRAM used: {} / {}", metrics.memory.used, metrics.memory.total);
    33
    //! ```
    34
    35
    use std::time::{Duration, Instant};
    36
    37
    // ============================================================================
    38
    // GPU Vendor Identification (TRUENO-SPEC-010 Section 3.2)
    39
    // ============================================================================
    40
    41
    /// GPU vendor identifier based on PCI vendor ID
    42
    ///
    43
    /// Vendor IDs from PCI-SIG registry:
    44
    /// - NVIDIA: 0x10de
    45
    /// - AMD: 0x1002
    46
    /// - Intel: 0x8086
    47
    /// - Apple: 0x106b
    48
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    49
    pub enum GpuVendor {
    50
        /// NVIDIA Corporation (0x10de)
    51
        Nvidia,
    52
        /// Advanced Micro Devices (0x1002)
    53
        Amd,
    54
        /// Intel Corporation (0x8086)
    55
        Intel,
    56
        /// Apple Inc. (0x106b)
    57
        Apple,
    58
        /// Unknown vendor with raw PCI vendor ID
    59
        Unknown(u32),
    60
    }
    61
    62
    impl GpuVendor {
    63
        /// Create vendor from PCI vendor ID
    64
        #[must_use]
    65
    0
        pub const fn from_vendor_id(id: u32) -> Self {
    66
    0
            match id {
    67
    0
                0x10de => Self::Nvidia,
    68
    0
                0x1002 => Self::Amd,
    69
    0
                0x8086 => Self::Intel,
    70
    0
                0x106b => Self::Apple,
    71
    0
                _ => Self::Unknown(id),
    72
            }
    73
    0
        }
    74
    75
        /// Get the display name for the vendor
    76
        #[must_use]
    77
    0
        pub const fn name(&self) -> &'static str {
    78
    0
            match self {
    79
    0
                Self::Nvidia => "NVIDIA",
    80
    0
                Self::Amd => "AMD",
    81
    0
                Self::Intel => "Intel",
    82
    0
                Self::Apple => "Apple",
    83
    0
                Self::Unknown(_) => "Unknown",
    84
            }
    85
    0
        }
    86
    87
        /// Check if this is an NVIDIA GPU (supports CUDA)
    88
        #[must_use]
    89
    0
        pub const fn is_nvidia(&self) -> bool {
    90
    0
            matches!(self, Self::Nvidia)
    91
    0
        }
    92
    }
    93
    94
    impl std::fmt::Display for GpuVendor {
    95
    0
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    96
    0
            match self {
    97
    0
                Self::Unknown(id) => write!(f, "Unknown (0x{id:04x})"),
    98
    0
                _ => write!(f, "{}", self.name()),
    99
            }
    100
    0
        }
    101
    }
    102
    103
    // ============================================================================
    104
    // GPU Backend Selection (TRUENO-SPEC-010 Section 3.2)
    105
    // ============================================================================
    106
    107
    /// GPU compute backend
    108
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    109
    pub enum GpuBackend {
    110
        /// Vulkan (Linux, Windows, Android)
    111
        Vulkan,
    112
        /// Metal (macOS, iOS)
    113
        Metal,
    114
        /// DirectX 12 (Windows)
    115
        Dx12,
    116
        /// DirectX 11 (Windows, fallback)
    117
        Dx11,
    118
        /// WebGPU (WASM browser)
    119
        WebGpu,
    120
        /// Native CUDA (NVIDIA only, via trueno-gpu)
    121
        Cuda,
    122
        /// OpenGL (fallback)
    123
        OpenGl,
    124
        /// CPU fallback (no GPU)
    125
        Cpu,
    126
    }
    127
    128
    impl GpuBackend {
    129
        /// Get the display name for the backend
    130
        #[must_use]
    131
    0
        pub const fn name(&self) -> &'static str {
    132
    0
            match self {
    133
    0
                Self::Vulkan => "Vulkan",
    134
    0
                Self::Metal => "Metal",
    135
    0
                Self::Dx12 => "DirectX 12",
    136
    0
                Self::Dx11 => "DirectX 11",
    137
    0
                Self::WebGpu => "WebGPU",
    138
    0
                Self::Cuda => "CUDA",
    139
    0
                Self::OpenGl => "OpenGL",
    140
    0
                Self::Cpu => "CPU",
    141
            }
    142
    0
        }
    143
    144
        /// Check if this is a GPU backend (not CPU fallback)
    145
        #[must_use]
    146
    0
        pub const fn is_gpu(&self) -> bool {
    147
    0
            !matches!(self, Self::Cpu)
    148
    0
        }
    149
    150
        /// Check if this backend supports compute shaders
    151
        #[must_use]
    152
    0
        pub const fn supports_compute(&self) -> bool {
    153
    0
            matches!(
    154
    0
                self,
    155
                Self::Vulkan | Self::Metal | Self::Dx12 | Self::WebGpu | Self::Cuda
    156
            )
    157
    0
        }
    158
    }
    159
    160
    impl std::fmt::Display for GpuBackend {
    161
    0
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    162
    0
            write!(f, "{}", self.name())
    163
    0
        }
    164
    }
    165
    166
    // ============================================================================
    167
    // GPU Device Information (TRUENO-SPEC-010 Section 3.1)
    168
    // ============================================================================
    169
    170
    /// GPU device information (TRUENO-SPEC-010)
    171
    ///
    172
    /// Contains static device properties that don't change during runtime.
    173
    #[derive(Debug, Clone)]
    174
    pub struct GpuDeviceInfo {
    175
        /// Device index (0-based)
    176
        pub index: u32,
    177
        /// Device name (e.g., "NVIDIA GeForce RTX 4090")
    178
        pub name: String,
    179
        /// Vendor identifier
    180
        pub vendor: GpuVendor,
    181
        /// Total VRAM in bytes
    182
        pub vram_total: u64,
    183
        /// Compute capability (NVIDIA) or architecture info as (major, minor)
    184
        pub compute_capability: Option<(u32, u32)>,
    185
        /// Driver version string
    186
        pub driver_version: Option<String>,
    187
        /// PCI bus ID (e.g., "0000:01:00.0")
    188
        pub pci_bus_id: Option<String>,
    189
        /// wgpu/CUDA backend being used
    190
        pub backend: GpuBackend,
    191
    }
    192
    193
    impl GpuDeviceInfo {
    194
        /// Create a new device info with required fields
    195
        #[must_use]
    196
    0
        pub fn new(
    197
    0
            index: u32,
    198
    0
            name: impl Into<String>,
    199
    0
            vendor: GpuVendor,
    200
    0
            backend: GpuBackend,
    201
    0
        ) -> Self {
    202
    0
            Self {
    203
    0
                index,
    204
    0
                name: name.into(),
    205
    0
                vendor,
    206
    0
                vram_total: 0,
    207
    0
                compute_capability: None,
    208
    0
                driver_version: None,
    209
    0
                pci_bus_id: None,
    210
    0
                backend,
    211
    0
            }
    212
    0
        }
    213
    214
        /// Set VRAM total
    215
        #[must_use]
    216
    0
        pub fn with_vram(mut self, bytes: u64) -> Self {
    217
    0
            self.vram_total = bytes;
    218
    0
            self
    219
    0
        }
    220
    221
        /// Set compute capability
    222
        #[must_use]
    223
    0
        pub fn with_compute_capability(mut self, major: u32, minor: u32) -> Self {
    224
    0
            self.compute_capability = Some((major, minor));
    225
    0
            self
    226
    0
        }
    227
    228
        /// Set driver version
    229
        #[must_use]
    230
    0
        pub fn with_driver_version(mut self, version: impl Into<String>) -> Self {
    231
    0
            self.driver_version = Some(version.into());
    232
    0
            self
    233
    0
        }
    234
    235
        /// Set PCI bus ID
    236
        #[must_use]
    237
    0
        pub fn with_pci_bus_id(mut self, bus_id: impl Into<String>) -> Self {
    238
    0
            self.pci_bus_id = Some(bus_id.into());
    239
    0
            self
    240
    0
        }
    241
    242
        /// Query device info via wgpu (cross-platform, native only)
    243
        ///
    244
        /// On WASM, use async methods with `wasm_bindgen_futures`.
    245
        ///
    246
        /// # Errors
    247
        ///
    248
        /// Returns error if no GPU is available or query fails.
    249
        #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
    250
    0
        pub fn query() -> Result<Self, MonitorError> {
    251
    0
            query_wgpu_device_info(0)
    252
    0
        }
    253
    254
        /// Query device info via wgpu for a specific device index (native only)
    255
        ///
    256
        /// On WASM, use async methods with `wasm_bindgen_futures`.
    257
        ///
    258
        /// # Errors
    259
        ///
    260
        /// Returns error if device index is invalid or query fails.
    261
        #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
    262
    0
        pub fn query_device(index: u32) -> Result<Self, MonitorError> {
    263
    0
            query_wgpu_device_info(index)
    264
    0
        }
    265
    266
        /// Enumerate all available GPU devices (native only)
    267
        ///
    268
        /// On WASM, use async methods with `wasm_bindgen_futures`.
    269
        ///
    270
        /// # Errors
    271
        ///
    272
        /// Returns error if enumeration fails.
    273
        #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
    274
    0
        pub fn enumerate() -> Result<Vec<Self>, MonitorError> {
    275
    0
            enumerate_wgpu_devices()
    276
    0
        }
    277
    278
        /// Get VRAM in megabytes (convenience method)
    279
        #[must_use]
    280
    0
        pub fn vram_mb(&self) -> u64 {
    281
    0
            self.vram_total / (1024 * 1024)
    282
    0
        }
    283
    284
        /// Get VRAM in gigabytes (convenience method)
    285
        #[must_use]
    286
    0
        pub fn vram_gb(&self) -> f64 {
    287
    0
            self.vram_total as f64 / (1024.0 * 1024.0 * 1024.0)
    288
    0
        }
    289
    290
        /// Check if device supports CUDA (is NVIDIA)
    291
        #[must_use]
    292
    0
        pub fn supports_cuda(&self) -> bool {
    293
    0
            self.vendor.is_nvidia()
    294
    0
        }
    295
    }
    296
    297
    impl std::fmt::Display for GpuDeviceInfo {
    298
    0
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    299
    0
            write!(
    300
    0
                f,
    301
    0
                "[{}] {} ({}) - {:.1} GB VRAM",
    302
                self.index,
    303
                self.name,
    304
                self.backend,
    305
    0
                self.vram_gb()
    306
            )
    307
    0
        }
    308
    }
    309
    310
    // ============================================================================
    311
    // GPU Memory Metrics (TRUENO-SPEC-010 Section 4.1.2)
    312
    // ============================================================================
    313
    314
    /// GPU memory metrics
    315
    #[derive(Debug, Clone, Copy, Default)]
    316
    pub struct GpuMemoryMetrics {
    317
        /// Total VRAM in bytes
    318
        pub total: u64,
    319
        /// Used VRAM in bytes
    320
        pub used: u64,
    321
        /// Free VRAM in bytes
    322
        pub free: u64,
    323
        /// Number of active allocations (if tracked)
    324
        pub allocations: u64,
    325
    }
    326
    327
    impl GpuMemoryMetrics {
    328
        /// Create new memory metrics
    329
        #[must_use]
    330
    0
        pub const fn new(total: u64, used: u64, free: u64) -> Self {
    331
    0
            Self {
    332
    0
                total,
    333
    0
                used,
    334
    0
                free,
    335
    0
                allocations: 0,
    336
    0
            }
    337
    0
        }
    338
    339
        /// Calculate usage percentage (0.0 - 100.0)
    340
        #[must_use]
    341
    0
        pub fn usage_percent(&self) -> f64 {
    342
    0
            if self.total == 0 {
    343
    0
                0.0
    344
            } else {
    345
    0
                (self.used as f64 / self.total as f64) * 100.0
    346
            }
    347
    0
        }
    348
    349
        /// Get used VRAM in megabytes
    350
        #[must_use]
    351
    0
        pub fn used_mb(&self) -> u64 {
    352
    0
            self.used / (1024 * 1024)
    353
    0
        }
    354
    355
        /// Get free VRAM in megabytes
    356
        #[must_use]
    357
    0
        pub fn free_mb(&self) -> u64 {
    358
    0
            self.free / (1024 * 1024)
    359
    0
        }
    360
    }
    361
    362
    // ============================================================================
    363
    // GPU Utilization Metrics (TRUENO-SPEC-010 Section 4.1.1)
    364
    // ============================================================================
    365
    366
    /// GPU utilization metrics
    367
    #[derive(Debug, Clone, Copy, Default)]
    368
    pub struct GpuUtilization {
    369
        /// GPU compute utilization (0-100%)
    370
        pub gpu_percent: u32,
    371
        /// Memory controller utilization (0-100%)
    372
        pub memory_percent: u32,
    373
        /// Video encoder utilization (0-100%), if available
    374
        pub encoder_percent: Option<u32>,
    375
        /// Video decoder utilization (0-100%), if available
    376
        pub decoder_percent: Option<u32>,
    377
    }
    378
    379
    // ============================================================================
    380
    // GPU Thermal Metrics (TRUENO-SPEC-010 Section 4.1.3)
    381
    // ============================================================================
    382
    383
    /// GPU thermal metrics
    384
    #[derive(Debug, Clone, Copy, Default)]
    385
    pub struct GpuThermalMetrics {
    386
        /// Current temperature in Celsius
    387
        pub temperature_celsius: u32,
    388
        /// Shutdown threshold temperature, if available
    389
        pub temperature_threshold_shutdown: Option<u32>,
    390
        /// Fan speed percentage (0-100), if available
    391
        pub fan_speed_percent: Option<u32>,
    392
    }
    393
    394
    impl GpuThermalMetrics {
    395
        /// Check if temperature is in safe range (< 80°C)
    396
        #[must_use]
    397
    0
        pub const fn is_safe(&self) -> bool {
    398
    0
            self.temperature_celsius < 80
    399
    0
        }
    400
    401
        /// Check if temperature is critical (>= 90°C)
    402
        #[must_use]
    403
    0
        pub const fn is_critical(&self) -> bool {
    404
    0
            self.temperature_celsius >= 90
    405
    0
        }
    406
    407
        /// Get thermal status string
    408
        #[must_use]
    409
    0
        pub const fn status(&self) -> &'static str {
    410
    0
            match self.temperature_celsius {
    411
    0
                0..=50 => "COOL",
    412
    0
                51..=70 => "WARM",
    413
    0
                71..=85 => "HOT",
    414
    0
                _ => "CRITICAL",
    415
            }
    416
    0
        }
    417
    }
    418
    419
    // ============================================================================
    420
    // GPU Power Metrics (TRUENO-SPEC-010 Section 4.1.3)
    421
    // ============================================================================
    422
    423
    /// GPU power metrics
    424
    #[derive(Debug, Clone, Copy, Default)]
    425
    pub struct GpuPowerMetrics {
    426
        /// Current power draw in watts
    427
        pub power_draw_watts: f32,
    428
        /// Power limit (TDP) in watts
    429
        pub power_limit_watts: f32,
    430
        /// Power state (P-state, 0 = highest performance)
    431
        pub power_state: u32,
    432
    }
    433
    434
    impl GpuPowerMetrics {
    435
        /// Calculate power usage percentage
    436
        #[must_use]
    437
    0
        pub fn usage_percent(&self) -> f64 {
    438
    0
            if self.power_limit_watts <= 0.0 {
    439
    0
                0.0
    440
            } else {
    441
    0
                (self.power_draw_watts as f64 / self.power_limit_watts as f64) * 100.0
    442
            }
    443
    0
        }
    444
    }
    445
    446
    // ============================================================================
    447
    // GPU Clock Metrics (TRUENO-SPEC-010 Section 4.1.4)
    448
    // ============================================================================
    449
    450
    /// GPU clock metrics
    451
    #[derive(Debug, Clone, Copy, Default)]
    452
    pub struct GpuClockMetrics {
    453
        /// Graphics/shader clock in MHz
    454
        pub graphics_mhz: u32,
    455
        /// Memory clock in MHz
    456
        pub memory_mhz: u32,
    457
        /// SM clock in MHz (NVIDIA), if available
    458
        pub sm_mhz: Option<u32>,
    459
    }
    460
    461
    // ============================================================================
    462
    // GPU PCIe Metrics (TRUENO-SPEC-010 Section 4.1.5)
    463
    // ============================================================================
    464
    465
    /// GPU PCIe metrics
    466
    #[derive(Debug, Clone, Copy, Default)]
    467
    pub struct GpuPcieMetrics {
    468
        /// PCIe TX throughput in bytes/sec
    469
        pub tx_bytes_per_sec: u64,
    470
        /// PCIe RX throughput in bytes/sec
    471
        pub rx_bytes_per_sec: u64,
    472
        /// PCIe link generation (1-5)
    473
        pub link_gen: u32,
    474
        /// PCIe link width (lanes)
    475
        pub link_width: u32,
    476
    }
    477
    478
    // ============================================================================
    479
    // Combined GPU Metrics Snapshot (TRUENO-SPEC-010 Section 4.2)
    480
    // ============================================================================
    481
    482
    /// Complete GPU metrics snapshot
    483
    ///
    484
    /// Contains all available metrics at a point in time.
    485
    #[derive(Debug, Clone)]
    486
    pub struct GpuMetrics {
    487
        /// Timestamp of measurement
    488
        pub timestamp: Instant,
    489
        /// Device index
    490
        pub device_index: u32,
    491
        /// Memory metrics
    492
        pub memory: GpuMemoryMetrics,
    493
        /// Utilization metrics
    494
        pub utilization: GpuUtilization,
    495
        /// Thermal metrics (if available)
    496
        pub thermal: Option<GpuThermalMetrics>,
    497
        /// Power metrics (if available)
    498
        pub power: Option<GpuPowerMetrics>,
    499
        /// Clock metrics (if available)
    500
        pub clocks: Option<GpuClockMetrics>,
    501
        /// PCIe metrics (if available)
    502
        pub pcie: Option<GpuPcieMetrics>,
    503
    }
    504
    505
    impl GpuMetrics {
    506
        /// Create a new metrics snapshot with only memory info
    507
        #[must_use]
    508
    0
        pub fn new(device_index: u32, memory: GpuMemoryMetrics) -> Self {
    509
    0
            Self {
    510
    0
                timestamp: Instant::now(),
    511
    0
                device_index,
    512
    0
                memory,
    513
    0
                utilization: GpuUtilization::default(),
    514
    0
                thermal: None,
    515
    0
                power: None,
    516
    0
                clocks: None,
    517
    0
                pcie: None,
    518
    0
            }
    519
    0
        }
    520
    521
        /// Age of this snapshot
    522
        #[must_use]
    523
    0
        pub fn age(&self) -> Duration {
    524
    0
            self.timestamp.elapsed()
    525
    0
        }
    526
    }
    527
    528
    // ============================================================================
    529
    // Monitor Configuration (TRUENO-SPEC-010 Section 8.2)
    530
    // ============================================================================
    531
    532
    /// Configuration for GPU monitoring
    533
    #[derive(Debug, Clone)]
    534
    pub struct MonitorConfig {
    535
        /// Polling interval for metrics collection
    536
        pub poll_interval: Duration,
    537
        /// History buffer size (number of samples to retain)
    538
        pub history_size: usize,
    539
        /// Enable background collection thread
    540
        pub background_collection: bool,
    541
    }
    542
    543
    impl Default for MonitorConfig {
    544
    0
        fn default() -> Self {
    545
    0
            Self {
    546
    0
                poll_interval: Duration::from_millis(100),
    547
    0
                history_size: 600, // 60 seconds at 100ms
    548
    0
                background_collection: false,
    549
    0
            }
    550
    0
        }
    551
    }
    552
    553
    impl MonitorConfig {
    554
        /// Create config for high-frequency monitoring
    555
        #[must_use]
    556
    0
        pub fn high_frequency() -> Self {
    557
    0
            Self {
    558
    0
                poll_interval: Duration::from_millis(50),
    559
    0
                history_size: 1200,
    560
    0
                background_collection: true,
    561
    0
            }
    562
    0
        }
    563
    564
        /// Create config for low-overhead monitoring
    565
        #[must_use]
    566
    0
        pub fn low_overhead() -> Self {
    567
    0
            Self {
    568
    0
                poll_interval: Duration::from_millis(500),
    569
    0
                history_size: 120,
    570
    0
                background_collection: false,
    571
    0
            }
    572
    0
        }
    573
    }
    574
    575
    // ============================================================================
    576
    // Error Types
    577
    // ============================================================================
    578
    579
    /// Errors from GPU monitoring operations
    580
    #[derive(Debug, Clone)]
    581
    pub enum MonitorError {
    582
        /// No GPU device available
    583
        NoDevice,
    584
        /// Invalid device index
    585
        InvalidDevice(u32),
    586
        /// GPU backend initialization failed
    587
        BackendInit(String),
    588
        /// Metrics query failed
    589
        QueryFailed(String),
    590
        /// Feature not available on this GPU/platform
    591
        NotAvailable(String),
    592
    }
    593
    594
    impl std::fmt::Display for MonitorError {
    595
    0
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    596
    0
            match self {
    597
    0
                Self::NoDevice => write!(f, "No GPU device available"),
    598
    0
                Self::InvalidDevice(idx) => write!(f, "Invalid device index: {idx}"),
    599
    0
                Self::BackendInit(msg) => write!(f, "Backend initialization failed: {msg}"),
    600
    0
                Self::QueryFailed(msg) => write!(f, "Metrics query failed: {msg}"),
    601
    0
                Self::NotAvailable(msg) => write!(f, "Feature not available: {msg}"),
    602
            }
    603
    0
        }
    604
    }
    605
    606
    impl std::error::Error for MonitorError {}
    607
    608
    // ============================================================================
    609
    // wgpu Backend Implementation
    610
    // ============================================================================
    611
    612
    /// Query device info from wgpu adapter
    613
    #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
    614
    0
    fn query_wgpu_device_info(device_index: u32) -> Result<GpuDeviceInfo, MonitorError> {
    615
        use crate::backends::gpu::runtime;
    616
    617
    0
        runtime::block_on(async {
    618
    0
            let instance = wgpu::Instance::default();
    619
    620
            // Get all adapters (wgpu 27+ returns Vec directly)
    621
    0
            let adapters = instance.enumerate_adapters(wgpu::Backends::all());
    622
    623
    0
            if adapters.is_empty() {
    624
    0
                return Err(MonitorError::NoDevice);
    625
    0
            }
    626
    627
    0
            let adapter = adapters
    628
    0
                .get(device_index as usize)
    629
    0
                .ok_or(MonitorError::InvalidDevice(device_index))?;
    630
    631
    0
            let info = adapter.get_info();
    632
    633
            // Map wgpu backend to our backend enum
    634
    0
            let backend = match info.backend {
    635
    0
                wgpu::Backend::Vulkan => GpuBackend::Vulkan,
    636
    0
                wgpu::Backend::Metal => GpuBackend::Metal,
    637
    0
                wgpu::Backend::Dx12 => GpuBackend::Dx12,
    638
    0
                wgpu::Backend::Gl => GpuBackend::OpenGl,
    639
    0
                wgpu::Backend::BrowserWebGpu => GpuBackend::WebGpu,
    640
    0
                wgpu::Backend::Noop => GpuBackend::Cpu,
    641
            };
    642
    643
            // Map vendor ID
    644
    0
            let vendor = GpuVendor::from_vendor_id(info.vendor);
    645
    646
            // Get memory limits (rough estimate from adapter limits)
    647
    0
            let limits = adapter.limits();
    648
            // Use max buffer size as a proxy for VRAM (not exact but gives an idea)
    649
    0
            let vram_estimate = limits.max_buffer_size;
    650
    651
    0
            Ok(GpuDeviceInfo::new(device_index, info.name, vendor, backend)
    652
    0
                .with_vram(vram_estimate)
    653
    0
                .with_driver_version(format!("{:?}", info.driver_info)))
    654
    0
        })
    655
    0
    }
    656
    657
    /// Enumerate all wgpu devices
    658
    #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
    659
    0
    fn enumerate_wgpu_devices() -> Result<Vec<GpuDeviceInfo>, MonitorError> {
    660
        use crate::backends::gpu::runtime;
    661
    662
    0
        runtime::block_on(async {
    663
    0
            let instance = wgpu::Instance::default();
    664
    0
            let adapters = instance.enumerate_adapters(wgpu::Backends::all());
    665
    666
    0
            if adapters.is_empty() {
    667
    0
                return Err(MonitorError::NoDevice);
    668
    0
            }
    669
    670
    0
            let mut devices = Vec::with_capacity(adapters.len());
    671
    672
    0
            for (idx, adapter) in adapters.iter().enumerate() {
    673
    0
                let info: wgpu::AdapterInfo = adapter.get_info();
    674
    675
    0
                let backend = match info.backend {
    676
    0
                    wgpu::Backend::Vulkan => GpuBackend::Vulkan,
    677
    0
                    wgpu::Backend::Metal => GpuBackend::Metal,
    678
    0
                    wgpu::Backend::Dx12 => GpuBackend::Dx12,
    679
    0
                    wgpu::Backend::Gl => GpuBackend::OpenGl,
    680
    0
                    wgpu::Backend::BrowserWebGpu => GpuBackend::WebGpu,
    681
    0
                    wgpu::Backend::Noop => GpuBackend::Cpu,
    682
                };
    683
    684
    0
                let vendor = GpuVendor::from_vendor_id(info.vendor);
    685
    0
                let limits = adapter.limits();
    686
    687
    0
                devices.push(
    688
    0
                    GpuDeviceInfo::new(idx as u32, info.name, vendor, backend)
    689
    0
                        .with_vram(limits.max_buffer_size)
    690
    0
                        .with_driver_version(format!("{:?}", info.driver_info)),
    691
                );
    692
            }
    693
    694
    0
            Ok(devices)
    695
    0
        })
    696
    0
    }
    697
    698
    // ============================================================================
    699
    // CUDA Backend Implementation (TRUENO-SPEC-010 Section 8)
    700
    // ============================================================================
    701
    702
    /// Query device info from native CUDA via trueno-gpu
    703
    ///
    704
    /// This provides more accurate information than wgpu including:
    705
    /// - Actual device name (e.g., "NVIDIA GeForce RTX 4090")
    706
    /// - Accurate VRAM total from cuDeviceTotalMem
    707
    #[cfg(feature = "cuda-monitor")]
    708
    pub fn query_cuda_device_info(device_index: u32) -> Result<GpuDeviceInfo, MonitorError> {
    709
        use trueno_gpu::CudaDeviceInfo;
    710
    711
        let cuda_info = CudaDeviceInfo::query(device_index)
    712
            .map_err(|e| MonitorError::BackendInit(format!("CUDA query failed: {}", e)))?;
    713
    714
        Ok(GpuDeviceInfo::new(
    715
            cuda_info.index,
    716
            cuda_info.name,
    717
            GpuVendor::Nvidia, // CUDA is NVIDIA-only
    718
            GpuBackend::Cuda,
    719
        )
    720
        .with_vram(cuda_info.total_memory))
    721
    }
    722
    723
    /// Enumerate all CUDA devices via trueno-gpu
    724
    #[cfg(feature = "cuda-monitor")]
    725
    pub fn enumerate_cuda_devices() -> Result<Vec<GpuDeviceInfo>, MonitorError> {
    726
        use trueno_gpu::CudaDeviceInfo;
    727
    728
        let cuda_devices = CudaDeviceInfo::enumerate()
    729
            .map_err(|e| MonitorError::BackendInit(format!("CUDA enumerate failed: {}", e)))?;
    730
    731
        Ok(cuda_devices
    732
            .into_iter()
    733
            .map(|cuda_info| {
    734
                GpuDeviceInfo::new(
    735
                    cuda_info.index,
    736
                    cuda_info.name,
    737
                    GpuVendor::Nvidia,
    738
                    GpuBackend::Cuda,
    739
                )
    740
                .with_vram(cuda_info.total_memory)
    741
            })
    742
            .collect())
    743
    }
    744
    745
    /// Query real-time CUDA memory metrics
    746
    ///
    747
    /// Returns current free/used VRAM from cuMemGetInfo.
    748
    #[cfg(feature = "cuda-monitor")]
    749
    pub fn query_cuda_memory(device_index: u32) -> Result<GpuMemoryMetrics, MonitorError> {
    750
        use trueno_gpu::driver::CudaContext;
    751
        use trueno_gpu::CudaMemoryInfo;
    752
    753
        let ctx = CudaContext::new(device_index as i32)
    754
            .map_err(|e| MonitorError::BackendInit(format!("CUDA context failed: {}", e)))?;
    755
    756
        let mem = CudaMemoryInfo::query(&ctx)
    757
            .map_err(|e| MonitorError::QueryFailed(format!("CUDA memory query failed: {}", e)))?;
    758
    759
        Ok(GpuMemoryMetrics::new(mem.total, mem.used(), mem.free))
    760
    }
    761
    762
    /// Check if CUDA monitoring is available
    763
    #[cfg(feature = "cuda-monitor")]
    764
    #[must_use]
    765
    pub fn cuda_monitor_available() -> bool {
    766
        trueno_gpu::cuda_monitoring_available()
    767
    }
    768
    769
    /// Check if CUDA monitoring is available (stub when feature disabled)
    770
    #[cfg(not(feature = "cuda-monitor"))]
    771
    #[must_use]
    772
    0
    pub fn cuda_monitor_available() -> bool {
    773
    0
        false
    774
    0
    }
    775
    776
    // ============================================================================
    777
    // GPU Monitor (TRUENO-SPEC-010 Section 8.2)
    778
    // ============================================================================
    779
    780
    use std::collections::VecDeque;
    781
    use std::sync::{Arc, Mutex, RwLock};
    782
    783
    /// GPU Monitor for real-time metrics collection (TRUENO-SPEC-010)
    784
    ///
    785
    /// Provides both on-demand and background metric collection with configurable
    786
    /// polling intervals and history retention.
    787
    ///
    788
    /// # Example
    789
    ///
    790
    /// ```rust,ignore
    791
    /// use trueno::monitor::{GpuMonitor, MonitorConfig};
    792
    ///
    793
    /// // Create monitor for device 0
    794
    /// let monitor = GpuMonitor::new(0, MonitorConfig::default())?;
    795
    ///
    796
    /// // Get latest metrics
    797
    /// let metrics = monitor.latest()?;
    798
    /// println!("GPU usage: {}%", metrics.utilization.gpu_percent);
    799
    ///
    800
    /// // Get history (ring buffer)
    801
    /// let history = monitor.history();
    802
    /// println!("Samples: {}", history.len());
    803
    /// ```
    804
    pub struct GpuMonitor {
    805
        /// Device info
    806
        device_info: GpuDeviceInfo,
    807
        /// Configuration
    808
        config: MonitorConfig,
    809
        /// Metrics history (ring buffer)
    810
        history: Arc<RwLock<VecDeque<GpuMetrics>>>,
    811
        /// Background thread handle
    812
        #[cfg(feature = "gpu")]
    813
        _background_handle: Option<std::thread::JoinHandle<()>>,
    814
        /// Stop signal for background thread
    815
        stop_signal: Arc<Mutex<bool>>,
    816
    }
    817
    818
    impl GpuMonitor {
    819
        /// Create a new GPU monitor for the specified device
    820
        ///
    821
        /// # Errors
    822
        ///
    823
        /// Returns error if device is not found or initialization fails.
    824
        #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
    825
    0
        pub fn new(device_index: u32, config: MonitorConfig) -> Result<Self, MonitorError> {
    826
    0
            let device_info = GpuDeviceInfo::query_device(device_index)?;
    827
    0
            let history = Arc::new(RwLock::new(VecDeque::with_capacity(config.history_size)));
    828
    0
            let stop_signal = Arc::new(Mutex::new(false));
    829
    830
    0
            let monitor = Self {
    831
    0
                device_info,
    832
    0
                config,
    833
    0
                history,
    834
    0
                _background_handle: None,
    835
    0
                stop_signal,
    836
    0
            };
    837
    838
    0
            Ok(monitor)
    839
    0
        }
    840
    841
        /// Create monitor without GPU feature (for testing)
    842
        #[cfg(not(feature = "gpu"))]
    843
        pub fn new(_device_index: u32, _config: MonitorConfig) -> Result<Self, MonitorError> {
    844
            Err(MonitorError::NotAvailable(
    845
                "GPU feature not enabled".to_string(),
    846
            ))
    847
        }
    848
    849
        /// Create a mock monitor for testing (no GPU required)
    850
        #[must_use]
    851
    0
        pub fn mock(device_info: GpuDeviceInfo, config: MonitorConfig) -> Self {
    852
    0
            Self {
    853
    0
                device_info,
    854
    0
                config,
    855
    0
                history: Arc::new(RwLock::new(VecDeque::with_capacity(16))),
    856
    0
                #[cfg(feature = "gpu")]
    857
    0
                _background_handle: None,
    858
    0
                stop_signal: Arc::new(Mutex::new(false)),
    859
    0
            }
    860
    0
        }
    861
    862
        /// Get device info
    863
        #[must_use]
    864
    0
        pub fn device_info(&self) -> &GpuDeviceInfo {
    865
    0
            &self.device_info
    866
    0
        }
    867
    868
        /// Get current configuration
    869
        #[must_use]
    870
    0
        pub fn config(&self) -> &MonitorConfig {
    871
    0
            &self.config
    872
    0
        }
    873
    874
        /// Collect metrics sample now
    875
        ///
    876
        /// This performs an immediate collection and adds to history.
    877
    0
        pub fn collect(&self) -> Result<GpuMetrics, MonitorError> {
    878
            // For now, return basic memory metrics
    879
            // Full implementation would query NVML/wgpu for utilization, thermal, etc.
    880
    0
            let memory = GpuMemoryMetrics::new(
    881
    0
                self.device_info.vram_total,
    882
                0, // Would query actual usage
    883
    0
                self.device_info.vram_total,
    884
            );
    885
    886
    0
            let metrics = GpuMetrics::new(self.device_info.index, memory);
    887
    888
            // Add to history
    889
    0
            if let Ok(mut history) = self.history.write() {
    890
    0
                if history.len() >= self.config.history_size {
    891
    0
                    history.pop_front();
    892
    0
                }
    893
    0
                history.push_back(metrics.clone());
    894
    0
            }
    895
    896
    0
            Ok(metrics)
    897
    0
        }
    898
    899
        /// Get the latest metrics snapshot (without collecting)
    900
    0
        pub fn latest(&self) -> Result<GpuMetrics, MonitorError> {
    901
    0
            self.history
    902
    0
                .read()
    903
    0
                .ok()
    904
    0
                .and_then(|h| h.back().cloned())
    905
    0
                .ok_or(MonitorError::QueryFailed(
    906
    0
                    "No metrics available".to_string(),
    907
    0
                ))
    908
    0
        }
    909
    910
        /// Get history buffer (read-only snapshot)
    911
        #[must_use]
    912
    0
        pub fn history(&self) -> Vec<GpuMetrics> {
    913
    0
            self.history
    914
    0
                .read()
    915
    0
                .map(|h| h.iter().cloned().collect())
    916
    0
                .unwrap_or_default()
    917
    0
        }
    918
    919
        /// Get number of samples in history
    920
        #[must_use]
    921
    0
        pub fn sample_count(&self) -> usize {
    922
    0
            self.history.read().map(|h| h.len()).unwrap_or(0)
    923
    0
        }
    924
    925
        /// Clear history buffer
    926
    0
        pub fn clear_history(&self) {
    927
    0
            if let Ok(mut history) = self.history.write() {
    928
    0
                history.clear();
    929
    0
            }
    930
    0
        }
    931
    932
        /// Check if background collection is active
    933
        #[must_use]
    934
    0
        pub fn is_collecting(&self) -> bool {
    935
            #[cfg(feature = "gpu")]
    936
            {
    937
    0
                self._background_handle.is_some()
    938
            }
    939
            #[cfg(not(feature = "gpu"))]
    940
            {
    941
                false
    942
            }
    943
    0
        }
    944
    945
        /// Stop background collection (if running)
    946
    0
        pub fn stop(&self) {
    947
    0
            if let Ok(mut stop) = self.stop_signal.lock() {
    948
    0
                *stop = true;
    949
    0
            }
    950
    0
        }
    951
    }
    952
    953
    impl Drop for GpuMonitor {
    954
    0
        fn drop(&mut self) {
    955
    0
            self.stop();
    956
    0
        }
    957
    }
    958
    959
    // ============================================================================
    960
    // Tests (EXTREME TDD - Tests First!)
    961
    // ============================================================================
    962
    963
    #[cfg(test)]
    964
    #[allow(clippy::unwrap_used, clippy::expect_used)]
    965
    mod tests {
    966
        use super::*;
    967
    968
        // =========================================================================
    969
        // H₀-MON-01: GpuVendor identification
    970
        // =========================================================================
    971
    972
        #[test]
    973
        fn h0_mon_01_vendor_nvidia_id() {
    974
            let vendor = GpuVendor::from_vendor_id(0x10de);
    975
            assert_eq!(vendor, GpuVendor::Nvidia);
    976
            assert!(vendor.is_nvidia());
    977
            assert_eq!(vendor.name(), "NVIDIA");
    978
        }
    979
    980
        #[test]
    981
        fn h0_mon_02_vendor_amd_id() {
    982
            let vendor = GpuVendor::from_vendor_id(0x1002);
    983
            assert_eq!(vendor, GpuVendor::Amd);
    984
            assert!(!vendor.is_nvidia());
    985
            assert_eq!(vendor.name(), "AMD");
    986
        }
    987
    988
        #[test]
    989
        fn h0_mon_03_vendor_intel_id() {
    990
            let vendor = GpuVendor::from_vendor_id(0x8086);
    991
            assert_eq!(vendor, GpuVendor::Intel);
    992
            assert!(!vendor.is_nvidia());
    993
            assert_eq!(vendor.name(), "Intel");
    994
        }
    995
    996
        #[test]
    997
        fn h0_mon_04_vendor_apple_id() {
    998
            let vendor = GpuVendor::from_vendor_id(0x106b);
    999
            assert_eq!(vendor, GpuVendor::Apple);
    1000
            assert!(!vendor.is_nvidia());
    1001
            assert_eq!(vendor.name(), "Apple");
    1002
        }
    1003
    1004
        #[test]
    1005
        fn h0_mon_05_vendor_unknown_id() {
    1006
            let vendor = GpuVendor::from_vendor_id(0x9999);
    1007
            assert_eq!(vendor, GpuVendor::Unknown(0x9999));
    1008
            assert!(!vendor.is_nvidia());
    1009
            assert_eq!(vendor.name(), "Unknown");
    1010
        }
    1011
    1012
        #[test]
    1013
        fn h0_mon_06_vendor_display() {
    1014
            assert_eq!(format!("{}", GpuVendor::Nvidia), "NVIDIA");
    1015
            assert_eq!(format!("{}", GpuVendor::Amd), "AMD");
    1016
            assert_eq!(
    1017
                format!("{}", GpuVendor::Unknown(0x1234)),
    1018
                "Unknown (0x1234)"
    1019
            );
    1020
        }
    1021
    1022
        // =========================================================================
    1023
        // H₀-MON-10: GpuBackend identification
    1024
        // =========================================================================
    1025
    1026
        #[test]
    1027
        fn h0_mon_10_backend_names() {
    1028
            assert_eq!(GpuBackend::Vulkan.name(), "Vulkan");
    1029
            assert_eq!(GpuBackend::Metal.name(), "Metal");
    1030
            assert_eq!(GpuBackend::Dx12.name(), "DirectX 12");
    1031
            assert_eq!(GpuBackend::Dx11.name(), "DirectX 11");
    1032
            assert_eq!(GpuBackend::WebGpu.name(), "WebGPU");
    1033
            assert_eq!(GpuBackend::Cuda.name(), "CUDA");
    1034
            assert_eq!(GpuBackend::OpenGl.name(), "OpenGL");
    1035
            assert_eq!(GpuBackend::Cpu.name(), "CPU");
    1036
        }
    1037
    1038
        #[test]
    1039
        fn h0_mon_11_backend_is_gpu() {
    1040
            assert!(GpuBackend::Vulkan.is_gpu());
    1041
            assert!(GpuBackend::Metal.is_gpu());
    1042
            assert!(GpuBackend::Dx12.is_gpu());
    1043
            assert!(GpuBackend::Dx11.is_gpu());
    1044
            assert!(GpuBackend::WebGpu.is_gpu());
    1045
            assert!(GpuBackend::Cuda.is_gpu());
    1046
            assert!(GpuBackend::OpenGl.is_gpu());
    1047
            assert!(!GpuBackend::Cpu.is_gpu());
    1048
        }
    1049
    1050
        #[test]
    1051
        fn h0_mon_12_backend_supports_compute() {
    1052
            assert!(GpuBackend::Vulkan.supports_compute());
    1053
            assert!(GpuBackend::Metal.supports_compute());
    1054
            assert!(GpuBackend::Dx12.supports_compute());
    1055
            assert!(GpuBackend::WebGpu.supports_compute());
    1056
            assert!(GpuBackend::Cuda.supports_compute());
    1057
            assert!(!GpuBackend::Dx11.supports_compute()); // DX11 compute shaders limited
    1058
            assert!(!GpuBackend::OpenGl.supports_compute());
    1059
            assert!(!GpuBackend::Cpu.supports_compute());
    1060
        }
    1061
    1062
        // =========================================================================
    1063
        // H₀-MON-20: GpuDeviceInfo construction
    1064
        // =========================================================================
    1065
    1066
        #[test]
    1067
        fn h0_mon_20_device_info_basic() {
    1068
            let info = GpuDeviceInfo::new(0, "Test GPU", GpuVendor::Nvidia, GpuBackend::Vulkan);
    1069
    1070
            assert_eq!(info.index, 0);
    1071
            assert_eq!(info.name, "Test GPU");
    1072
            assert_eq!(info.vendor, GpuVendor::Nvidia);
    1073
            assert_eq!(info.backend, GpuBackend::Vulkan);
    1074
            assert_eq!(info.vram_total, 0);
    1075
            assert!(info.compute_capability.is_none());
    1076
        }
    1077
    1078
        #[test]
    1079
        fn h0_mon_21_device_info_builder() {
    1080
            let info = GpuDeviceInfo::new(0, "RTX 4090", GpuVendor::Nvidia, GpuBackend::Vulkan)
    1081
                .with_vram(24_000_000_000)
    1082
                .with_compute_capability(8, 9)
    1083
                .with_driver_version("535.154.05")
    1084
                .with_pci_bus_id("0000:01:00.0");
    1085
    1086
            assert_eq!(info.vram_total, 24_000_000_000);
    1087
            assert_eq!(info.compute_capability, Some((8, 9)));
    1088
            assert_eq!(info.driver_version, Some("535.154.05".to_string()));
    1089
            assert_eq!(info.pci_bus_id, Some("0000:01:00.0".to_string()));
    1090
        }
    1091
    1092
        #[test]
    1093
        fn h0_mon_22_device_info_vram_helpers() {
    1094
            let info = GpuDeviceInfo::new(0, "Test", GpuVendor::Nvidia, GpuBackend::Vulkan)
    1095
                .with_vram(24 * 1024 * 1024 * 1024); // 24 GB
    1096
    1097
            assert_eq!(info.vram_mb(), 24 * 1024);
    1098
            assert!((info.vram_gb() - 24.0).abs() < 0.01);
    1099
        }
    1100
    1101
        #[test]
    1102
        fn h0_mon_23_device_info_supports_cuda() {
    1103
            let nvidia = GpuDeviceInfo::new(0, "RTX", GpuVendor::Nvidia, GpuBackend::Vulkan);
    1104
            let amd = GpuDeviceInfo::new(0, "RX", GpuVendor::Amd, GpuBackend::Vulkan);
    1105
    1106
            assert!(nvidia.supports_cuda());
    1107
            assert!(!amd.supports_cuda());
    1108
        }
    1109
    1110
        #[test]
    1111
        fn h0_mon_24_device_info_display() {
    1112
            let info = GpuDeviceInfo::new(0, "RTX 4090", GpuVendor::Nvidia, GpuBackend::Vulkan)
    1113
                .with_vram(24 * 1024 * 1024 * 1024);
    1114
    1115
            let display = format!("{info}");
    1116
            assert!(display.contains("RTX 4090"));
    1117
            assert!(display.contains("Vulkan"));
    1118
            assert!(display.contains("24.0"));
    1119
        }
    1120
    1121
        // =========================================================================
    1122
        // H₀-MON-30: GpuMemoryMetrics
    1123
        // =========================================================================
    1124
    1125
        #[test]
    1126
        fn h0_mon_30_memory_metrics_basic() {
    1127
            let mem = GpuMemoryMetrics::new(24_000_000_000, 8_000_000_000, 16_000_000_000);
    1128
    1129
            assert_eq!(mem.total, 24_000_000_000);
    1130
            assert_eq!(mem.used, 8_000_000_000);
    1131
            assert_eq!(mem.free, 16_000_000_000);
    1132
        }
    1133
    1134
        #[test]
    1135
        fn h0_mon_31_memory_metrics_usage_percent() {
    1136
            let mem = GpuMemoryMetrics::new(100, 25, 75);
    1137
            assert!((mem.usage_percent() - 25.0).abs() < 0.01);
    1138
        }
    1139
    1140
        #[test]
    1141
        fn h0_mon_32_memory_metrics_usage_percent_zero_total() {
    1142
            let mem = GpuMemoryMetrics::new(0, 0, 0);
    1143
            assert!((mem.usage_percent() - 0.0).abs() < 0.01);
    1144
        }
    1145
    1146
        #[test]
    1147
        fn h0_mon_33_memory_metrics_mb_helpers() {
    1148
            let mem = GpuMemoryMetrics::new(
    1149
                24 * 1024 * 1024 * 1024,
    1150
                8 * 1024 * 1024 * 1024,
    1151
                16 * 1024 * 1024 * 1024,
    1152
            );
    1153
    1154
            assert_eq!(mem.used_mb(), 8 * 1024);
    1155
            assert_eq!(mem.free_mb(), 16 * 1024);
    1156
        }
    1157
    1158
        // =========================================================================
    1159
        // H₀-MON-40: GpuThermalMetrics
    1160
        // =========================================================================
    1161
    1162
        #[test]
    1163
        fn h0_mon_40_thermal_safe() {
    1164
            let thermal = GpuThermalMetrics {
    1165
                temperature_celsius: 50,
    1166
                ..Default::default()
    1167
            };
    1168
            assert!(thermal.is_safe());
    1169
            assert!(!thermal.is_critical());
    1170
            assert_eq!(thermal.status(), "COOL");
    1171
        }
    1172
    1173
        #[test]
    1174
        fn h0_mon_41_thermal_warm() {
    1175
            let thermal = GpuThermalMetrics {
    1176
                temperature_celsius: 65,
    1177
                ..Default::default()
    1178
            };
    1179
            assert!(thermal.is_safe());
    1180
            assert!(!thermal.is_critical());
    1181
            assert_eq!(thermal.status(), "WARM");
    1182
        }
    1183
    1184
        #[test]
    1185
        fn h0_mon_42_thermal_hot() {
    1186
            let thermal = GpuThermalMetrics {
    1187
                temperature_celsius: 82,
    1188
                ..Default::default()
    1189
            };
    1190
            assert!(!thermal.is_safe());
    1191
            assert!(!thermal.is_critical());
    1192
            assert_eq!(thermal.status(), "HOT");
    1193
        }
    1194
    1195
        #[test]
    1196
        fn h0_mon_43_thermal_critical() {
    1197
            let thermal = GpuThermalMetrics {
    1198
                temperature_celsius: 95,
    1199
                ..Default::default()
    1200
            };
    1201
            assert!(!thermal.is_safe());
    1202
            assert!(thermal.is_critical());
    1203
            assert_eq!(thermal.status(), "CRITICAL");
    1204
        }
    1205
    1206
        // =========================================================================
    1207
        // H₀-MON-50: GpuPowerMetrics
    1208
        // =========================================================================
    1209
    1210
        #[test]
    1211
        fn h0_mon_50_power_usage_percent() {
    1212
            let power = GpuPowerMetrics {
    1213
                power_draw_watts: 225.0,
    1214
                power_limit_watts: 450.0,
    1215
                power_state: 0,
    1216
            };
    1217
            assert!((power.usage_percent() - 50.0).abs() < 0.01);
    1218
        }
    1219
    1220
        #[test]
    1221
        fn h0_mon_51_power_usage_percent_zero_limit() {
    1222
            let power = GpuPowerMetrics {
    1223
                power_draw_watts: 100.0,
    1224
                power_limit_watts: 0.0,
    1225
                power_state: 0,
    1226
            };
    1227
            assert!((power.usage_percent() - 0.0).abs() < 0.01);
    1228
        }
    1229
    1230
        // =========================================================================
    1231
        // H₀-MON-60: GpuMetrics
    1232
        // =========================================================================
    1233
    1234
        #[test]
    1235
        fn h0_mon_60_metrics_creation() {
    1236
            let mem = GpuMemoryMetrics::new(1000, 500, 500);
    1237
            let metrics = GpuMetrics::new(0, mem);
    1238
    1239
            assert_eq!(metrics.device_index, 0);
    1240
            assert_eq!(metrics.memory.total, 1000);
    1241
            assert!(metrics.thermal.is_none());
    1242
            assert!(metrics.power.is_none());
    1243
        }
    1244
    1245
        #[test]
    1246
        fn h0_mon_61_metrics_age() {
    1247
            let mem = GpuMemoryMetrics::new(1000, 500, 500);
    1248
            let metrics = GpuMetrics::new(0, mem);
    1249
    1250
            // Age should be very small immediately after creation
    1251
            assert!(metrics.age() < Duration::from_millis(100));
    1252
        }
    1253
    1254
        // =========================================================================
    1255
        // H₀-MON-70: MonitorConfig
    1256
        // =========================================================================
    1257
    1258
        #[test]
    1259
        fn h0_mon_70_config_default() {
    1260
            let config = MonitorConfig::default();
    1261
    1262
            assert_eq!(config.poll_interval, Duration::from_millis(100));
    1263
            assert_eq!(config.history_size, 600);
    1264
            assert!(!config.background_collection);
    1265
        }
    1266
    1267
        #[test]
    1268
        fn h0_mon_71_config_high_frequency() {
    1269
            let config = MonitorConfig::high_frequency();
    1270
    1271
            assert_eq!(config.poll_interval, Duration::from_millis(50));
    1272
            assert_eq!(config.history_size, 1200);
    1273
            assert!(config.background_collection);
    1274
        }
    1275
    1276
        #[test]
    1277
        fn h0_mon_72_config_low_overhead() {
    1278
            let config = MonitorConfig::low_overhead();
    1279
    1280
            assert_eq!(config.poll_interval, Duration::from_millis(500));
    1281
            assert_eq!(config.history_size, 120);
    1282
            assert!(!config.background_collection);
    1283
        }
    1284
    1285
        // =========================================================================
    1286
        // H₀-MON-80: MonitorError
    1287
        // =========================================================================
    1288
    1289
        #[test]
    1290
        fn h0_mon_80_error_display() {
    1291
            assert_eq!(
    1292
                format!("{}", MonitorError::NoDevice),
    1293
                "No GPU device available"
    1294
            );
    1295
            assert_eq!(
    1296
                format!("{}", MonitorError::InvalidDevice(5)),
    1297
                "Invalid device index: 5"
    1298
            );
    1299
            assert_eq!(
    1300
                format!("{}", MonitorError::BackendInit("test".to_string())),
    1301
                "Backend initialization failed: test"
    1302
            );
    1303
        }
    1304
    1305
        // =========================================================================
    1306
        // Integration tests (require GPU feature)
    1307
        // =========================================================================
    1308
    1309
        #[test]
    1310
        #[cfg(feature = "gpu")]
    1311
        fn h0_mon_90_query_device_info() {
    1312
            // This test requires actual GPU hardware
    1313
            match GpuDeviceInfo::query() {
    1314
                Ok(info) => {
    1315
                    // Verify we got valid data
    1316
                    assert!(!info.name.is_empty());
    1317
                    assert!(info.backend.is_gpu());
    1318
                    println!("Found GPU: {info}");
    1319
                }
    1320
                Err(MonitorError::NoDevice) => {
    1321
                    // No GPU is OK for CI environments
    1322
                    println!("No GPU available (expected in CI)");
    1323
                }
    1324
                Err(e) => {
    1325
                    panic!("Unexpected error: {e}");
    1326
                }
    1327
            }
    1328
        }
    1329
    1330
        #[test]
    1331
        #[cfg(feature = "gpu")]
    1332
        fn h0_mon_91_enumerate_devices() {
    1333
            match GpuDeviceInfo::enumerate() {
    1334
                Ok(devices) => {
    1335
                    for dev in &devices {
    1336
                        println!("Found: {dev}");
    1337
                    }
    1338
                }
    1339
                Err(MonitorError::NoDevice) => {
    1340
                    println!("No GPU available (expected in CI)");
    1341
                }
    1342
                Err(e) => {
    1343
                    panic!("Unexpected error: {e}");
    1344
                }
    1345
            }
    1346
        }
    1347
    1348
        // =========================================================================
    1349
        // H₀-MON-100: GpuMonitor (mock)
    1350
        // =========================================================================
    1351
    1352
        #[test]
    1353
        fn h0_mon_100_monitor_mock_creation() {
    1354
            let info = GpuDeviceInfo::new(0, "Mock GPU", GpuVendor::Nvidia, GpuBackend::Vulkan)
    1355
                .with_vram(24 * 1024 * 1024 * 1024);
    1356
            let config = MonitorConfig::default();
    1357
            let monitor = GpuMonitor::mock(info, config);
    1358
    1359
            assert_eq!(monitor.device_info().name, "Mock GPU");
    1360
            assert_eq!(monitor.config().poll_interval, Duration::from_millis(100));
    1361
        }
    1362
    1363
        #[test]
    1364
        fn h0_mon_101_monitor_collect() {
    1365
            let info = GpuDeviceInfo::new(0, "Mock GPU", GpuVendor::Nvidia, GpuBackend::Vulkan)
    1366
                .with_vram(24 * 1024 * 1024 * 1024);
    1367
            let config = MonitorConfig::default();
    1368
            let monitor = GpuMonitor::mock(info, config);
    1369
    1370
            // Initially no samples
    1371
            assert_eq!(monitor.sample_count(), 0);
    1372
    1373
            // Collect a sample
    1374
            let metrics = monitor.collect().expect("collect should work");
    1375
            assert_eq!(metrics.device_index, 0);
    1376
            assert_eq!(monitor.sample_count(), 1);
    1377
    1378
            // Collect more samples
    1379
            monitor.collect().expect("collect should work");
    1380
            monitor.collect().expect("collect should work");
    1381
            assert_eq!(monitor.sample_count(), 3);
    1382
        }
    1383
    1384
        #[test]
    1385
        fn h0_mon_102_monitor_history_buffer() {
    1386
            let info = GpuDeviceInfo::new(0, "Mock GPU", GpuVendor::Nvidia, GpuBackend::Vulkan)
    1387
                .with_vram(1024);
    1388
    1389
            // Small history size to test ring buffer
    1390
            let config = MonitorConfig {
    1391
                history_size: 3,
    1392
                ..Default::default()
    1393
            };
    1394
            let monitor = GpuMonitor::mock(info, config);
    1395
    1396
            // Fill beyond capacity
    1397
            for _ in 0..5 {
    1398
                monitor.collect().expect("collect should work");
    1399
            }
    1400
    1401
            // Should only have 3 samples (ring buffer)
    1402
            assert_eq!(monitor.sample_count(), 3);
    1403
    1404
            // History should return 3 items
    1405
            let history = monitor.history();
    1406
            assert_eq!(history.len(), 3);
    1407
        }
    1408
    1409
        #[test]
    1410
        fn h0_mon_103_monitor_latest() {
    1411
            let info = GpuDeviceInfo::new(0, "Mock GPU", GpuVendor::Nvidia, GpuBackend::Vulkan)
    1412
                .with_vram(1024);
    1413
            let config = MonitorConfig::default();
    1414
            let monitor = GpuMonitor::mock(info, config);
    1415
    1416
            // No samples yet - should error
    1417
            assert!(monitor.latest().is_err());
    1418
    1419
            // After collecting, latest should work
    1420
            monitor.collect().expect("collect should work");
    1421
            let latest = monitor.latest().expect("latest should work");
    1422
            assert_eq!(latest.device_index, 0);
    1423
        }
    1424
    1425
        #[test]
    1426
        fn h0_mon_104_monitor_clear_history() {
    1427
            let info = GpuDeviceInfo::new(0, "Mock GPU", GpuVendor::Nvidia, GpuBackend::Vulkan)
    1428
                .with_vram(1024);
    1429
            let config = MonitorConfig::default();
    1430
            let monitor = GpuMonitor::mock(info, config);
    1431
    1432
            // Collect some samples
    1433
            monitor.collect().expect("collect should work");
    1434
            monitor.collect().expect("collect should work");
    1435
            assert_eq!(monitor.sample_count(), 2);
    1436
    1437
            // Clear history
    1438
            monitor.clear_history();
    1439
            assert_eq!(monitor.sample_count(), 0);
    1440
        }
    1441
    1442
        #[test]
    1443
        fn h0_mon_105_monitor_is_collecting() {
    1444
            let info = GpuDeviceInfo::new(0, "Mock GPU", GpuVendor::Nvidia, GpuBackend::Vulkan);
    1445
            let config = MonitorConfig::default();
    1446
            let monitor = GpuMonitor::mock(info, config);
    1447
    1448
            // Mock monitor is not actively collecting in background
    1449
            assert!(!monitor.is_collecting());
    1450
        }
    1451
    1452
        #[test]
    1453
        fn h0_mon_106_monitor_stop() {
    1454
            let info = GpuDeviceInfo::new(0, "Mock GPU", GpuVendor::Nvidia, GpuBackend::Vulkan);
    1455
            let config = MonitorConfig::default();
    1456
            let monitor = GpuMonitor::mock(info, config);
    1457
    1458
            // Stop should not panic even without background collection
    1459
            monitor.stop();
    1460
        }
    1461
    1462
        // =========================================================================
    1463
        // H₀-MON-110: GpuMonitor integration tests (require GPU feature)
    1464
        // =========================================================================
    1465
    1466
        #[test]
    1467
        #[cfg(feature = "gpu")]
    1468
        fn h0_mon_110_monitor_real_gpu() {
    1469
            match GpuMonitor::new(0, MonitorConfig::default()) {
    1470
                Ok(monitor) => {
    1471
                    println!("GPU Monitor: {}", monitor.device_info());
    1472
    1473
                    // Collect a sample
    1474
                    match monitor.collect() {
    1475
                        Ok(metrics) => {
    1476
                            println!("Collected metrics: device={}", metrics.device_index);
    1477
                            assert_eq!(monitor.sample_count(), 1);
    1478
                        }
    1479
                        Err(e) => {
    1480
                            println!("Collect failed (expected in CI): {e}");
    1481
                        }
    1482
                    }
    1483
                }
    1484
                Err(MonitorError::NoDevice) => {
    1485
                    println!("No GPU available (expected in CI)");
    1486
                }
    1487
                Err(e) => {
    1488
                    panic!("Unexpected error: {e}");
    1489
                }
    1490
            }
    1491
        }
    1492
    1493
        // =========================================================================
    1494
        // H₀-MON-120: CUDA monitoring integration tests
    1495
        // =========================================================================
    1496
    1497
        #[test]
    1498
        fn h0_mon_120_cuda_monitor_available_check() {
    1499
            // Should return false without cuda-monitor feature
    1500
            let available = super::cuda_monitor_available();
    1501
            #[cfg(feature = "cuda-monitor")]
    1502
            {
    1503
                // With feature, returns true/false based on hardware
    1504
                println!("CUDA monitoring available: {}", available);
    1505
            }
    1506
            #[cfg(not(feature = "cuda-monitor"))]
    1507
            {
    1508
                assert!(!available, "Should be false without cuda-monitor feature");
    1509
            }
    1510
        }
    1511
    1512
        #[test]
    1513
        #[cfg(feature = "cuda-monitor")]
    1514
        fn h0_mon_121_query_cuda_device_info() {
    1515
            use super::query_cuda_device_info;
    1516
    1517
            match query_cuda_device_info(0) {
    1518
                Ok(info) => {
    1519
                    assert!(!info.name.is_empty());
    1520
                    assert_eq!(info.vendor, GpuVendor::Nvidia);
    1521
                    assert_eq!(info.backend, GpuBackend::Cuda);
    1522
                    assert!(info.vram_total > 0);
    1523
                    println!("CUDA Device: {}", info);
    1524
                }
    1525
                Err(e) => {
    1526
                    println!("No CUDA device (expected in CI): {}", e);
    1527
                }
    1528
            }
    1529
        }
    1530
    1531
        #[test]
    1532
        #[cfg(feature = "cuda-monitor")]
    1533
        fn h0_mon_122_enumerate_cuda_devices() {
    1534
            use super::enumerate_cuda_devices;
    1535
    1536
            match enumerate_cuda_devices() {
    1537
                Ok(devices) => {
    1538
                    for dev in &devices {
    1539
                        assert_eq!(dev.vendor, GpuVendor::Nvidia);
    1540
                        assert_eq!(dev.backend, GpuBackend::Cuda);
    1541
                        println!("Found CUDA device: {}", dev);
    1542
                    }
    1543
                }
    1544
                Err(e) => {
    1545
                    println!("CUDA enumeration failed (expected in CI): {}", e);
    1546
                }
    1547
            }
    1548
        }
    1549
    1550
        #[test]
    1551
        #[cfg(feature = "cuda-monitor")]
    1552
        fn h0_mon_123_query_cuda_memory() {
    1553
            use super::query_cuda_memory;
    1554
    1555
            match query_cuda_memory(0) {
    1556
                Ok(mem) => {
    1557
                    assert!(mem.total > 0);
    1558
                    assert!(mem.free <= mem.total);
    1559
                    println!(
    1560
                        "CUDA Memory: {} / {} MB ({:.1}% used)",
    1561
                        mem.used_mb(),
    1562
                        mem.total / (1024 * 1024),
    1563
                        mem.usage_percent()
    1564
                    );
    1565
                }
    1566
                Err(e) => {
    1567
                    println!("CUDA memory query failed (expected in CI): {}", e);
    1568
                }
    1569
            }
    1570
        }
    1571
    }
    1572
    1573
    // ============================================================================
    1574
    // Unified Compute Device Abstraction (TRUENO-SPEC-020)
    1575
    // ============================================================================
    1576
    1577
    /// Device identification
    1578
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    1579
    pub struct DeviceId(pub u32);
    1580
    1581
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    1582
    pub enum DeviceType {
    1583
        Cpu,
    1584
        NvidiaGpu,
    1585
        AmdGpu,
    1586
        IntelGpu,
    1587
        AppleSilicon,
    1588
        Hpu, // Hardware Processing Unit (e.g., Gaudi, TPU)
    1589
    }
    1590
    1591
    /// Unified compute device abstraction (TRUENO-SPEC-020)
    1592
    pub trait ComputeDevice: Send + Sync {
    1593
        /// Device identification
    1594
        fn device_id(&self) -> DeviceId;
    1595
        fn device_name(&self) -> &str;
    1596
        fn device_type(&self) -> DeviceType;
    1597
    1598
        /// Compute metrics
    1599
        fn compute_utilization(&self) -> anyhow::Result<f64>;      // 0.0-100.0%
    1600
        fn compute_clock_mhz(&self) -> anyhow::Result<u32>;
    1601
        fn compute_temperature_c(&self) -> anyhow::Result<f64>;
    1602
        fn compute_power_watts(&self) -> anyhow::Result<f64>;
    1603
        fn compute_power_limit_watts(&self) -> anyhow::Result<f64>;
    1604
    1605
        /// Memory metrics
    1606
        fn memory_used_bytes(&self) -> anyhow::Result<u64>;
    1607
        fn memory_total_bytes(&self) -> anyhow::Result<u64>;
    1608
        fn memory_bandwidth_gbps(&self) -> anyhow::Result<f64>;
    1609
    1610
        /// Streaming multiprocessor / Compute Unit metrics
    1611
        fn sm_count(&self) -> u32;
    1612
        fn active_sm_count(&self) -> anyhow::Result<u32>;
    1613
    1614
        /// PCIe / Interconnect metrics
    1615
        fn pcie_tx_bytes_per_sec(&self) -> anyhow::Result<u64>;
    1616
        fn pcie_rx_bytes_per_sec(&self) -> anyhow::Result<u64>;
    1617
        fn pcie_generation(&self) -> u8;
    1618
        fn pcie_width(&self) -> u8;
    1619
    }
    1620