concinnity_engine/app/
syscpu.rs1use std::time::{Duration, Instant};
12
13pub(crate) fn process_cpu_time() -> Option<Duration> {
17 imp::process_cpu_time()
18}
19
20#[derive(Debug, Default)]
26pub struct CpuSampler {
27 last: Option<(Duration, Instant)>,
28}
29
30impl CpuSampler {
31 pub fn new() -> Self {
33 Self { last: None }
34 }
35
36 pub fn sample(&mut self) -> Option<f32> {
39 self.fold(process_cpu_time()?, Instant::now())
40 }
41
42 fn fold(&mut self, cpu: Duration, at: Instant) -> Option<f32> {
45 let rate = match self.last {
46 Some((last_cpu, last_at)) => {
47 let wall = at.saturating_duration_since(last_at).as_secs_f64();
48 if wall <= 0.0 {
51 return None;
52 }
53 let busy = cpu.saturating_sub(last_cpu).as_secs_f64();
56 Some((busy / wall) as f32)
57 }
58 None => None,
59 };
60 self.last = Some((cpu, at));
61 rate
62 }
63}
64
65#[cfg(unix)]
66mod imp {
67 use std::time::Duration;
68
69 pub(super) fn process_cpu_time() -> Option<Duration> {
72 let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
75 let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) };
78 (rc == 0).then(|| timeval(usage.ru_utime) + timeval(usage.ru_stime))
79 }
80
81 fn timeval(t: libc::timeval) -> Duration {
82 let secs = t.tv_sec.max(0) as u64;
83 let micros = t.tv_usec.clamp(0, 999_999) as u32;
84 Duration::new(secs, micros * 1_000)
85 }
86}
87
88#[cfg(windows)]
89mod imp {
90 use std::time::Duration;
91 use windows::Win32::Foundation::FILETIME;
92 use windows::Win32::System::Threading::{GetCurrentProcess, GetProcessTimes};
93
94 pub(super) fn process_cpu_time() -> Option<Duration> {
97 let mut creation = FILETIME::default();
98 let mut exit = FILETIME::default();
99 let mut kernel = FILETIME::default();
100 let mut user = FILETIME::default();
101 unsafe {
104 GetProcessTimes(
105 GetCurrentProcess(),
106 &mut creation,
107 &mut exit,
108 &mut kernel,
109 &mut user,
110 )
111 }
112 .ok()?;
113 Some(filetime(kernel) + filetime(user))
114 }
115
116 fn filetime(ft: FILETIME) -> Duration {
118 let ticks = ((ft.dwHighDateTime as u64) << 32) | ft.dwLowDateTime as u64;
119 Duration::from_nanos(ticks.saturating_mul(100))
120 }
121}
122
123#[cfg(not(any(unix, windows)))]
124mod imp {
125 use std::time::Duration;
126
127 pub(super) fn process_cpu_time() -> Option<Duration> {
128 None
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
141 fn query_returns_a_plausible_value() {
142 if !cfg!(any(unix, windows)) {
143 return;
144 }
145
146 let start = process_cpu_time().expect("CPU query works on this platform");
147
148 let deadline = Instant::now() + Duration::from_secs(5);
151 let mut work: u64 = 0;
152 let latest = loop {
153 for _ in 0..4096 {
154 work = work.wrapping_mul(2_654_435_761).wrapping_add(1);
155 }
156 let now = process_cpu_time().expect("CPU query keeps working");
157 assert!(now >= start, "process CPU time must be non-decreasing");
158 if now > start || Instant::now() >= deadline {
159 break now;
160 }
161 };
162 std::hint::black_box(work);
163
164 assert!(
165 latest > start,
166 "process CPU time should advance while the test burns CPU"
167 );
168 }
169
170 #[test]
172 fn first_fold_reports_no_rate() {
173 let mut sampler = CpuSampler::new();
174 assert_eq!(sampler.fold(Duration::from_secs(1), Instant::now()), None);
175 }
176
177 #[test]
179 fn fold_reports_cpu_time_over_wall_time() {
180 let t0 = Instant::now();
181 let mut sampler = CpuSampler::new();
182 sampler.fold(Duration::ZERO, t0);
183
184 let rate = sampler
185 .fold(Duration::from_secs(1), t0 + Duration::from_secs(2))
186 .expect("second fold spans a real interval");
187 assert!((rate - 0.5).abs() < 1e-5, "expected 0.5 cores, got {rate}");
188 }
189
190 #[test]
193 fn fold_reports_more_than_one_core() {
194 let t0 = Instant::now();
195 let mut sampler = CpuSampler::new();
196 sampler.fold(Duration::ZERO, t0);
197
198 let rate = sampler
199 .fold(Duration::from_secs(4), t0 + Duration::from_secs(1))
200 .expect("second fold spans a real interval");
201 assert!((rate - 4.0).abs() < 1e-5, "expected 4.0 cores, got {rate}");
202 }
203
204 #[test]
207 fn fold_measures_each_interval_independently() {
208 let t0 = Instant::now();
209 let mut sampler = CpuSampler::new();
210 sampler.fold(Duration::ZERO, t0);
211 sampler.fold(Duration::from_secs(1), t0 + Duration::from_secs(1));
212
213 let rate = sampler
214 .fold(Duration::from_millis(1500), t0 + Duration::from_secs(2))
215 .expect("third fold spans a real interval");
216 assert!((rate - 0.5).abs() < 1e-5, "expected 0.5 cores, got {rate}");
217 }
218
219 #[test]
221 fn fold_with_no_elapsed_time_reports_no_rate() {
222 let t0 = Instant::now();
223 let mut sampler = CpuSampler::new();
224 sampler.fold(Duration::ZERO, t0);
225 assert_eq!(sampler.fold(Duration::from_secs(1), t0), None);
226 }
227}