use std::path::Path;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::Instant;
const MEM_SMOOTH_SECS: f64 = 180.0;
pub struct Health {
start: Instant,
interval_secs: u64,
last_sample: Option<Instant>,
mem_ema: Option<f64>,
load1: Option<f64>,
disk_free_pct: Option<u8>,
gpu_pct: Option<u8>,
gpu_inflight: bool,
gpu_tx: Sender<Option<u8>>,
gpu_rx: Receiver<Option<u8>>,
}
impl Health {
pub fn new(interval_secs: u64) -> Self {
let (gpu_tx, gpu_rx) = channel();
Health {
start: Instant::now(),
interval_secs: interval_secs.max(1),
last_sample: None,
mem_ema: None,
load1: None,
disk_free_pct: None,
gpu_pct: None,
gpu_inflight: false,
gpu_tx,
gpu_rx,
}
}
fn alpha(&self) -> f64 {
let i = self.interval_secs as f64;
(i / (MEM_SMOOTH_SECS + i)).clamp(0.0, 1.0)
}
pub fn maybe_sample(&mut self, cwd: &Path, gpu: bool) -> bool {
let mut changed = false;
while let Ok(v) = self.gpu_rx.try_recv() {
if self.gpu_pct != v { changed = true; }
self.gpu_pct = v;
self.gpu_inflight = false;
}
let now = Instant::now();
let due = self
.last_sample
.map(|t| now.duration_since(t).as_secs() >= self.interval_secs)
.unwrap_or(true);
if !due {
return changed;
}
self.last_sample = Some(now);
self.load1 = crate::sys::health::load_avg1();
self.disk_free_pct = crate::sys::health::disk_free_pct(cwd);
if let Some(m) = crate::sys::health::mem_used_pct() {
let s = m as f64;
self.mem_ema = Some(match self.mem_ema {
Some(e) => e + self.alpha() * (s - e),
None => s,
});
}
if gpu && !self.gpu_inflight {
self.gpu_inflight = true;
let tx = self.gpu_tx.clone();
std::thread::spawn(move || {
let _ = tx.send(query_gpu());
});
}
true
}
pub fn line(&self) -> String {
let mut parts = vec![format!("up {}", fmt_uptime(self.start.elapsed().as_secs()))];
if let Some(l) = self.load1 {
parts.push(format!("load {l:.2}"));
}
if let Some(m) = self.mem_ema {
parts.push(format!("mem {}%", m.round() as u8));
}
if let Some(d) = self.disk_free_pct {
parts.push(format!("disk {d}% free"));
}
if let Some(g) = self.gpu_pct {
parts.push(format!("gpu {g}%"));
}
parts.join(" · ")
}
}
fn fmt_uptime(secs: u64) -> String {
let (h, m, s) = (secs / 3600, (secs % 3600) / 60, secs % 60);
if h > 0 {
format!("{h}h {m:02}m")
} else if m > 0 {
format!("{m}m")
} else {
format!("{s}s")
}
}
fn query_gpu() -> Option<u8> {
let out = std::process::Command::new("nvidia-smi")
.args(["--query-gpu=memory.used,memory.total", "--format=csv,noheader,nounits"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
let (mut used, mut total) = (0f64, 0f64);
for line in text.lines().filter(|l| !l.trim().is_empty()) {
let mut it = line.split(',');
let u: f64 = it.next()?.trim().parse().ok()?;
let t: f64 = it.next()?.trim().parse().ok()?;
used += u;
total += t;
}
if total <= 0.0 {
return None;
}
Some(((used * 100.0 / total).round() as u8).min(100))
}