1#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct ThreadTimes {
19 pub cpu_ns: u64,
21 pub voluntary_switches: Option<u64>,
23}
24
25pub 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 #[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 if unsafe { getrusage(RUSAGE_THREAD, &mut r) } != 0 {
53 return None;
54 }
55 let mut ts = sys::timespec { tv_sec: 0, tv_nsec: 0 };
61 if unsafe { sys::clock_gettime(CLOCK_THREAD_CPUTIME_ID, &mut ts) } != 0 {
63 return None;
64 }
65 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 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 (unsafe { QueryThreadCycleTime(sys::GetCurrentThread(), &mut c) } != 0).then_some(c)
115 }
116
117 static RATE: AtomicU64 = AtomicU64::new(0);
119
120 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 #[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 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 assert!(low.len() <= 3, "{} of {n} 3 ms spins counted 1.5 ms or less: {low:?} us", low.len());
209 }
210}