1use std::collections::HashMap;
7
8use nvml_wrapper::Nvml;
9use nvml_wrapper::enum_wrappers::device::{Clock, TemperatureSensor};
10use nvml_wrapper::enums::device::UsedGpuMemory;
11use nvml_wrapper::{Device, struct_wrappers::device::ProcessInfo as NvmlProcessInfo};
12
13use crate::snapshot::{GpuProcess, GpuStats};
14
15pub struct NvmlCollector {
17 nvml: Option<Nvml>,
19 init_error: Option<String>,
21 last_sample_ts: HashMap<u32, u64>,
24}
25
26impl NvmlCollector {
27 pub fn new() -> Self {
30 match Nvml::init() {
31 Ok(nvml) => Self {
32 nvml: Some(nvml),
33 init_error: None,
34 last_sample_ts: HashMap::new(),
35 },
36 Err(err) => Self {
37 nvml: None,
38 init_error: Some(err.to_string()),
39 last_sample_ts: HashMap::new(),
40 },
41 }
42 }
43
44 pub fn init_error(&self) -> Option<&str> {
46 self.init_error.as_deref()
47 }
48
49 pub fn collect(&mut self) -> Vec<GpuStats> {
52 let Some(nvml) = self.nvml.as_ref() else {
53 return Vec::new();
54 };
55 let count = nvml.device_count().unwrap_or(0);
56 let mut gpus = Vec::with_capacity(count as usize);
57 for index in 0..count {
58 let Ok(device) = nvml.device_by_index(index) else {
59 continue;
60 };
61 let last_ts = self.last_sample_ts.get(&index).copied();
62 let (stats, newest_ts) = collect_device(index, &device, last_ts);
63 if let Some(ts) = newest_ts {
64 self.last_sample_ts.insert(index, ts);
65 }
66 gpus.push(stats);
67 }
68 gpus
69 }
70}
71
72impl Default for NvmlCollector {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78fn collect_device(
80 index: u32,
81 device: &Device<'_>,
82 last_ts: Option<u64>,
83) -> (GpuStats, Option<u64>) {
84 let utilization = device.utilization_rates().ok();
85 let memory = device.memory_info().ok();
86 let (processes, newest_ts) = collect_processes(device, last_ts);
87 let stats = GpuStats {
88 index,
89 vendor: "NVIDIA".to_owned(),
90 name: device.name().unwrap_or_else(|_| format!("GPU {index}")),
91 utilization_percent: utilization.as_ref().map(|u| u.gpu as f32),
92 memory_utilization_percent: utilization.as_ref().map(|u| u.memory as f32),
93 memory_used_bytes: memory.as_ref().map(|m| m.used),
94 memory_total_bytes: memory.as_ref().map(|m| m.total),
95 temperature_c: device
96 .temperature(TemperatureSensor::Gpu)
97 .ok()
98 .map(|t| t as f32),
99 power_w: device.power_usage().ok().map(|mw| mw as f32 / 1000.0),
101 power_limit_w: device
102 .enforced_power_limit()
103 .ok()
104 .map(|mw| mw as f32 / 1000.0),
105 fan_percent: device.fan_speed(0).ok().map(|p| p as f32),
106 clock_mhz: device.clock_info(Clock::Graphics).ok(),
107 processes,
108 };
109 (stats, newest_ts)
110}
111
112fn collect_processes(device: &Device<'_>, last_ts: Option<u64>) -> (Vec<GpuProcess>, Option<u64>) {
114 let mut by_pid: HashMap<u32, GpuProcess> = HashMap::new();
117 let lists = [
118 device.running_compute_processes(),
119 device.running_graphics_processes(),
120 ];
121 for info in lists.into_iter().flat_map(|r| r.unwrap_or_default()) {
122 let memory = used_memory(&info);
123 let entry = by_pid.entry(info.pid).or_insert_with(|| GpuProcess {
124 pid: info.pid,
125 ..Default::default()
126 });
127 entry.memory_bytes = entry.memory_bytes.max(memory);
128 }
129
130 let mut newest_ts = last_ts;
132 let samples = device
135 .process_utilization_stats(last_ts)
136 .unwrap_or_default();
137 let mut latest: HashMap<u32, (u64, u32)> = HashMap::new();
138 for sample in samples {
139 newest_ts = Some(newest_ts.map_or(sample.timestamp, |t| t.max(sample.timestamp)));
140 let slot = latest.entry(sample.pid).or_insert((0, 0));
141 if sample.timestamp >= slot.0 {
142 *slot = (sample.timestamp, sample.sm_util);
143 }
144 }
145 for (pid, (_, sm_util)) in latest {
146 if let Some(process) = by_pid.get_mut(&pid) {
147 process.sm_percent = Some(sm_util as f32);
148 }
149 }
150
151 let mut processes: Vec<GpuProcess> = by_pid.into_values().collect();
152 processes.sort_unstable_by_key(|p| std::cmp::Reverse(p.memory_bytes));
153 (processes, newest_ts)
154}
155
156fn used_memory(info: &NvmlProcessInfo) -> Option<u64> {
157 match info.used_gpu_memory {
158 UsedGpuMemory::Used(bytes) => Some(bytes),
159 UsedGpuMemory::Unavailable => None,
160 }
161}