use std::{
sync::{Arc, Mutex},
time::{Duration, Instant},
};
const CPU_MAX_MILLIS: u32 = 1000;
pub trait CpuUsageProvider: Send + Sync + 'static {
fn cpu_usage_millis(&self) -> u32;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopCpuUsageProvider;
impl CpuUsageProvider for NoopCpuUsageProvider {
fn cpu_usage_millis(&self) -> u32 {
0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CpuSamplerConfig {
pub refresh_interval: Duration,
pub beta: f64,
}
impl Default for CpuSamplerConfig {
fn default() -> Self {
Self {
refresh_interval: Duration::from_millis(250),
beta: 0.95,
}
}
}
#[derive(Debug)]
pub struct LinuxCpuUsageProvider {
config: CpuSamplerConfig,
state: Mutex<LinuxCpuState>,
}
#[derive(Debug, Clone, Copy)]
struct LinuxCpuState {
previous: Option<CpuTimes>,
last_refresh: Option<Instant>,
usage_millis: u32,
}
#[derive(Debug, Clone, Copy)]
struct CpuTimes {
idle: u64,
total: u64,
}
impl LinuxCpuUsageProvider {
pub fn new() -> Self {
Self::with_config(CpuSamplerConfig::default())
}
pub fn with_config(config: CpuSamplerConfig) -> Self {
Self {
config,
state: Mutex::new(LinuxCpuState {
previous: None,
last_refresh: None,
usage_millis: 0,
}),
}
}
fn refresh(&self, now: Instant) -> u32 {
let mut state = self.state.lock().expect("linux cpu state lock");
if let Some(last_refresh) = state.last_refresh {
if now.duration_since(last_refresh) < self.config.refresh_interval {
return state.usage_millis;
}
}
let Some(current) = read_cpu_times() else {
state.last_refresh = Some(now);
return state.usage_millis;
};
if let Some(previous) = state.previous {
let raw = usage_between(previous, current);
let beta = self.config.beta.clamp(0.0, 1.0);
state.usage_millis = ((state.usage_millis as f64 * beta) + (raw as f64 * (1.0 - beta)))
.round()
.clamp(0.0, CPU_MAX_MILLIS as f64) as u32;
}
state.previous = Some(current);
state.last_refresh = Some(now);
state.usage_millis
}
}
impl Default for LinuxCpuUsageProvider {
fn default() -> Self {
Self::new()
}
}
impl CpuUsageProvider for LinuxCpuUsageProvider {
fn cpu_usage_millis(&self) -> u32 {
self.refresh(Instant::now())
}
}
pub type SharedCpuUsageProvider = Arc<dyn CpuUsageProvider>;
pub fn default_cpu_provider() -> SharedCpuUsageProvider {
real_cpu_provider().unwrap_or_else(|| Arc::new(NoopCpuUsageProvider))
}
pub fn real_cpu_provider() -> Option<SharedCpuUsageProvider> {
#[cfg(target_os = "linux")]
{
return Some(Arc::new(LinuxCpuUsageProvider::new()));
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
fn read_cpu_times() -> Option<CpuTimes> {
#[cfg(target_os = "linux")]
{
read_linux_proc_stat()
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
#[cfg(target_os = "linux")]
fn read_linux_proc_stat() -> Option<CpuTimes> {
let stat = std::fs::read_to_string("/proc/stat").ok()?;
parse_proc_stat(&stat)
}
#[cfg(target_os = "linux")]
fn parse_proc_stat(input: &str) -> Option<CpuTimes> {
let line = input.lines().find(|line| line.starts_with("cpu "))?;
parse_proc_stat_line(line)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn parse_proc_stat_line(line: &str) -> Option<CpuTimes> {
let mut parts = line.split_whitespace();
if parts.next()? != "cpu" {
return None;
}
let values = parts
.take(10)
.map(str::parse::<u64>)
.collect::<Result<Vec<_>, _>>()
.ok()?;
if values.len() < 4 {
return None;
}
let idle = values[3] + values.get(4).copied().unwrap_or(0);
let total = values.iter().copied().sum();
Some(CpuTimes { idle, total })
}
fn usage_between(previous: CpuTimes, current: CpuTimes) -> u32 {
let total_delta = current.total.saturating_sub(previous.total);
if total_delta == 0 {
return 0;
}
let idle_delta = current.idle.saturating_sub(previous.idle);
let busy_delta = total_delta.saturating_sub(idle_delta);
((busy_delta.saturating_mul(CPU_MAX_MILLIS as u64)) / total_delta).min(CPU_MAX_MILLIS as u64)
as u32
}
#[cfg(test)]
mod tests {
use super::{CpuTimes, parse_proc_stat_line, usage_between};
#[test]
fn parses_linux_proc_stat_line() {
let times = parse_proc_stat_line("cpu 10 20 30 40 5 6 7 8 9 10").expect("times");
assert_eq!(times.idle, 45);
assert_eq!(times.total, 145);
}
#[test]
fn calculates_busy_cpu_millis() {
let previous = CpuTimes {
idle: 100,
total: 200,
};
let current = CpuTimes {
idle: 125,
total: 300,
};
assert_eq!(usage_between(previous, current), 750);
}
}