/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 | | |