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 physical_ordinal_for_worker(
145 logical_ordinal: usize,
146 cuda_visible_devices: Option<&str>,
147) -> Option<usize> {
148 let Some(visible) = cuda_visible_devices
149 .map(str::trim)
150 .filter(|v| !v.is_empty())
151 else {
152 return Some(logical_ordinal);
153 };
154 visible
155 .split(',')
156 .map(str::trim)
157 .nth(logical_ordinal)?
158 .parse::<usize>()
159 .ok()
160}
161
162pub(crate) fn resolve_nvidia_smi() -> &'static str {
163 if std::path::Path::new("/run/current-system/sw/bin/nvidia-smi").exists() {
164 "/run/current-system/sw/bin/nvidia-smi"
165 } else {
166 "nvidia-smi"
167 }
168}
169
170pub fn parse_nvidia_smi_line(line: &str) -> Option<(usize, String, u64, u64)> {
174 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
175 if parts.len() < 4 {
176 return None;
177 }
178 let ordinal: usize = parts[0].parse().ok()?;
179 let name = parts[1].to_string();
180 let total_mb: u64 = parts[2].parse().ok()?;
181 let used_mb: u64 = parts[3].parse().ok()?;
182 Some((ordinal, name, total_mb * 1_000_000, used_mb * 1_000_000))
185}
186
187use mold_core::{CpuSnapshot, RamSnapshot};
188use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
189
190pub fn metal_snapshot() -> Vec<GpuSnapshot> {
199 #[cfg(target_os = "macos")]
200 {
201 let mut sys = sysinfo::System::new_with_specifics(
202 sysinfo::RefreshKind::nothing().with_memory(sysinfo::MemoryRefreshKind::everything()),
203 );
204 sys.refresh_memory();
205 let total = sys.total_memory();
206 let used = sys.used_memory();
207 vec![GpuSnapshot {
208 ordinal: 0,
209 name: "Apple Metal GPU".to_string(),
210 backend: GpuBackend::Metal,
211 vram_total: total,
212 vram_used: used,
213 vram_used_by_mold: None,
214 vram_used_by_other: None,
215 gpu_utilization: None,
216 }]
217 }
218 #[cfg(not(target_os = "macos"))]
219 {
220 Vec::new()
221 }
222}
223
224pub fn ram_snapshot() -> RamSnapshot {
227 let mut sys = System::new_with_specifics(
228 RefreshKind::nothing()
229 .with_memory(sysinfo::MemoryRefreshKind::everything())
230 .with_processes(ProcessRefreshKind::nothing().with_memory()),
231 );
232 sys.refresh_memory();
233 let pid = Pid::from_u32(std::process::id());
234 sys.refresh_processes_specifics(
235 sysinfo::ProcessesToUpdate::Some(&[pid]),
236 true,
237 ProcessRefreshKind::nothing().with_memory(),
238 );
239 let total = sys.total_memory();
240 let used = sys.used_memory();
241 let used_by_mold = sys.process(pid).map(|p| p.memory()).unwrap_or(0);
242 let used_by_other = used.saturating_sub(used_by_mold);
243 RamSnapshot {
244 total,
245 used,
246 used_by_mold,
247 used_by_other,
248 }
249}
250
251pub struct SmiSource;
252
253impl SmiSource {
254 pub fn snapshot() -> Vec<GpuSnapshot> {
261 let bin = resolve_nvidia_smi();
262 let output = match std::process::Command::new(bin)
263 .args([
264 "--query-gpu=index,name,memory.total,memory.used",
265 "--format=csv,noheader,nounits",
266 ])
267 .output()
268 {
269 Ok(o) if o.status.success() => o,
270 Ok(_) => return Vec::new(),
271 Err(_) => return Vec::new(),
272 };
273 let text = match String::from_utf8(output.stdout) {
274 Ok(s) => s,
275 Err(_) => return Vec::new(),
276 };
277 Self::parse_snapshot(&text)
278 }
279
280 pub fn parse_snapshot(text: &str) -> Vec<GpuSnapshot> {
282 text.lines()
283 .filter_map(|l| {
284 let (ordinal, name, total, used) = parse_nvidia_smi_line(l)?;
285 Some(GpuSnapshot {
286 ordinal,
287 name,
288 backend: GpuBackend::Cuda,
289 vram_total: total,
290 vram_used: used,
291 vram_used_by_mold: None,
292 vram_used_by_other: None,
293 gpu_utilization: None,
294 })
295 })
296 .collect()
297 }
298}
299
300pub fn build_snapshot() -> ResourceSnapshot {
310 build_snapshot_inner(None)
311}
312
313fn build_snapshot_inner(cpu: Option<CpuSnapshot>) -> ResourceSnapshot {
314 let hostname = hostname::get()
315 .ok()
316 .and_then(|h| h.into_string().ok())
317 .unwrap_or_else(|| "unknown".to_string());
318 let timestamp = SystemTime::now()
319 .duration_since(UNIX_EPOCH)
320 .map(|d| d.as_millis() as i64)
321 .unwrap_or(0);
322
323 let gpus = collect_gpus();
324 let system_ram = ram_snapshot();
325
326 ResourceSnapshot {
327 hostname,
328 timestamp,
329 gpus,
330 system_ram,
331 cpu,
332 }
333}
334
335pub struct CpuSampler {
337 sys: System,
338 cores: u16,
339}
340
341impl CpuSampler {
342 pub fn new() -> Self {
343 let mut sys = System::new_with_specifics(
344 RefreshKind::nothing().with_cpu(CpuRefreshKind::everything().with_cpu_usage()),
345 );
346 sys.refresh_cpu_usage();
349 let cores = sys.cpus().len().min(u16::MAX as usize) as u16;
350 Self { sys, cores }
351 }
352
353 pub fn sample(&mut self) -> CpuSnapshot {
354 self.sys.refresh_cpu_usage();
355 CpuSnapshot {
356 cores: self.cores,
357 usage_percent: self.sys.global_cpu_usage().clamp(0.0, 100.0),
358 }
359 }
360}
361
362impl Default for CpuSampler {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368#[allow(clippy::needless_return)]
369fn collect_gpus() -> Vec<GpuSnapshot> {
370 #[cfg(target_os = "macos")]
372 {
373 return metal_snapshot();
374 }
375 #[cfg(all(not(target_os = "macos"), feature = "nvml"))]
377 {
378 if let Ok(src) = NvmlSource::try_new() {
379 let gpus = src.snapshot(std::process::id());
380 if !gpus.is_empty() {
381 return gpus;
382 }
383 }
384 }
385 #[cfg(not(target_os = "macos"))]
386 {
387 SmiSource::snapshot()
388 }
389}
390
391pub fn spawn_aggregator(bcast: Arc<ResourceBroadcaster>) -> JoinHandle<()> {
395 tokio::spawn(async move {
396 bcast.publish(build_snapshot_inner(None));
400 let mut interval = tokio::time::interval(Duration::from_secs(1));
401 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
402 interval.tick().await;
404
405 let mut sampler: Option<CpuSampler> = None;
408 loop {
409 interval.tick().await;
410 let taken = sampler.take();
411 let (snap, returned) = tokio::task::spawn_blocking(move || {
412 let mut s = taken.unwrap_or_default();
413 let cpu = s.sample();
414 let snap = build_snapshot_inner(Some(cpu));
415 (snap, s)
416 })
417 .await
418 .unwrap_or_else(|_| {
419 (
420 ResourceSnapshot {
421 hostname: "unknown".to_string(),
422 timestamp: 0,
423 gpus: Vec::new(),
424 system_ram: mold_core::RamSnapshot {
425 total: 0,
426 used: 0,
427 used_by_mold: 0,
428 used_by_other: 0,
429 },
430 cpu: None,
431 },
432 CpuSampler::new(),
433 )
434 });
435 sampler = Some(returned);
436 bcast.publish(snap);
437 }
438 })
439}