refactor: add SystemStats singleton + nova-stats daemon for cpu/mem polling
This commit is contained in:
parent
71a843e0f3
commit
136ff53cb5
13 changed files with 371 additions and 196 deletions
7
stats-daemon/Cargo.lock
generated
Normal file
7
stats-daemon/Cargo.lock
generated
Normal 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
8
stats-daemon/Cargo.toml
Normal 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
172
stats-daemon/src/main.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue