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 process_utilization: bool,
27}
28
29impl NvmlCollector {
30 pub fn new() -> Self {
33 let (nvml, init_error) = Self::init();
34 Self {
35 nvml,
36 init_error,
37 last_sample_ts: HashMap::new(),
38 process_utilization: true,
39 }
40 }
41
42 #[cfg(target_os = "macos")]
45 fn init() -> (Option<Nvml>, Option<String>) {
46 (None, None)
47 }
48
49 #[cfg(not(target_os = "macos"))]
50 fn init() -> (Option<Nvml>, Option<String>) {
51 match Nvml::init() {
52 Ok(nvml) => (Some(nvml), None),
53 Err(err) => (None, Some(err.to_string())),
54 }
55 }
56
57 pub fn set_process_utilization(&mut self, enabled: bool) {
60 self.process_utilization = enabled;
61 }
62
63 pub fn init_error(&self) -> Option<&str> {
65 self.init_error.as_deref()
66 }
67
68 pub fn collect(&mut self) -> Vec<GpuStats> {
71 let Some(nvml) = self.nvml.as_ref() else {
72 return Vec::new();
73 };
74 let count = nvml.device_count().unwrap_or(0);
75 let mut gpus = Vec::with_capacity(count as usize);
76 for index in 0..count {
77 let Ok(device) = nvml.device_by_index(index) else {
78 continue;
79 };
80 let last_ts = self.last_sample_ts.get(&index).copied();
81 let (stats, newest_ts) =
82 collect_device(index, &device, last_ts, self.process_utilization);
83 if let Some(ts) = newest_ts {
84 self.last_sample_ts.insert(index, ts);
85 }
86 gpus.push(stats);
87 }
88 gpus
89 }
90}
91
92impl Default for NvmlCollector {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98fn collect_device(
100 index: u32,
101 device: &Device<'_>,
102 last_ts: Option<u64>,
103 process_utilization: bool,
104) -> (GpuStats, Option<u64>) {
105 let utilization = device.utilization_rates().ok();
106 let memory = device.memory_info().ok();
107 let (processes, newest_ts) = collect_processes(device, last_ts, process_utilization);
108 let stats = GpuStats {
109 index,
110 vendor: "NVIDIA".to_owned(),
111 name: device.name().unwrap_or_else(|_| format!("GPU {index}")),
112 integrated: false,
114 utilization_percent: utilization.as_ref().map(|u| u.gpu as f32),
115 memory_utilization_percent: utilization.as_ref().map(|u| u.memory as f32),
116 memory_used_bytes: memory.as_ref().map(|m| m.used),
117 memory_total_bytes: memory.as_ref().map(|m| m.total),
118 temperature_c: device
119 .temperature(TemperatureSensor::Gpu)
120 .ok()
121 .map(|t| t as f32),
122 power_w: device.power_usage().ok().map(|mw| mw as f32 / 1000.0),
124 power_limit_w: device
125 .enforced_power_limit()
126 .ok()
127 .map(|mw| mw as f32 / 1000.0),
128 fan_percent: device.fan_speed(0).ok().map(|p| p as f32),
129 clock_mhz: device.clock_info(Clock::Graphics).ok(),
130 processes,
131 };
132 (stats, newest_ts)
133}
134
135fn collect_processes(
138 device: &Device<'_>,
139 last_ts: Option<u64>,
140 with_utilization: bool,
141) -> (Vec<GpuProcess>, Option<u64>) {
142 let mut by_pid: HashMap<u32, GpuProcess> = HashMap::new();
145 let lists = [
146 device.running_compute_processes(),
147 device.running_graphics_processes(),
148 ];
149 for info in lists.into_iter().flat_map(|r| r.unwrap_or_default()) {
150 let memory = used_memory(&info);
151 let entry = by_pid.entry(info.pid).or_insert_with(|| GpuProcess {
152 pid: info.pid,
153 ..Default::default()
154 });
155 entry.memory_bytes = entry.memory_bytes.max(memory);
156 }
157
158 let mut newest_ts = last_ts;
161 if !with_utilization || by_pid.is_empty() {
162 let mut processes: Vec<GpuProcess> = by_pid.into_values().collect();
163 processes.sort_unstable_by_key(|p| std::cmp::Reverse(p.memory_bytes));
164 return (processes, newest_ts);
165 }
166 let samples = device
169 .process_utilization_stats(last_ts)
170 .unwrap_or_default();
171 let mut latest: HashMap<u32, (u64, u32)> = HashMap::new();
172 for sample in samples {
173 newest_ts = Some(newest_ts.map_or(sample.timestamp, |t| t.max(sample.timestamp)));
174 let slot = latest.entry(sample.pid).or_insert((0, 0));
175 if sample.timestamp >= slot.0 {
176 *slot = (sample.timestamp, sample.sm_util);
177 }
178 }
179 for (pid, (_, sm_util)) in latest {
180 if let Some(process) = by_pid.get_mut(&pid) {
181 process.sm_percent = Some(sm_util as f32);
182 }
183 }
184
185 let mut processes: Vec<GpuProcess> = by_pid.into_values().collect();
186 processes.sort_unstable_by_key(|p| std::cmp::Reverse(p.memory_bytes));
187 (processes, newest_ts)
188}
189
190fn used_memory(info: &NvmlProcessInfo) -> Option<u64> {
191 match info.used_gpu_memory {
192 UsedGpuMemory::Used(bytes) => Some(bytes),
193 UsedGpuMemory::Unavailable => None,
194 }
195}