use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy)]
pub struct HostSample {
pub busy_jiffies: u64,
pub mem_available_kb: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct CgroupSample {
pub cpu_usage_us: u64,
pub mem_current: u64,
pub mem_peak: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoadRow {
pub at: String,
pub task: String,
pub wall_ms: u64,
pub machine_cores: Option<f64>,
pub machine_mem_delta_mb: Option<i64>,
pub endpoint_cores: Option<f64>,
pub endpoint_mem_bytes: Option<u64>,
pub endpoint_mem_peak_bytes: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct LoadSummary {
pub calls: u64,
pub wall_ms: u64,
pub machine_cores_avg: Option<f64>,
pub endpoint_cores_avg: Option<f64>,
pub last: Option<LoadRow>,
pub calls_without_attribution: u64,
pub cores_total: Option<usize>,
}
pub fn read_host() -> Option<HostSample> {
let stat = std::fs::read_to_string("/proc/stat").ok()?;
let line = stat.lines().find(|l| l.starts_with("cpu "))?;
let v: Vec<u64> = line
.split_whitespace()
.skip(1)
.filter_map(|f| f.parse().ok())
.collect();
if v.len() < 5 {
return None;
}
let busy: u64 = v.iter().sum::<u64>().saturating_sub(v[3] + v[4]);
let mem = std::fs::read_to_string("/proc/meminfo").ok()?;
let mem_available_kb = mem
.lines()
.find(|l| l.starts_with("MemAvailable:"))?
.split_whitespace()
.nth(1)?
.parse()
.ok()?;
Some(HostSample {
busy_jiffies: busy,
mem_available_kb,
})
}
pub fn read_cgroup(dir: &Path) -> Option<CgroupSample> {
let stat = std::fs::read_to_string(dir.join("cpu.stat")).ok()?;
let cpu_usage_us = stat
.lines()
.find(|l| l.starts_with("usage_usec"))?
.split_whitespace()
.nth(1)?
.parse()
.ok()?;
let num = |f: &str| -> Option<u64> {
std::fs::read_to_string(dir.join(f))
.ok()?
.trim()
.parse()
.ok()
};
Some(CgroupSample {
cpu_usage_us,
mem_current: num("memory.current")?,
mem_peak: num("memory.peak"),
})
}
const USER_HZ: f64 = 100.0;
pub fn row(
task: &str,
wall: std::time::Duration,
host: (Option<HostSample>, Option<HostSample>),
cg: (Option<CgroupSample>, Option<CgroupSample>),
) -> LoadRow {
let wall_s = wall.as_secs_f64().max(0.001);
let (machine_cores, machine_mem_delta_mb) = match (host.0, host.1) {
(Some(a), Some(b)) => (
Some((b.busy_jiffies.saturating_sub(a.busy_jiffies) as f64) / USER_HZ / wall_s),
Some((a.mem_available_kb as i64 - b.mem_available_kb as i64) / 1024),
),
_ => (None, None),
};
let (endpoint_cores, endpoint_mem_bytes, endpoint_mem_peak_bytes) = match (cg.0, cg.1) {
(Some(a), Some(b)) => (
Some((b.cpu_usage_us.saturating_sub(a.cpu_usage_us) as f64) / 1_000_000.0 / wall_s),
Some(b.mem_current),
b.mem_peak,
),
_ => (None, None, None),
};
LoadRow {
at: crate::usage::now(),
task: task.to_string(),
wall_ms: wall.as_millis() as u64,
machine_cores,
machine_mem_delta_mb,
endpoint_cores,
endpoint_mem_bytes,
endpoint_mem_peak_bytes,
}
}
pub struct LoadLog {
path: PathBuf,
}
impl LoadLog {
pub fn new(root: &Path) -> Self {
Self {
path: root.join("load.jsonl"),
}
}
pub fn append(&self, row: &LoadRow) {
use std::io::Write;
let Ok(line) = serde_json::to_string(row) else {
return;
};
let w = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
.and_then(|mut f| writeln!(f, "{line}"));
if let Err(e) = w {
eprintln!("cyberbrain: load log append failed: {e}");
}
}
pub fn last_of(&self, tasks: &[&str]) -> Option<LoadRow> {
let text = std::fs::read_to_string(&self.path).ok()?;
text.lines()
.rev()
.filter_map(|l| serde_json::from_str::<LoadRow>(l).ok())
.find(|r| tasks.contains(&r.task.as_str()))
}
pub fn summary(&self) -> LoadSummary {
let mut s = LoadSummary {
cores_total: std::thread::available_parallelism().ok().map(|n| n.get()),
..LoadSummary::default()
};
let Ok(text) = std::fs::read_to_string(&self.path) else {
return s;
};
let (mut mach, mut mach_n) = (0.0f64, 0u64);
let (mut ep, mut ep_n) = (0.0f64, 0u64);
for line in text.lines().filter(|l| !l.trim().is_empty()) {
let Ok(r) = serde_json::from_str::<LoadRow>(line) else {
continue;
};
s.calls += 1;
s.wall_ms += r.wall_ms;
if let Some(c) = r.machine_cores {
mach += c;
mach_n += 1;
}
match r.endpoint_cores {
Some(c) => {
ep += c;
ep_n += 1;
}
None => s.calls_without_attribution += 1,
}
s.last = Some(r);
}
if mach_n > 0 {
s.machine_cores_avg = Some(mach / mach_n as f64);
}
if ep_n > 0 {
s.endpoint_cores_avg = Some(ep / ep_n as f64);
}
s
}
}