refactor: add SystemStats singleton + nova-stats daemon for cpu/mem polling

This commit is contained in:
Damocles 2026-04-15 02:10:45 +02:00
parent 71a843e0f3
commit 136ff53cb5
13 changed files with 371 additions and 196 deletions

7
stats-daemon/Cargo.lock generated Normal file
View file

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "nova-stats"
version = "0.1.0"

8
stats-daemon/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "nova-stats"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "nova-stats"
path = "src/main.rs"

172
stats-daemon/src/main.rs Normal file
View file

@ -0,0 +1,172 @@
use std::fs;
use std::io::{self, Write};
use std::thread;
use std::time::{Duration, Instant};
struct Sample {
idle: u64,
total: u64,
}
fn read_stat() -> Vec<Sample> {
fs::read_to_string("/proc/stat")
.unwrap_or_default()
.lines()
.filter(|l| l.starts_with("cpu"))
.map(|l| {
let vals: Vec<u64> = l
.split_whitespace()
.skip(1)
.filter_map(|s| s.parse().ok())
.collect();
let idle = vals.get(3).copied().unwrap_or(0) + vals.get(4).copied().unwrap_or(0);
let total = vals.iter().sum();
Sample { idle, total }
})
.collect()
}
fn pct(prev: &Sample, curr: &Sample) -> u32 {
let dt = curr.total.saturating_sub(prev.total);
let di = curr.idle.saturating_sub(prev.idle);
if dt == 0 {
return 0;
}
(dt.saturating_sub(di) * 100 / dt) as u32
}
fn read_core_freqs() -> Vec<f64> {
let mut freqs = Vec::new();
for i in 0.. {
let path = format!("/sys/devices/system/cpu/cpu{i}/cpufreq/scaling_cur_freq");
match fs::read_to_string(&path) {
Ok(s) => match s.trim().parse::<u64>() {
Ok(khz) => freqs.push(khz as f64 / 1_000_000.0),
Err(_) => break,
},
Err(_) => break,
}
}
freqs
}
fn emit_cpu(out: &mut impl Write, prev: &[Sample], curr: &[Sample], freqs: &[f64]) {
if curr.is_empty() {
return;
}
let usage = prev
.first()
.zip(curr.first())
.map(|(p, c)| pct(p, c))
.unwrap_or(0);
let core_usage: Vec<u32> = prev
.iter()
.skip(1)
.zip(curr.iter().skip(1))
.map(|(p, c)| pct(p, c))
.collect();
let avg_freq = if freqs.is_empty() {
0.0
} else {
freqs.iter().sum::<f64>() / freqs.len() as f64
};
let _ = write!(
out,
"{{\"type\":\"cpu\",\"usage\":{usage},\"freq_ghz\":{avg_freq:.3},\"core_usage\":["
);
for (i, u) in core_usage.iter().enumerate() {
if i > 0 {
let _ = write!(out, ",");
}
let _ = write!(out, "{u}");
}
let _ = write!(out, "],\"core_freq_ghz\":[");
for (i, f) in freqs.iter().enumerate() {
if i > 0 {
let _ = write!(out, ",");
}
let _ = write!(out, "{f:.3}");
}
let _ = writeln!(out, "]}}");
}
fn emit_mem(out: &mut impl Write) {
let content = fs::read_to_string("/proc/meminfo").unwrap_or_default();
let mut total = 0u64;
let mut avail = 0u64;
let mut buffers = 0u64;
let mut cached = 0u64;
let mut sreclaimable = 0u64;
for line in content.lines() {
let mut parts = line.splitn(2, ':');
let key = parts.next().unwrap_or("").trim();
let val: u64 = parts
.next()
.unwrap_or("")
.split_whitespace()
.next()
.unwrap_or("")
.parse()
.unwrap_or(0);
match key {
"MemTotal" => total = val,
"MemAvailable" => avail = val,
"Buffers" => buffers = val,
"Cached" => cached = val,
"SReclaimable" => sreclaimable = val,
_ => {}
}
}
if total == 0 {
return;
}
let used = total.saturating_sub(avail);
let cached_total = cached + sreclaimable;
let percent = used * 100 / total;
let gb = |kb: u64| kb as f64 / 1_048_576.0;
let _ = writeln!(
out,
"{{\"type\":\"mem\",\"percent\":{percent},\"used_gb\":{:.3},\"total_gb\":{:.3},\"avail_gb\":{:.3},\"cached_gb\":{:.3},\"buffers_gb\":{:.3}}}",
gb(used),
gb(total),
gb(avail),
gb(cached_total),
gb(buffers),
);
}
fn main() {
let stdout = io::stdout();
let mut out = io::BufWriter::new(stdout.lock());
let mut prev: Vec<Sample> = vec![];
let mut tick = 0u64;
loop {
let t0 = Instant::now();
let curr = read_stat();
let freqs = read_core_freqs();
emit_cpu(&mut out, &prev, &curr, &freqs);
prev = curr;
if tick % 2 == 0 {
emit_mem(&mut out);
}
let _ = out.flush();
tick += 1;
let elapsed = t0.elapsed();
if elapsed < Duration::from_secs(1) {
thread::sleep(Duration::from_secs(1) - elapsed);
}
}
}