use std::time::{Duration, Instant};
pub(crate) fn process_cpu_time() -> Option<Duration> {
imp::process_cpu_time()
}
#[derive(Debug, Default)]
pub struct CpuSampler {
last: Option<(Duration, Instant)>,
}
impl CpuSampler {
pub fn new() -> Self {
Self { last: None }
}
pub fn sample(&mut self) -> Option<f32> {
self.fold(process_cpu_time()?, Instant::now())
}
fn fold(&mut self, cpu: Duration, at: Instant) -> Option<f32> {
let rate = match self.last {
Some((last_cpu, last_at)) => {
let wall = at.saturating_duration_since(last_at).as_secs_f64();
if wall <= 0.0 {
return None;
}
let busy = cpu.saturating_sub(last_cpu).as_secs_f64();
Some((busy / wall) as f32)
}
None => None,
};
self.last = Some((cpu, at));
rate
}
}
#[cfg(unix)]
mod imp {
use std::time::Duration;
pub(super) fn process_cpu_time() -> Option<Duration> {
let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) };
(rc == 0).then(|| timeval(usage.ru_utime) + timeval(usage.ru_stime))
}
fn timeval(t: libc::timeval) -> Duration {
let secs = t.tv_sec.max(0) as u64;
let micros = t.tv_usec.clamp(0, 999_999) as u32;
Duration::new(secs, micros * 1_000)
}
}
#[cfg(windows)]
mod imp {
use std::time::Duration;
use windows::Win32::Foundation::FILETIME;
use windows::Win32::System::Threading::{GetCurrentProcess, GetProcessTimes};
pub(super) fn process_cpu_time() -> Option<Duration> {
let mut creation = FILETIME::default();
let mut exit = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
unsafe {
GetProcessTimes(
GetCurrentProcess(),
&mut creation,
&mut exit,
&mut kernel,
&mut user,
)
}
.ok()?;
Some(filetime(kernel) + filetime(user))
}
fn filetime(ft: FILETIME) -> Duration {
let ticks = ((ft.dwHighDateTime as u64) << 32) | ft.dwLowDateTime as u64;
Duration::from_nanos(ticks.saturating_mul(100))
}
}
#[cfg(not(any(unix, windows)))]
mod imp {
use std::time::Duration;
pub(super) fn process_cpu_time() -> Option<Duration> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_returns_a_plausible_value() {
if !cfg!(any(unix, windows)) {
return;
}
let start = process_cpu_time().expect("CPU query works on this platform");
let deadline = Instant::now() + Duration::from_secs(5);
let mut work: u64 = 0;
let latest = loop {
for _ in 0..4096 {
work = work.wrapping_mul(2_654_435_761).wrapping_add(1);
}
let now = process_cpu_time().expect("CPU query keeps working");
assert!(now >= start, "process CPU time must be non-decreasing");
if now > start || Instant::now() >= deadline {
break now;
}
};
std::hint::black_box(work);
assert!(
latest > start,
"process CPU time should advance while the test burns CPU"
);
}
#[test]
fn first_fold_reports_no_rate() {
let mut sampler = CpuSampler::new();
assert_eq!(sampler.fold(Duration::from_secs(1), Instant::now()), None);
}
#[test]
fn fold_reports_cpu_time_over_wall_time() {
let t0 = Instant::now();
let mut sampler = CpuSampler::new();
sampler.fold(Duration::ZERO, t0);
let rate = sampler
.fold(Duration::from_secs(1), t0 + Duration::from_secs(2))
.expect("second fold spans a real interval");
assert!((rate - 0.5).abs() < 1e-5, "expected 0.5 cores, got {rate}");
}
#[test]
fn fold_reports_more_than_one_core() {
let t0 = Instant::now();
let mut sampler = CpuSampler::new();
sampler.fold(Duration::ZERO, t0);
let rate = sampler
.fold(Duration::from_secs(4), t0 + Duration::from_secs(1))
.expect("second fold spans a real interval");
assert!((rate - 4.0).abs() < 1e-5, "expected 4.0 cores, got {rate}");
}
#[test]
fn fold_measures_each_interval_independently() {
let t0 = Instant::now();
let mut sampler = CpuSampler::new();
sampler.fold(Duration::ZERO, t0);
sampler.fold(Duration::from_secs(1), t0 + Duration::from_secs(1));
let rate = sampler
.fold(Duration::from_millis(1500), t0 + Duration::from_secs(2))
.expect("third fold spans a real interval");
assert!((rate - 0.5).abs() < 1e-5, "expected 0.5 cores, got {rate}");
}
#[test]
fn fold_with_no_elapsed_time_reports_no_rate() {
let t0 = Instant::now();
let mut sampler = CpuSampler::new();
sampler.fold(Duration::ZERO, t0);
assert_eq!(sampler.fold(Duration::from_secs(1), t0), None);
}
}