use crate::model::MetricState;
use crate::units::Percent;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CpuUsage {
pub busy: Percent,
pub breakdown: MetricState<CpuBreakdown>,
}
impl CpuUsage {
#[must_use]
pub const fn plain(busy: Percent) -> Self {
Self {
busy,
breakdown: MetricState::Unsupported,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CpuBreakdown {
pub user: Percent,
pub system: Percent,
pub nice: Percent,
pub idle: Percent,
pub iowait: MetricState<Percent>,
pub irq: MetricState<Percent>,
pub softirq: MetricState<Percent>,
pub steal: MetricState<Percent>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum CpuNormalization {
#[default]
Core,
Machine,
}
impl CpuNormalization {
#[must_use]
pub const fn description(self) -> &'static str {
match self {
Self::Core => "one core = 100%, so a multi-threaded process may exceed 100%",
Self::Machine => "the whole machine = 100%, so no process exceeds 100%",
}
}
#[must_use]
pub fn apply(self, core_normalized: Percent, logical_cpus: u16) -> Option<Percent> {
match self {
Self::Core => Some(core_normalized),
Self::Machine => {
if logical_cpus == 0 {
return None;
}
Percent::new(core_normalized.value() / f32::from(logical_cpus))
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CpuSnapshot {
pub logical_count: u16,
pub physical_count: MetricState<u16>,
pub total: MetricState<CpuUsage>,
pub per_core: MetricState<Vec<CpuUsage>>,
pub frequency_mhz: MetricState<u64>,
}
impl CpuSnapshot {
#[must_use]
pub const fn warming_up(logical_count: u16) -> Self {
Self {
logical_count,
physical_count: MetricState::WarmingUp,
total: MetricState::WarmingUp,
per_core: MetricState::WarmingUp,
frequency_mhz: MetricState::WarmingUp,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct LoadSnapshot {
pub one: f32,
pub five: f32,
pub fifteen: f32,
}
impl LoadSnapshot {
#[must_use]
pub fn per_cpu(&self, logical_cpus: u16) -> Option<f32> {
if logical_cpus == 0 {
return None;
}
let value = self.one / f32::from(logical_cpus);
value.is_finite().then_some(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn core_normalization_is_the_identity() {
let cpu = Percent::new(287.0).expect("valid");
let out = CpuNormalization::Core.apply(cpu, 8).expect("valid");
assert!((out.value() - 287.0).abs() < f32::EPSILON);
}
#[test]
fn machine_normalization_divides_by_the_logical_cpu_count() {
let cpu = Percent::new(400.0).expect("valid");
let out = CpuNormalization::Machine.apply(cpu, 8).expect("valid");
assert!((out.value() - 50.0).abs() < f32::EPSILON);
}
#[test]
fn machine_normalization_is_undefined_without_a_cpu_count() {
let cpu = Percent::new(400.0).expect("valid");
assert!(CpuNormalization::Machine.apply(cpu, 0).is_none());
}
#[test]
fn the_default_convention_is_documented_as_per_core() {
assert_eq!(CpuNormalization::default(), CpuNormalization::Core);
assert!(
CpuNormalization::default()
.description()
.contains("exceed 100%")
);
}
#[test]
fn load_per_cpu_is_undefined_without_a_cpu_count() {
let load = LoadSnapshot {
one: 11.4,
five: 8.0,
fifteen: 4.0,
};
assert!(load.per_cpu(0).is_none());
let per_cpu = load.per_cpu(8).expect("valid");
assert!((per_cpu - 1.425).abs() < 0.001);
}
#[test]
fn a_warming_up_cpu_snapshot_reports_no_utilization() {
let cpu = CpuSnapshot::warming_up(8);
assert_eq!(cpu.logical_count, 8);
assert!(cpu.total.fresh().is_none());
assert!(cpu.total.is_warming_up());
}
}