use mold_core::ResourceSnapshot;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::broadcast;
use tokio::task::JoinHandle;
const BROADCAST_BUFFER: usize = 4;
#[derive(Clone)]
pub struct ResourceBroadcaster {
tx: broadcast::Sender<ResourceSnapshot>,
latest: Arc<Mutex<Option<ResourceSnapshot>>>,
}
impl ResourceBroadcaster {
pub fn new() -> Arc<Self> {
let (tx, _rx) = broadcast::channel(BROADCAST_BUFFER);
Arc::new(Self {
tx,
latest: Arc::new(Mutex::new(None)),
})
}
pub fn publish(&self, snapshot: ResourceSnapshot) {
*self.latest.lock().expect("resource cache mutex poisoned") = Some(snapshot.clone());
let _ = self.tx.send(snapshot);
}
pub fn subscribe(&self) -> broadcast::Receiver<ResourceSnapshot> {
self.tx.subscribe()
}
pub fn latest(&self) -> Option<ResourceSnapshot> {
self.latest
.lock()
.expect("resource cache mutex poisoned")
.clone()
}
}
#[cfg(feature = "nvml")]
pub(crate) mod nvml_source {
use mold_core::{GpuBackend, GpuSnapshot};
use nvml_wrapper::enums::device::UsedGpuMemory;
use nvml_wrapper::Nvml;
pub struct NvmlSource {
nvml: Nvml,
}
impl NvmlSource {
pub fn try_new() -> anyhow::Result<Self> {
let nvml = Nvml::init()?;
Ok(Self { nvml })
}
pub fn snapshot(&self, pid: u32) -> Vec<GpuSnapshot> {
let count = match self.nvml.device_count() {
Ok(c) => c,
Err(e) => {
tracing::debug!(err = %e, "NVML device_count failed");
return Vec::new();
}
};
let mut out = Vec::with_capacity(count as usize);
for ordinal in 0..count {
let Ok(dev) = self.nvml.device_by_index(ordinal) else {
continue;
};
let name = dev
.name()
.unwrap_or_else(|_| format!("CUDA Device {ordinal}"));
let mem = match dev.memory_info() {
Ok(m) => m,
Err(e) => {
tracing::debug!(ordinal, err = %e, "NVML memory_info failed");
continue;
}
};
let used_by_mold = dev.running_compute_processes().ok().map(|procs| {
procs
.iter()
.filter(|p| p.pid == pid)
.map(|p| match p.used_gpu_memory {
UsedGpuMemory::Used(b) => b,
UsedGpuMemory::Unavailable => 0,
})
.sum::<u64>()
});
let used_by_other = used_by_mold.map(|m| mem.used.saturating_sub(m));
let gpu_util = dev.utilization_rates().ok().map(|u| u.gpu.min(100) as u8);
out.push(GpuSnapshot {
ordinal: ordinal as usize,
name,
backend: GpuBackend::Cuda,
vram_total: mem.total,
vram_used: mem.used,
vram_used_by_mold: used_by_mold,
vram_used_by_other: used_by_other,
gpu_utilization: gpu_util,
});
}
out
}
}
}
#[cfg(feature = "nvml")]
pub use nvml_source::NvmlSource;
use mold_core::{GpuBackend, GpuSnapshot};
pub(crate) fn resolve_nvidia_smi() -> &'static str {
if std::path::Path::new("/run/current-system/sw/bin/nvidia-smi").exists() {
"/run/current-system/sw/bin/nvidia-smi"
} else {
"nvidia-smi"
}
}
pub fn parse_nvidia_smi_line(line: &str) -> Option<(usize, String, u64, u64)> {
let parts: Vec<&str> = line.split(',').map(str::trim).collect();
if parts.len() < 4 {
return None;
}
let ordinal: usize = parts[0].parse().ok()?;
let name = parts[1].to_string();
let total_mb: u64 = parts[2].parse().ok()?;
let used_mb: u64 = parts[3].parse().ok()?;
Some((ordinal, name, total_mb * 1_000_000, used_mb * 1_000_000))
}
use mold_core::{CpuSnapshot, RamSnapshot};
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
pub fn metal_snapshot() -> Vec<GpuSnapshot> {
#[cfg(target_os = "macos")]
{
let mut sys = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::nothing().with_memory(sysinfo::MemoryRefreshKind::everything()),
);
sys.refresh_memory();
let total = sys.total_memory();
let used = sys.used_memory();
vec![GpuSnapshot {
ordinal: 0,
name: "Apple Metal GPU".to_string(),
backend: GpuBackend::Metal,
vram_total: total,
vram_used: used,
vram_used_by_mold: None,
vram_used_by_other: None,
gpu_utilization: None,
}]
}
#[cfg(not(target_os = "macos"))]
{
Vec::new()
}
}
pub fn ram_snapshot() -> RamSnapshot {
let mut sys = System::new_with_specifics(
RefreshKind::nothing()
.with_memory(sysinfo::MemoryRefreshKind::everything())
.with_processes(ProcessRefreshKind::nothing().with_memory()),
);
sys.refresh_memory();
let pid = Pid::from_u32(std::process::id());
sys.refresh_processes_specifics(
sysinfo::ProcessesToUpdate::Some(&[pid]),
true,
ProcessRefreshKind::nothing().with_memory(),
);
let total = sys.total_memory();
let used = sys.used_memory();
let used_by_mold = sys.process(pid).map(|p| p.memory()).unwrap_or(0);
let used_by_other = used.saturating_sub(used_by_mold);
RamSnapshot {
total,
used,
used_by_mold,
used_by_other,
}
}
pub struct SmiSource;
impl SmiSource {
pub fn snapshot() -> Vec<GpuSnapshot> {
let bin = resolve_nvidia_smi();
let output = match std::process::Command::new(bin)
.args([
"--query-gpu=index,name,memory.total,memory.used",
"--format=csv,noheader,nounits",
])
.output()
{
Ok(o) if o.status.success() => o,
Ok(_) => return Vec::new(),
Err(_) => return Vec::new(),
};
let text = match String::from_utf8(output.stdout) {
Ok(s) => s,
Err(_) => return Vec::new(),
};
Self::parse_snapshot(&text)
}
pub fn parse_snapshot(text: &str) -> Vec<GpuSnapshot> {
text.lines()
.filter_map(|l| {
let (ordinal, name, total, used) = parse_nvidia_smi_line(l)?;
Some(GpuSnapshot {
ordinal,
name,
backend: GpuBackend::Cuda,
vram_total: total,
vram_used: used,
vram_used_by_mold: None,
vram_used_by_other: None,
gpu_utilization: None,
})
})
.collect()
}
}
pub fn build_snapshot() -> ResourceSnapshot {
build_snapshot_inner(None)
}
fn build_snapshot_inner(cpu: Option<CpuSnapshot>) -> ResourceSnapshot {
let hostname = hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_else(|| "unknown".to_string());
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
let gpus = collect_gpus();
let system_ram = ram_snapshot();
ResourceSnapshot {
hostname,
timestamp,
gpus,
system_ram,
cpu,
}
}
pub struct CpuSampler {
sys: System,
cores: u16,
}
impl CpuSampler {
pub fn new() -> Self {
let mut sys = System::new_with_specifics(
RefreshKind::nothing().with_cpu(CpuRefreshKind::everything().with_cpu_usage()),
);
sys.refresh_cpu_usage();
let cores = sys.cpus().len().min(u16::MAX as usize) as u16;
Self { sys, cores }
}
pub fn sample(&mut self) -> CpuSnapshot {
self.sys.refresh_cpu_usage();
CpuSnapshot {
cores: self.cores,
usage_percent: self.sys.global_cpu_usage().clamp(0.0, 100.0),
}
}
}
impl Default for CpuSampler {
fn default() -> Self {
Self::new()
}
}
#[allow(clippy::needless_return)]
fn collect_gpus() -> Vec<GpuSnapshot> {
#[cfg(target_os = "macos")]
{
return metal_snapshot();
}
#[cfg(all(not(target_os = "macos"), feature = "nvml"))]
{
if let Ok(src) = NvmlSource::try_new() {
let gpus = src.snapshot(std::process::id());
if !gpus.is_empty() {
return gpus;
}
}
}
#[cfg(not(target_os = "macos"))]
{
SmiSource::snapshot()
}
}
pub fn spawn_aggregator(bcast: Arc<ResourceBroadcaster>) -> JoinHandle<()> {
tokio::spawn(async move {
bcast.publish(build_snapshot_inner(None));
let mut interval = tokio::time::interval(Duration::from_secs(1));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
interval.tick().await;
let mut sampler: Option<CpuSampler> = None;
loop {
interval.tick().await;
let taken = sampler.take();
let (snap, returned) = tokio::task::spawn_blocking(move || {
let mut s = taken.unwrap_or_default();
let cpu = s.sample();
let snap = build_snapshot_inner(Some(cpu));
(snap, s)
})
.await
.unwrap_or_else(|_| {
(
ResourceSnapshot {
hostname: "unknown".to_string(),
timestamp: 0,
gpus: Vec::new(),
system_ram: mold_core::RamSnapshot {
total: 0,
used: 0,
used_by_mold: 0,
used_by_other: 0,
},
cpu: None,
},
CpuSampler::new(),
)
});
sampler = Some(returned);
bcast.publish(snap);
}
})
}