use std::time::Duration;
#[derive(Clone, Debug, PartialEq)]
pub struct ProcessSummary {
pid: u32,
parent_pid: Option<u32>,
name: String,
running: bool,
cpu_usage_percent: f32,
memory_bytes: u64,
}
impl ProcessSummary {
pub fn pid(&self) -> u32 {
self.pid
}
pub fn parent_pid(&self) -> Option<u32> {
self.parent_pid
}
pub fn name(&self) -> &str {
&self.name
}
pub fn running(&self) -> bool {
self.running
}
pub fn cpu_usage_percent(&self) -> f32 {
self.cpu_usage_percent
}
pub fn memory_bytes(&self) -> u64 {
self.memory_bytes
}
}
pub fn snapshot() -> Vec<ProcessSummary> {
let system = sysinfo::System::new_all();
system
.processes()
.iter()
.map(|(pid, process)| ProcessSummary {
pid: pid.as_u32(),
parent_pid: process.parent().map(sysinfo::Pid::as_u32),
name: process.name().to_string(),
running: matches!(
process.status(),
sysinfo::ProcessStatus::Run
| sysinfo::ProcessStatus::Sleep
| sysinfo::ProcessStatus::Idle
),
cpu_usage_percent: process.cpu_usage(),
memory_bytes: process.memory(),
})
.collect()
}
#[derive(Debug)]
pub struct CpuSampler {
system: sysinfo::System,
}
impl CpuSampler {
pub fn new() -> Self {
let mut system = sysinfo::System::new();
system.refresh_cpu_usage();
Self { system }
}
pub fn minimum_interval() -> Duration {
sysinfo::MINIMUM_CPU_UPDATE_INTERVAL
}
pub fn sample(&mut self) -> f32 {
self.system.refresh_cpu_usage();
let cpus = self.system.cpus();
if cpus.is_empty() {
return 0.0;
}
let mean = cpus.iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / cpus.len() as f32;
mean.clamp(0.0, 100.0)
}
}
impl Default for CpuSampler {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enumeration_finds_this_process() {
let processes = snapshot();
assert!(!processes.is_empty(), "a host always runs something");
let me = std::process::id();
let mine = processes
.iter()
.find(|process| process.pid() == me)
.expect("enumeration must include the calling process");
assert!(
!mine.name().is_empty(),
"a process the host reports must have a name to match on"
);
assert!(
mine.running(),
"the calling process is running by definition"
);
}
#[test]
fn a_sample_is_a_percentage_of_one_core() {
let mut sampler = CpuSampler::new();
let first = sampler.sample();
assert!(
(0.0..=100.0).contains(&first),
"even the baseline sample must be in range, got {first}"
);
std::thread::sleep(CpuSampler::minimum_interval());
let second = sampler.sample();
assert!(
(0.0..=100.0).contains(&second),
"a sample must be a percentage, got {second}"
);
}
#[test]
fn the_minimum_interval_is_nonzero() {
assert!(CpuSampler::minimum_interval() > Duration::ZERO);
}
}