use std::{
fs,
process::Command,
sync::OnceLock,
time::{Duration, Instant},
};
const PROC_SELF_STAT: &str = "/proc/self/stat";
const GETCONF: &str = "getconf";
const CLK_TCK: &str = "CLK_TCK";
const NS_PER_SEC: u128 = 1_000_000_000;
const UTIME_FIELD_IDX: usize = 11;
const STIME_FIELD_IDX: usize = 12;
fn clock_ticks_per_second() -> Option<u128> {
static TICKS: OnceLock<u128> = OnceLock::new();
if let Some(ticks) = TICKS.get() {
return Some(*ticks);
}
let output = Command::new(GETCONF).arg(CLK_TCK).output().ok()?;
let ticks = output
.status
.success()
.then(|| String::from_utf8(output.stdout).ok()?.trim().parse().ok())
.flatten()
.filter(|ticks| *ticks != 0)?;
let _ = TICKS.set(ticks);
Some(ticks)
}
pub fn process_cpu_ns() -> Option<u128> {
let raw = fs::read_to_string(PROC_SELF_STAT).ok()?;
let close = raw.rfind(')')?;
let fields: Vec<&str> = raw[close + 1..].split_whitespace().collect();
let user_ticks = fields.get(UTIME_FIELD_IDX)?.parse::<u128>().ok()?;
let system_ticks = fields.get(STIME_FIELD_IDX)?.parse::<u128>().ok()?;
let ticks_per_second = clock_ticks_per_second().filter(|ticks| *ticks != 0)?;
Some((user_ticks + system_ticks) * NS_PER_SEC / ticks_per_second)
}
pub fn cpu_seconds_since(start_ns: Option<u128>) -> Option<f64> {
let start = start_ns?;
let end = process_cpu_ns()?;
Some(end.saturating_sub(start) as f64 / NS_PER_SEC as f64)
}
const PROC_THREAD_SELF_SCHEDSTAT: &str = "/proc/thread-self/schedstat";
pub(crate) fn thread_cpu_ns() -> Option<u128> {
let raw = fs::read_to_string(PROC_THREAD_SELF_SCHEDSTAT).ok()?;
raw.split_whitespace().next()?.parse().ok()
}
pub(crate) fn thread_cpu_delta_ns(start: Option<u128>) -> u64 {
let Some(t0) = start else {
return 0;
};
thread_cpu_ns().map_or(0, |t1| {
u64::try_from(t1.saturating_sub(t0)).unwrap_or(u64::MAX)
})
}
pub fn timed<T>(f: impl FnOnce() -> T) -> (T, Duration, Option<f64>) {
let cpu0 = process_cpu_ns();
let t0 = Instant::now();
let out = f();
let wall = t0.elapsed();
(out, wall, cpu_seconds_since(cpu0))
}
#[cfg(test)]
mod tests {
use std::{hint::black_box, thread};
use super::*;
const EXITING_WORKERS: usize = 4;
const TEST_WORK_WINDOW: Duration = Duration::from_millis(50);
const TEST_BUSY_INNER_ITERS: u64 = 4_096;
const TEST_BUSY_MIX_CONST: u64 = 2_654_435_761;
const TEST_MIN_BUSY_CPU_S: f64 = 0.010;
const TEST_SLEEP_VS_BUSY_FACTOR: f64 = 5.0;
const TEST_TIMED_SLEEP: Duration = Duration::from_millis(20);
const TEST_WORKER_BUSY_WINDOW: Duration = Duration::from_millis(50);
const TEST_MIN_EXITED_WORKER_CPU_S: f64 = 0.01;
#[test]
fn on_cpu_time_excludes_sleep() {
let Some(start_busy) = thread_cpu_ns() else {
return;
};
let t = Instant::now();
let mut acc = 0u64;
while t.elapsed() < TEST_WORK_WINDOW {
for i in 0..TEST_BUSY_INNER_ITERS {
acc = acc.wrapping_add(i.wrapping_mul(TEST_BUSY_MIX_CONST));
}
}
black_box(acc);
let busy = (thread_cpu_ns().expect("cpu") - start_busy) as f64 / NS_PER_SEC as f64;
let start_sleep = thread_cpu_ns().expect("cpu");
thread::sleep(TEST_WORK_WINDOW);
let slept = (thread_cpu_ns().expect("cpu") - start_sleep) as f64 / NS_PER_SEC as f64;
assert!(
busy > TEST_MIN_BUSY_CPU_S,
"50ms of busy work must accrue on-CPU time, got {busy}s"
);
assert!(
slept < busy / TEST_SLEEP_VS_BUSY_FACTOR,
"a sleep ({slept}s) must accrue far less on-CPU time than busy work ({busy}s)"
);
}
#[test]
fn timed_returns_value_wall_and_cpu() {
let (value, wall, cpu) = timed(|| {
thread::sleep(TEST_TIMED_SLEEP);
42u64
});
assert_eq!(value, 42);
assert!(wall >= TEST_TIMED_SLEEP);
if process_cpu_ns().is_some() {
assert!(cpu.expect("procfs host measures cpu") >= 0.0);
}
}
#[test]
fn process_cpu_retains_exited_worker_time() {
let Some(before) = process_cpu_ns() else {
return;
};
let workers: Vec<_> = (0..EXITING_WORKERS)
.map(|_| {
thread::spawn(|| {
let started = Instant::now();
let mut value = 0u64;
while started.elapsed() < TEST_WORKER_BUSY_WINDOW {
value = value.wrapping_mul(31).wrapping_add(17);
black_box(value);
}
})
})
.collect();
for worker in workers {
worker.join().expect("worker");
}
let elapsed = cpu_seconds_since(Some(before)).expect("procfs CPU");
assert!(
elapsed > TEST_MIN_EXITED_WORKER_CPU_S,
"exited workers must remain in process CPU total, got {elapsed}s"
);
}
}