1use mold_core::ResourceSnapshot;
10use std::sync::{Arc, Mutex};
11use std::time::{Duration, SystemTime, UNIX_EPOCH};
12use tokio::sync::broadcast;
13use tokio::task::JoinHandle;
14
15const BROADCAST_BUFFER: usize = 4;
19
20#[derive(Clone)]
25pub struct ResourceBroadcaster {
26 tx: broadcast::Sender<ResourceSnapshot>,
27 latest: Arc<Mutex<Option<ResourceSnapshot>>>,
28}
29
30impl ResourceBroadcaster {
31 pub fn new() -> Arc<Self> {
32 let (tx, _rx) = broadcast::channel(BROADCAST_BUFFER);
33 Arc::new(Self {
34 tx,
35 latest: Arc::new(Mutex::new(None)),
36 })
37 }
38
39 pub fn publish(&self, snapshot: ResourceSnapshot) {
43 *self.latest.lock().expect("resource cache mutex poisoned") = Some(snapshot.clone());
47 let _ = self.tx.send(snapshot);
48 }
49
50 pub fn subscribe(&self) -> broadcast::Receiver<ResourceSnapshot> {
51 self.tx.subscribe()
52 }
53
54 pub fn latest(&self) -> Option<ResourceSnapshot> {
56 self.latest
57 .lock()
58 .expect("resource cache mutex poisoned")
59 .clone()
60 }
61}
62
63#[cfg(feature = "nvml")]
64pub(crate) mod nvml_source {
65 use mold_core::{GpuBackend, GpuSnapshot};
66 use nvml_wrapper::enums::device::UsedGpuMemory;
67 use nvml_wrapper::Nvml;
68
69 pub struct NvmlSource {
70 nvml: Nvml,
71 }
72
73 impl NvmlSource {
74 pub fn try_new() -> anyhow::Result<Self> {
75 let nvml = Nvml::init()?;
76 Ok(Self { nvml })
77 }
78
79 pub fn snapshot(&self, pid: u32) -> Vec<GpuSnapshot> {
83 let count = match self.nvml.device_count() {
84 Ok(c) => c,
85 Err(e) => {
86 tracing::debug!(err = %e, "NVML device_count failed");
87 return Vec::new();
88 }
89 };
90 let mut out = Vec::with_capacity(count as usize);
91 for ordinal in 0..count {
92 let Ok(dev) = self.nvml.device_by_index(ordinal) else {
93 continue;
94 };
95 let name = dev
96 .name()
97 .unwrap_or_else(|_| format!("CUDA Device {ordinal}"));
98 let mem = match dev.memory_info() {
99 Ok(m) => m,
100 Err(e) => {
101 tracing::debug!(ordinal, err = %e, "NVML memory_info failed");
102 continue;
103 }
104 };
105 let used_by_mold = dev.running_compute_processes().ok().map(|procs| {
106 procs
107 .iter()
108 .filter(|p| p.pid == pid)
109 .map(|p| match p.used_gpu_memory {
110 UsedGpuMemory::Used(b) => b,
111 UsedGpuMemory::Unavailable => 0,
112 })
113 .sum::<u64>()
114 });
115 let used_by_other = used_by_mold.map(|m| mem.used.saturating_sub(m));
116 let gpu_util = dev.utilization_rates().ok().map(|u| u.gpu.min(100) as u8);
119 out.push(GpuSnapshot {
120 ordinal: ordinal as usize,
121 name,
122 backend: GpuBackend::Cuda,
123 vram_total: mem.total,
124 vram_used: mem.used,
125 vram_used_by_mold: used_by_mold,
126 vram_used_by_other: used_by_other,
127 gpu_utilization: gpu_util,
128 });
129 }
130 out
131 }
132 }
133}
134
135#[cfg(feature = "nvml")]
136pub use nvml_source::NvmlSource;
137
138use mold_core::{GpuBackend, GpuSnapshot};
139
140pub(crate) fn resolve_nvidia_smi() -> &'static str {
143 if std::path::Path::new("/run/current-system/sw/bin/nvidia-smi").exists() {
144 "/run/current-system/sw/bin/nvidia-smi"
145 } else {
146 "nvidia-smi"
147 }
148}
149
150pub fn parse_nvidia_smi_line(line: &str) -> Option<(usize, String, u64, u64)> {
154 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
155 if parts.len() < 4 {
156 return None;
157 }
158 let ordinal: usize = parts[0].parse().ok()?;
159 let name = parts[1].to_string();
160 let total_mb: u64 = parts[2].parse().ok()?;
161 let used_mb: u64 = parts[3].parse().ok()?;
162 Some((ordinal, name, total_mb * 1_000_000, used_mb * 1_000_000))
165}
166
167use mold_core::{CpuSnapshot, RamSnapshot};
168use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
169
170pub fn metal_snapshot() -> Vec<GpuSnapshot> {
179 #[cfg(target_os = "macos")]
180 {
181 let mut sys = sysinfo::System::new_with_specifics(
182 sysinfo::RefreshKind::nothing().with_memory(sysinfo::MemoryRefreshKind::everything()),
183 );
184 sys.refresh_memory();
185 let total = sys.total_memory();
186 let used = sys.used_memory();
187 vec![GpuSnapshot {
188 ordinal: 0,
189 name: "Apple Metal GPU".to_string(),
190 backend: GpuBackend::Metal,
191 vram_total: total,
192 vram_used: used,
193 vram_used_by_mold: None,
194 vram_used_by_other: None,
195 gpu_utilization: None,
196 }]
197 }
198 #[cfg(not(target_os = "macos"))]
199 {
200 Vec::new()
201 }
202}
203
204pub fn ram_snapshot() -> RamSnapshot {
207 let mut sys = System::new_with_specifics(
208 RefreshKind::nothing()
209 .with_memory(sysinfo::MemoryRefreshKind::everything())
210 .with_processes(ProcessRefreshKind::nothing().with_memory()),
211 );
212 sys.refresh_memory();
213 let pid = Pid::from_u32(std::process::id());
214 sys.refresh_processes_specifics(
215 sysinfo::ProcessesToUpdate::Some(&[pid]),
216 true,
217 ProcessRefreshKind::nothing().with_memory(),
218 );
219 let total = sys.total_memory();
220 let used = sys.used_memory();
221 let used_by_mold = sys.process(pid).map(|p| p.memory()).unwrap_or(0);
222 let used_by_other = used.saturating_sub(used_by_mold);
223 RamSnapshot {
224 total,
225 used,
226 used_by_mold,
227 used_by_other,
228 }
229}
230
231pub struct SmiSource;
232
233impl SmiSource {
234 pub fn snapshot() -> Vec<GpuSnapshot> {
241 let bin = resolve_nvidia_smi();
242 let output = match std::process::Command::new(bin)
243 .args([
244 "--query-gpu=index,name,memory.total,memory.used",
245 "--format=csv,noheader,nounits",
246 ])
247 .output()
248 {
249 Ok(o) if o.status.success() => o,
250 Ok(_) => return Vec::new(),
251 Err(_) => return Vec::new(),
252 };
253 let text = match String::from_utf8(output.stdout) {
254 Ok(s) => s,
255 Err(_) => return Vec::new(),
256 };
257 Self::parse_snapshot(&text)
258 }
259
260 pub fn parse_snapshot(text: &str) -> Vec<GpuSnapshot> {
262 text.lines()
263 .filter_map(|l| {
264 let (ordinal, name, total, used) = parse_nvidia_smi_line(l)?;
265 Some(GpuSnapshot {
266 ordinal,
267 name,
268 backend: GpuBackend::Cuda,
269 vram_total: total,
270 vram_used: used,
271 vram_used_by_mold: None,
272 vram_used_by_other: None,
273 gpu_utilization: None,
274 })
275 })
276 .collect()
277 }
278}
279
280pub fn build_snapshot() -> ResourceSnapshot {
290 build_snapshot_inner(None)
291}
292
293fn build_snapshot_inner(cpu: Option<CpuSnapshot>) -> ResourceSnapshot {
294 let hostname = hostname::get()
295 .ok()
296 .and_then(|h| h.into_string().ok())
297 .unwrap_or_else(|| "unknown".to_string());
298 let timestamp = SystemTime::now()
299 .duration_since(UNIX_EPOCH)
300 .map(|d| d.as_millis() as i64)
301 .unwrap_or(0);
302
303 let gpus = collect_gpus();
304 let system_ram = ram_snapshot();
305
306 ResourceSnapshot {
307 hostname,
308 timestamp,
309 gpus,
310 system_ram,
311 cpu,
312 }
313}
314
315pub struct CpuSampler {
317 sys: System,
318 cores: u16,
319}
320
321impl CpuSampler {
322 pub fn new() -> Self {
323 let mut sys = System::new_with_specifics(
324 RefreshKind::nothing().with_cpu(CpuRefreshKind::everything().with_cpu_usage()),
325 );
326 sys.refresh_cpu_usage();
329 let cores = sys.cpus().len().min(u16::MAX as usize) as u16;
330 Self { sys, cores }
331 }
332
333 pub fn sample(&mut self) -> CpuSnapshot {
334 self.sys.refresh_cpu_usage();
335 CpuSnapshot {
336 cores: self.cores,
337 usage_percent: self.sys.global_cpu_usage().clamp(0.0, 100.0),
338 }
339 }
340}
341
342impl Default for CpuSampler {
343 fn default() -> Self {
344 Self::new()
345 }
346}
347
348#[allow(clippy::needless_return)]
349fn collect_gpus() -> Vec<GpuSnapshot> {
350 #[cfg(target_os = "macos")]
352 {
353 return metal_snapshot();
354 }
355 #[cfg(all(not(target_os = "macos"), feature = "nvml"))]
357 {
358 if let Ok(src) = NvmlSource::try_new() {
359 let gpus = src.snapshot(std::process::id());
360 if !gpus.is_empty() {
361 return gpus;
362 }
363 }
364 }
365 #[cfg(not(target_os = "macos"))]
366 {
367 SmiSource::snapshot()
368 }
369}
370
371pub fn spawn_aggregator(bcast: Arc<ResourceBroadcaster>) -> JoinHandle<()> {
375 tokio::spawn(async move {
376 bcast.publish(build_snapshot_inner(None));
380 let mut interval = tokio::time::interval(Duration::from_secs(1));
381 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
382 interval.tick().await;
384
385 let mut sampler: Option<CpuSampler> = None;
388 loop {
389 interval.tick().await;
390 let taken = sampler.take();
391 let (snap, returned) = tokio::task::spawn_blocking(move || {
392 let mut s = taken.unwrap_or_default();
393 let cpu = s.sample();
394 let snap = build_snapshot_inner(Some(cpu));
395 (snap, s)
396 })
397 .await
398 .unwrap_or_else(|_| {
399 (
400 ResourceSnapshot {
401 hostname: "unknown".to_string(),
402 timestamp: 0,
403 gpus: Vec::new(),
404 system_ram: mold_core::RamSnapshot {
405 total: 0,
406 used: 0,
407 used_by_mold: 0,
408 used_by_other: 0,
409 },
410 cpu: None,
411 },
412 CpuSampler::new(),
413 )
414 });
415 sampler = Some(returned);
416 bcast.publish(snap);
417 }
418 })
419}