use crate::collector::error::{CollectError, CollectErrorKind};
use crate::collector::linux::source::ParsedProcStat;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CpuCounters {
pub user: u64,
pub nice: u64,
pub system: u64,
pub idle: u64,
pub iowait: u64,
pub irq: u64,
pub softirq: u64,
pub steal: u64,
}
impl CpuCounters {
pub fn total(&self) -> u64 {
self.user
.saturating_add(self.nice)
.saturating_add(self.system)
.saturating_add(self.idle)
.saturating_add(self.iowait)
.saturating_add(self.irq)
.saturating_add(self.softirq)
.saturating_add(self.steal)
}
pub fn busy(&self) -> u64 {
self.user
.saturating_add(self.nice)
.saturating_add(self.system)
.saturating_add(self.irq)
.saturating_add(self.softirq)
.saturating_add(self.steal)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CpuSample {
pub usage_pct: f32,
pub iowait_pct: f32,
}
pub fn parse_proc_stat(raw: &str) -> Result<ParsedProcStat, CollectError> {
let mut aggregate = None;
for line in raw.lines() {
if let Some(rest) = line.strip_prefix("cpu ") {
aggregate = Some(parse_cpu_row("cpu", rest)?);
break;
}
}
Ok(ParsedProcStat { aggregate })
}
fn parse_cpu_row(label: &str, rest: &str) -> Result<CpuCounters, CollectError> {
let fields: Vec<&str> = rest.split_whitespace().collect();
let expected_min = 7;
if fields.len() < expected_min {
return Err(CollectError::new(
CollectErrorKind::Parse,
format!(
"expected at least {expected_min} fields on `{label}` row, found {}",
fields.len()
),
));
}
let mut values = [0u64; 10];
for (idx, field) in fields.iter().take(10).enumerate() {
values[idx] = field.parse::<u64>().map_err(|e| {
CollectError::new(
CollectErrorKind::Parse,
format!("non-numeric field on `{label}` row at index {idx}"),
)
.with_source(e)
})?;
}
Ok(CpuCounters {
user: values[0],
nice: values[1],
system: values[2],
idle: values[3],
iowait: values[4],
irq: values[5],
softirq: values[6],
steal: values[7],
})
}
pub fn compute_percentages(
prev: &CpuCounters,
curr: &CpuCounters,
) -> Result<CpuSample, CollectError> {
let delta = |before: u64, after: u64| -> Result<u64, CollectError> {
if after < before {
return Err(CollectError::counter_reset(
"CPU counter decreased between samples; baseline discarded",
));
}
let diff = after - before;
Ok(diff)
};
let busy_curr = curr.busy();
let busy_prev = prev.busy();
let total_curr = curr.total();
let total_prev = prev.total();
let delta_busy = delta(busy_prev, busy_curr)?;
let delta_iowait = delta(prev.iowait, curr.iowait)?;
let delta_total = delta(total_prev, total_curr)?;
if delta_total == 0 {
return Err(CollectError::counter_reset(
"CPU total delta is zero; baseline discarded to avoid division by zero",
));
}
#[allow(clippy::cast_precision_loss)]
let usage_pct = (delta_busy as f64) * 100.0 / (delta_total as f64);
#[allow(clippy::cast_precision_loss)]
let iowait_pct = (delta_iowait as f64) * 100.0 / (delta_total as f64);
let finalize = |value: f64| -> Result<f32, CollectError> {
if !value.is_finite() {
return Err(CollectError::new(
CollectErrorKind::Numeric,
"CPU percentage is not finite",
));
}
let clamped = value.clamp(0.0, 100.0);
#[allow(clippy::cast_possible_truncation)]
let as_f32 = clamped as f32;
if !as_f32.is_finite() || !(0.0..=100.0).contains(&as_f32) {
return Err(CollectError::new(
CollectErrorKind::Numeric,
"CPU percentage outside closed 0..=100 interval after conversion",
));
}
Ok(as_f32)
};
Ok(CpuSample {
usage_pct: finalize(usage_pct)?,
iowait_pct: finalize(iowait_pct)?,
})
}