Skip to main content

simoom_sys/
cputime.rs

1//! Per-thread CPU time, for telling a slow handler from a preempted thread.
2//!
3//! [`thread_times`] reads the calling thread's CPU time and, on Linux, its count of voluntary
4//! context switches (the thread blocked: sleep, I/O, a contended lock). A loop thread that was
5//! only preempted shows wall time but neither CPU time nor voluntary switches.
6//!
7//! - Linux (64-bit): `clock_gettime(CLOCK_THREAD_CPUTIME_ID)` for the CPU time and
8//!   `getrusage(RUSAGE_THREAD)` for the switches. `getrusage`'s CPU times are only as fresh as
9//!   the last scheduler tick for a running thread, so they miss most of a short call.
10//! - Other Unix: `clock_gettime(CLOCK_THREAD_CPUTIME_ID)`, CPU time only.
11//! - Windows: `QueryThreadCycleTime`, converted to nanoseconds with a cycle rate measured once
12//!   (about 1.5 ms of spinning on the first call).
13//!
14//! This file is owned by the facade work (the stall watchdog in `simoom`).
15
16/// CPU time used by the calling thread so far.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct ThreadTimes {
19    /// User plus system CPU time, nanoseconds.
20    pub cpu_ns: u64,
21    /// Voluntary context switches so far, where the platform counts them per thread (Linux).
22    pub voluntary_switches: Option<u64>,
23}
24
25/// The calling thread's CPU time, or `None` where it can't be read.
26pub fn thread_times() -> Option<ThreadTimes> {
27    imp::thread_times()
28}
29
30#[cfg(all(target_os = "linux", target_pointer_width = "64"))]
31mod imp {
32    use super::ThreadTimes;
33    use crate::os::sys;
34    use core::ffi::{c_int, c_long};
35
36    const RUSAGE_THREAD: c_int = 1;
37    const CLOCK_THREAD_CPUTIME_ID: c_int = 3;
38
39    /// `struct rusage` on 64-bit Linux: two timevals (four longs: ru_utime, ru_stime) and
40    /// fourteen longs.
41    #[repr(C)]
42    struct Rusage([c_long; 18]);
43
44    extern "C" {
45        fn getrusage(who: c_int, usage: *mut Rusage) -> c_int;
46    }
47
48    pub(super) fn thread_times() -> Option<ThreadTimes> {
49        let mut r = Rusage([0; 18]);
50        // SAFETY: `r` is a writable `struct rusage` with the 64-bit Linux layout; RUSAGE_THREAD
51        // exists since Linux 2.6.26.
52        if unsafe { getrusage(RUSAGE_THREAD, &mut r) } != 0 {
53            return None;
54        }
55        // The CPU time comes from the thread CPU clock, not from ru_utime + ru_stime: those come
56        // from the scheduler's runtime sum, which for a running thread is brought up to date only
57        // at a scheduler tick or a context switch, so a 3 ms call read as anything from 0 to 3 ms
58        // (4 ms ticks at HZ=250) and a CPU-bound route under the budget's few ticks often went
59        // unseen. The thread CPU clock updates the sum first, so it is exact.
60        let mut ts = sys::timespec { tv_sec: 0, tv_nsec: 0 };
61        // SAFETY: `ts` is a valid output struct; CLOCK_THREAD_CPUTIME_ID exists since Linux 2.6.12.
62        if unsafe { sys::clock_gettime(CLOCK_THREAD_CPUTIME_ID, &mut ts) } != 0 {
63            return None;
64        }
65        // Long 16 is ru_nvcsw (after the two timevals and maxrss, ixrss, idrss, isrss, minflt,
66        // majflt, nswap, inblock, oublock, msgsnd, msgrcv, nsignals); switch counts are exact.
67        Some(ThreadTimes { cpu_ns: ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64, voluntary_switches: Some(r.0[16] as u64) })
68    }
69}
70
71#[cfg(all(unix, not(all(target_os = "linux", target_pointer_width = "64"))))]
72mod imp {
73    use super::ThreadTimes;
74    use core::ffi::c_int;
75
76    #[cfg(target_os = "linux")]
77    const CLOCK_THREAD_CPUTIME_ID: c_int = 3;
78    #[cfg(any(target_os = "macos", target_os = "ios"))]
79    const CLOCK_THREAD_CPUTIME_ID: c_int = 16;
80    #[cfg(target_os = "freebsd")]
81    const CLOCK_THREAD_CPUTIME_ID: c_int = 14;
82
83    #[cfg(any(target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd"))]
84    pub(super) fn thread_times() -> Option<ThreadTimes> {
85        use crate::os::sys;
86        let mut ts = sys::timespec { tv_sec: 0, tv_nsec: 0 };
87        // SAFETY: `ts` is a valid output struct; an unknown clock id only returns an error.
88        if unsafe { sys::clock_gettime(CLOCK_THREAD_CPUTIME_ID, &mut ts) } != 0 {
89            return None;
90        }
91        Some(ThreadTimes { cpu_ns: ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64, voluntary_switches: None })
92    }
93
94    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd")))]
95    pub(super) fn thread_times() -> Option<ThreadTimes> {
96        None
97    }
98}
99
100#[cfg(windows)]
101mod imp {
102    use super::ThreadTimes;
103    use crate::os::sys;
104    use std::sync::atomic::{AtomicU64, Ordering};
105
106    #[link(name = "kernel32")]
107    extern "system" {
108        fn QueryThreadCycleTime(thread: sys::HANDLE, cycles: *mut u64) -> sys::BOOL;
109    }
110
111    fn cycles() -> Option<u64> {
112        let mut c = 0u64;
113        // SAFETY: the pseudo handle of the current thread is always valid; `c` is writable.
114        (unsafe { QueryThreadCycleTime(sys::GetCurrentThread(), &mut c) } != 0).then_some(c)
115    }
116
117    /// Cycles per microsecond times 1024, measured once (0: not yet).
118    static RATE: AtomicU64 = AtomicU64::new(0);
119
120    /// Spins about 500 us three times and keeps the highest cycles-per-wall-time ratio: a
121    /// preempted run only lowers it.
122    fn rate() -> Option<u64> {
123        let r = RATE.load(Ordering::Relaxed);
124        if r != 0 {
125            return Some(r);
126        }
127        let mut best = 0u64;
128        for _ in 0..3 {
129            let (c0, t0) = (cycles()?, crate::time::now_ns());
130            let mut t1 = t0;
131            while t1 - t0 < 500_000 {
132                std::hint::spin_loop();
133                t1 = crate::time::now_ns();
134            }
135            let c1 = cycles()?;
136            best = best.max((c1 - c0) * 1024 * 1000 / (t1 - t0).max(1));
137        }
138        let best = best.max(1);
139        RATE.store(best, Ordering::Relaxed);
140        Some(best)
141    }
142
143    pub(super) fn thread_times() -> Option<ThreadTimes> {
144        let r = rate()?;
145        let c = cycles()?;
146        Some(ThreadTimes { cpu_ns: (c as u128 * 1024 * 1000 / r as u128) as u64, voluntary_switches: None })
147    }
148}
149
150#[cfg(not(any(unix, windows)))]
151mod imp {
152    pub(super) fn thread_times() -> Option<super::ThreadTimes> {
153        None
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn cpu_time_counts_work_but_not_sleep() {
163        let Some(a) = thread_times() else { return };
164        std::thread::sleep(std::time::Duration::from_millis(30));
165        let b = thread_times().unwrap();
166        assert!(b.cpu_ns - a.cpu_ns < 15_000_000, "sleeping used {} ns of CPU", b.cpu_ns - a.cpu_ns);
167        if let (Some(x), Some(y)) = (a.voluntary_switches, b.voluntary_switches) {
168            assert!(y > x, "a sleep is a voluntary switch");
169        }
170        let t = crate::time::now_ns();
171        let mut x = 0u64;
172        while crate::time::now_ns() - t < 20_000_000 {
173            x = x.wrapping_add(std::hint::black_box(1));
174        }
175        let c = thread_times().unwrap();
176        assert!(c.cpu_ns - b.cpu_ns > 10_000_000, "20 ms of spinning counted {} ns ({x})", c.cpu_ns - b.cpu_ns);
177        let n = 100_000;
178        let t = std::time::Instant::now();
179        for _ in 0..n {
180            std::hint::black_box(thread_times());
181        }
182        eprintln!("thread_times: {:.0} ns per read", t.elapsed().as_nanos() as f64 / n as f64);
183    }
184
185    /// Short calls count in full: `getrusage`'s CPU times on Linux lag to the last scheduler
186    /// tick, so about half of these 3 ms spins (just after a sleep, as a loop thread runs a call
187    /// after a wait) read as 1 ms or less, and the watchdog missed CPU-bound routes.
188    #[test]
189    fn a_short_spin_after_a_sleep_counts_in_full() {
190        if thread_times().is_none() {
191            return;
192        }
193        let (n, mut low) = (20, Vec::new());
194        for i in 0..n {
195            // Uneven sleeps, so the spins don't lock to one phase of the tick.
196            std::thread::sleep(std::time::Duration::from_micros(5000 + i * 370));
197            let a = thread_times().unwrap();
198            let t = crate::time::now_ns();
199            while crate::time::now_ns() - t < 3_000_000 {
200                std::hint::spin_loop();
201            }
202            let us = (thread_times().unwrap().cpu_ns - a.cpu_ns) / 1000;
203            if us <= 1500 {
204                low.push(us);
205            }
206        }
207        // A preempted spin may use less CPU than its wall time; a few, not half.
208        assert!(low.len() <= 3, "{} of {n} 3 ms spins counted 1.5 ms or less: {low:?} us", low.len());
209    }
210}