use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
#[derive(Debug)]
pub struct GpuState {
pub used_mib: AtomicU32,
pub total_mib: AtomicU32,
pub name: std::sync::Mutex<String>,
}
impl GpuState {
pub fn new() -> Self {
Self {
used_mib: AtomicU32::new(0),
total_mib: AtomicU32::new(0),
name: std::sync::Mutex::new("GPU".into()),
}
}
pub fn read(&self) -> (u32, u32) {
(
self.used_mib.load(Ordering::Relaxed),
self.total_mib.load(Ordering::Relaxed),
)
}
pub fn ratio(&self) -> f64 {
let (used, total) = self.read();
if total == 0 {
return 0.0;
}
(used as f64 / total as f64).clamp(0.0, 1.0)
}
pub fn label(&self) -> String {
let (used, total) = self.read();
if total == 0 {
return "N/A".into();
}
format!(
"{:.1} GB / {:.1} GB",
used as f64 / 1024.0,
total as f64 / 1024.0
)
}
pub fn gpu_name(&self) -> String {
self.name.lock().unwrap().clone()
}
}
pub fn spawn_gpu_monitor() -> Arc<GpuState> {
let state = Arc::new(GpuState::new());
let bg = state.clone();
tokio::spawn(async move {
loop {
if let Some((used, total, name)) = poll_nvidia_smi().await {
bg.used_mib.store(used, Ordering::Relaxed);
bg.total_mib.store(total, Ordering::Relaxed);
if !name.is_empty() {
*bg.name.lock().unwrap() = name;
}
}
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
});
state
}
async fn poll_nvidia_smi() -> Option<(u32, u32, String)> {
let output = tokio::process::Command::new("nvidia-smi")
.args([
"--query-gpu=memory.used,memory.total,name",
"--format=csv,noheader,nounits",
])
.output()
.await
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let line = stdout.trim();
let parts: Vec<&str> = line.splitn(3, ',').collect();
if parts.len() < 2 {
return None;
}
let used: u32 = parts[0].trim().parse().ok()?;
let total: u32 = parts[1].trim().parse().ok()?;
let name = parts
.get(2)
.map(|s| s.trim().to_string())
.unwrap_or_default();
Some((used, total, name))
}