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