use std::fs;
use std::time::Duration;
use log::warn;
use tokio::time::interval;
use crate::widget::{Msg, SystemStats};
use crate::producer::{MsgSender, Producer, ProducerFuture, ProducerResult};
const DEFAULT_INTERVAL: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CpuTimes {
idle: u64,
total: u64,
}
pub fn parse_cpu_times(proc_stat: &str) -> Option<CpuTimes> {
let line = proc_stat
.lines()
.find(|l| l.split_whitespace().next() == Some("cpu"))?;
let nums: Vec<u64> = line
.split_whitespace()
.skip(1)
.map(|t| t.parse::<u64>())
.collect::<Result<_, _>>()
.ok()?;
if nums.len() < 4 {
return None;
}
let total: u64 = nums.iter().sum();
let idle = nums[3] + nums.get(4).copied().unwrap_or(0);
Some(CpuTimes { idle, total })
}
pub fn cpu_usage(prev: CpuTimes, cur: CpuTimes) -> f64 {
let total = cur.total.saturating_sub(prev.total);
if total == 0 {
return 0.0;
}
let idle = cur.idle.saturating_sub(prev.idle);
let busy = total.saturating_sub(idle);
busy as f64 / total as f64 * 100.0
}
pub fn parse_mem_usage(proc_meminfo: &str) -> Option<f64> {
let mut total = None;
let mut available = None;
for line in proc_meminfo.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
total = parse_kb(rest);
} else if let Some(rest) = line.strip_prefix("MemAvailable:") {
available = parse_kb(rest);
}
}
let total = total?;
let available = available?;
if total == 0 {
return None;
}
let used = total.saturating_sub(available);
Some(used as f64 / total as f64 * 100.0)
}
fn parse_kb(field: &str) -> Option<u64> {
field.split_whitespace().next()?.parse().ok()
}
fn read_cpu_times() -> Option<CpuTimes> {
match fs::read_to_string("/proc/stat") {
Ok(stat) => parse_cpu_times(&stat),
Err(e) => {
warn!("sysmon: reading /proc/stat failed: {e}");
None
}
}
}
fn read_mem_usage() -> Option<f64> {
match fs::read_to_string("/proc/meminfo") {
Ok(meminfo) => parse_mem_usage(&meminfo),
Err(e) => {
warn!("sysmon: reading /proc/meminfo failed: {e}");
None
}
}
}
pub struct SystemProducer {
interval: Duration,
}
impl SystemProducer {
pub fn new() -> Self {
Self {
interval: DEFAULT_INTERVAL,
}
}
pub fn with_interval(interval: Duration) -> Self {
Self { interval }
}
}
impl Default for SystemProducer {
fn default() -> Self {
Self::new()
}
}
impl Producer for SystemProducer {
fn name(&self) -> String {
"system-stats".to_string()
}
fn run(self: Box<Self>, tx: MsgSender) -> ProducerFuture {
Box::pin(run(tx, self.interval))
}
}
async fn run(tx: MsgSender, period: Duration) -> ProducerResult {
let mut ticker = interval(period);
ticker.tick().await;
let mut prev = read_cpu_times();
loop {
ticker.tick().await;
let cur = read_cpu_times();
let cpu = match (prev, cur) {
(Some(p), Some(c)) => cpu_usage(p, c),
_ => 0.0,
};
if cur.is_some() {
prev = cur;
}
let mem = read_mem_usage().unwrap_or(0.0);
if tx.send(Msg::System(SystemStats::new(cpu, mem))).is_err() {
return Ok(());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const PROC_STAT: &str = "cpu 100 0 50 800 30 0 20 0 0 0
cpu0 50 0 25 400 15 0 10 0 0 0
intr 12345 0 0
ctxt 67890
";
const PROC_MEMINFO: &str = "MemTotal: 16000000 kB
MemFree: 2000000 kB
MemAvailable: 8000000 kB
Buffers: 500000 kB
Cached: 3000000 kB
";
#[test]
fn parse_cpu_times_reads_the_aggregate_line() {
let times = parse_cpu_times(PROC_STAT).expect("aggregate cpu line");
assert_eq!(times.total, 1000);
assert_eq!(times.idle, 830);
}
#[test]
fn parse_cpu_times_rejects_input_without_an_aggregate_line() {
assert_eq!(parse_cpu_times("cpu0 1 2 3 4\nintr 5\n"), None);
assert_eq!(parse_cpu_times("not proc stat"), None);
}
#[test]
fn parse_cpu_times_rejects_a_truncated_aggregate_line() {
assert_eq!(parse_cpu_times("cpu 1 2 3\n"), None);
}
#[test]
fn cpu_usage_is_the_busy_fraction_between_samples() {
let prev = CpuTimes {
idle: 80,
total: 100,
};
let cur = CpuTimes {
idle: 130,
total: 200,
};
assert!((cpu_usage(prev, cur) - 50.0).abs() < 1e-6);
}
#[test]
fn cpu_usage_handles_no_elapsed_time() {
let same = CpuTimes {
idle: 80,
total: 100,
};
assert_eq!(cpu_usage(same, same), 0.0);
}
#[test]
fn cpu_usage_handles_a_counter_reset() {
let prev = CpuTimes {
idle: 130,
total: 200,
};
let cur = CpuTimes {
idle: 80,
total: 100,
};
assert_eq!(cpu_usage(prev, cur), 0.0);
}
#[test]
fn parse_mem_usage_is_used_over_total() {
let mem = parse_mem_usage(PROC_MEMINFO).expect("mem fields present");
assert!((mem - 50.0).abs() < 1e-6);
}
#[test]
fn parse_mem_usage_requires_both_fields() {
assert_eq!(parse_mem_usage("MemTotal: 16000000 kB\n"), None);
assert_eq!(parse_mem_usage("MemAvailable: 8000000 kB\n"), None);
}
#[test]
fn parse_mem_usage_rejects_zero_total() {
assert_eq!(
parse_mem_usage("MemTotal: 0 kB\nMemAvailable: 0 kB\n"),
None
);
}
}