use crate::prelude::*;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Sample {
pub cpu: Option<f32>,
pub memory: Option<Fraction>,
pub vram: Option<Fraction>,
pub gpu: Option<f32>,
pub disk: Option<Fraction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gpu_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cpu_count: Option<u32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Fraction {
pub used: u64,
pub total: u64,
}
impl Fraction {
pub fn ratio(&self) -> f32 {
if self.total == 0 {
return 0.0;
}
(self.used as f64 / self.total as f64) as f32
}
pub fn free(&self) -> u64 {
self.total.saturating_sub(self.used)
}
}
#[derive(Debug, Default)]
pub struct Sampler {
previous_cpu: Option<CpuTimes>,
last_cpu: Option<f32>,
}
#[derive(Debug, Clone, Copy)]
struct CpuTimes {
busy: u64,
total: u64,
}
impl Sampler {
pub fn new() -> Self {
Self::default()
}
pub fn sample(&mut self, workspace_root: &Path) -> Sample {
let cpu_now = cpu_times();
let cpu = match (self.previous_cpu, cpu_now) {
(Some(before), Some(now)) => {
let busy = now.busy.saturating_sub(before.busy) as f64;
let total = now.total.saturating_sub(before.total) as f64;
if total > 0.0 {
let fraction = (busy / total) as f32;
self.last_cpu = Some(fraction);
self.previous_cpu = Some(now);
Some(fraction)
} else {
self.last_cpu
}
}
(None, Some(now)) => {
self.previous_cpu = Some(now);
None
}
_ => self.last_cpu,
};
let graphics = graphics();
Sample {
cpu,
memory: memory(),
vram: graphics.as_ref().and_then(|g| g.vram),
gpu: graphics.as_ref().and_then(|g| g.load),
gpu_name: graphics.and_then(|g| g.name),
disk: disk(workspace_root),
cpu_count: cpu_count(),
}
}
}
fn cpu_count() -> Option<u32> {
std::thread::available_parallelism()
.ok()
.map(|n| n.get() as u32)
}
#[cfg(target_os = "linux")]
fn cpu_times() -> Option<CpuTimes> {
let stat = std::fs::read_to_string("/proc/stat").ok()?;
let line = stat.lines().find(|line| line.starts_with("cpu "))?;
let fields: Vec<u64> = line
.split_whitespace()
.skip(1)
.filter_map(|f| f.parse().ok())
.collect();
if fields.len() < 4 {
return None;
}
let total: u64 = fields.iter().sum();
let idle = fields[3] + fields.get(4).copied().unwrap_or(0);
Some(CpuTimes {
busy: total.saturating_sub(idle),
total,
})
}
#[cfg(target_os = "macos")]
extern "C" {
fn mach_port_deallocate(
task: libc::mach_port_t,
name: libc::mach_port_t,
) -> libc::kern_return_t;
}
#[cfg(target_os = "macos")]
fn cpu_times() -> Option<CpuTimes> {
unsafe {
let host = libc::mach_host_self();
let mut info: libc::host_cpu_load_info = std::mem::zeroed();
let mut count = libc::HOST_CPU_LOAD_INFO_COUNT;
let result = libc::host_statistics(
host,
libc::HOST_CPU_LOAD_INFO,
&mut info as *mut _ as libc::host_info_t,
&mut count,
);
let _ = mach_port_deallocate(libc::mach_task_self(), host);
if result != libc::KERN_SUCCESS {
return None;
}
let ticks = info.cpu_ticks;
let total: u64 = ticks.iter().map(|t| *t as u64).sum();
let idle = ticks[libc::CPU_STATE_IDLE as usize] as u64;
Some(CpuTimes {
busy: total.saturating_sub(idle),
total,
})
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn cpu_times() -> Option<CpuTimes> {
None
}
#[cfg(target_os = "linux")]
fn memory() -> Option<Fraction> {
let info = std::fs::read_to_string("/proc/meminfo").ok()?;
let read = |key: &str| -> Option<u64> {
info.lines()
.find(|line| line.starts_with(key))?
.split_whitespace()
.nth(1)?
.parse::<u64>()
.ok()
.map(|kb| kb * 1024)
};
let total = read("MemTotal:")?;
let available = read("MemAvailable:").or_else(|| read("MemFree:"))?;
Some(Fraction {
used: total.saturating_sub(available),
total,
})
}
#[cfg(not(target_os = "linux"))]
fn memory() -> Option<Fraction> {
let total: u64 = sysctl("hw.memsize")?;
let output = crate::git_backend::command_without_repo_scope("vm_stat")
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
let page_size: u64 = text
.lines()
.next()
.and_then(|line| line.split("page size of ").nth(1))
.and_then(|rest| rest.split_whitespace().next())
.and_then(|n| n.parse().ok())
.unwrap_or(4096);
let pages = |key: &str| -> u64 {
text.lines()
.find(|line| line.starts_with(key))
.and_then(|line| line.split(':').nth(1))
.map(|v| v.trim().trim_end_matches('.'))
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(0)
};
let free = (pages("Pages free") + pages("Pages inactive") + pages("Pages purgeable")) * page_size;
Some(Fraction {
used: total.saturating_sub(free),
total,
})
}
#[cfg(not(target_os = "linux"))]
fn sysctl(name: &str) -> Option<u64> {
let output = crate::git_backend::command_without_repo_scope("sysctl")
.args(["-n", name])
.output()
.ok()?;
String::from_utf8_lossy(&output.stdout).trim().parse().ok()
}
struct Graphics {
name: Option<String>,
load: Option<f32>,
vram: Option<Fraction>,
}
fn graphics() -> Option<Graphics> {
#[cfg(target_os = "macos")]
if let Some(found) = apple_graphics() {
return Some(found);
}
nvidia_graphics()
}
#[cfg(target_os = "macos")]
fn apple_graphics() -> Option<Graphics> {
let output = crate::git_backend::command_without_repo_scope("ioreg")
.args(["-r", "-d", "1", "-w", "0", "-c", "IOAccelerator"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8_lossy(&output.stdout);
let after = text.split("\"Device Utilization %\"=").nth(1)?;
let digits: String = after.chars().take_while(char::is_ascii_digit).collect();
let percent: f32 = digits.parse().ok()?;
Some(Graphics {
name: text
.split("\"model\" = <\"")
.nth(1)
.and_then(|rest| rest.split('"').next())
.map(str::to_string),
load: Some((percent / 100.0).clamp(0.0, 1.0)),
vram: None,
})
}
fn nvidia_graphics() -> Option<Graphics> {
let output = crate::git_backend::command_without_repo_scope("nvidia-smi")
.args([
"--query-gpu=name,utilization.gpu,memory.used,memory.total",
"--format=csv,noheader,nounits",
])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8_lossy(&output.stdout);
let line = text.lines().next()?;
let fields: Vec<&str> = line.split(',').map(str::trim).collect();
if fields.len() < 4 {
return None;
}
let mib = |value: &str| value.parse::<u64>().ok().map(|mb| mb * 1024 * 1024);
Some(Graphics {
name: Some(fields[0].to_string()),
load: fields[1].parse::<f32>().ok().map(|pct| pct / 100.0),
vram: match (mib(fields[2]), mib(fields[3])) {
(Some(used), Some(total)) => Some(Fraction { used, total }),
_ => None,
},
})
}
fn disk(workspace_root: &Path) -> Option<Fraction> {
let space = crate::capped::volume_space(workspace_root).ok()?;
Some(Fraction {
used: space.total.saturating_sub(space.available),
total: space.total,
})
}
#[cfg(test)]
#[path = "host_tests.rs"]
mod tests;