hematite/ui/
gpu_monitor.rs1use std::sync::atomic::{AtomicU32, Ordering};
7use std::sync::Arc;
8
9#[derive(Debug)]
11pub struct GpuState {
12 pub used_mib: AtomicU32,
14 pub total_mib: AtomicU32,
16 pub name: std::sync::Mutex<String>,
18}
19
20impl GpuState {
21 pub fn new() -> Self {
22 Self {
23 used_mib: AtomicU32::new(0),
24 total_mib: AtomicU32::new(0),
25 name: std::sync::Mutex::new("GPU".into()),
26 }
27 }
28
29 pub fn read(&self) -> (u32, u32) {
31 (
32 self.used_mib.load(Ordering::Relaxed),
33 self.total_mib.load(Ordering::Relaxed),
34 )
35 }
36
37 pub fn ratio(&self) -> f64 {
39 let (used, total) = self.read();
40 if total == 0 {
41 return 0.0;
42 }
43 (used as f64 / total as f64).clamp(0.0, 1.0)
44 }
45
46 pub fn label(&self) -> String {
48 let (used, total) = self.read();
49 if total == 0 {
50 return "N/A".into();
51 }
52 format!(
53 "{:.1} GB / {:.1} GB",
54 used as f64 / 1024.0,
55 total as f64 / 1024.0
56 )
57 }
58
59 pub fn gpu_name(&self) -> String {
61 self.name.lock().unwrap().clone()
62 }
63}
64
65pub fn spawn_gpu_monitor() -> Arc<GpuState> {
67 let state = Arc::new(GpuState::new());
68 let bg = state.clone();
69
70 tokio::spawn(async move {
71 loop {
72 if let Some((used, total, name)) = poll_nvidia_smi().await {
73 bg.used_mib.store(used, Ordering::Relaxed);
74 bg.total_mib.store(total, Ordering::Relaxed);
75 if !name.is_empty() {
76 *bg.name.lock().unwrap() = name;
77 }
78 }
79 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
80 }
81 });
82
83 state
84}
85
86async fn poll_nvidia_smi() -> Option<(u32, u32, String)> {
88 let output = tokio::process::Command::new("nvidia-smi")
89 .args([
90 "--query-gpu=memory.used,memory.total,name",
91 "--format=csv,noheader,nounits",
92 ])
93 .output()
94 .await
95 .ok()?;
96
97 if !output.status.success() {
98 return None;
99 }
100
101 let stdout = String::from_utf8_lossy(&output.stdout);
102 let line = stdout.trim();
103 let parts: Vec<&str> = line.splitn(3, ',').collect();
104 if parts.len() < 2 {
105 return None;
106 }
107
108 let used: u32 = parts[0].trim().parse().ok()?;
109 let total: u32 = parts[1].trim().parse().ok()?;
110 let name = parts
111 .get(2)
112 .map(|s| s.trim().to_string())
113 .unwrap_or_default();
114
115 Some((used, total, name))
116}