1use std::io;
20use std::time::Duration;
21
22#[derive(Clone, Copy, Debug, Default, PartialEq)]
24pub struct ProcessUsage {
25 pub resident_bytes: u64,
27 pub footprint_bytes: u64,
29 pub cpu_seconds: f64,
31 pub wakeups_total: Option<u64>,
33 pub disk_ops_total: Option<u64>,
35 pub disk_read_bytes: Option<u64>,
37 pub disk_write_bytes: Option<u64>,
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq)]
43pub struct UsageRates {
44 pub cpu_percent: f64,
46 pub wakeups_per_second: Option<f64>,
47 pub disk_ops_per_second: Option<f64>,
48 pub disk_read_bytes_per_second: Option<f64>,
49 pub disk_write_bytes_per_second: Option<f64>,
50}
51
52impl ProcessUsage {
53 pub fn rates_since(&self, earlier: &ProcessUsage, elapsed: Duration) -> UsageRates {
56 let secs = elapsed.as_secs_f64();
57 let rate = |now: Option<u64>, then: Option<u64>| match (now, then) {
58 (Some(now), Some(then)) if secs > 0.0 => Some(now.saturating_sub(then) as f64 / secs),
59 (Some(_), Some(_)) => Some(0.0),
60 _ => None,
61 };
62 UsageRates {
63 cpu_percent: if secs > 0.0 {
64 (self.cpu_seconds - earlier.cpu_seconds).max(0.0) / secs * 100.0
65 } else {
66 0.0
67 },
68 wakeups_per_second: rate(self.wakeups_total, earlier.wakeups_total),
69 disk_ops_per_second: rate(self.disk_ops_total, earlier.disk_ops_total),
70 disk_read_bytes_per_second: rate(self.disk_read_bytes, earlier.disk_read_bytes),
71 disk_write_bytes_per_second: rate(self.disk_write_bytes, earlier.disk_write_bytes),
72 }
73 }
74}
75
76pub fn process_usage() -> io::Result<ProcessUsage> {
78 imp::sample()
79}
80
81#[derive(Clone, Copy, Debug)]
83pub struct IdleObservation {
84 pub elapsed: Duration,
85 pub start: ProcessUsage,
86 pub end: ProcessUsage,
87 pub rates: UsageRates,
88}
89
90pub fn observe_idle(duration: Duration) -> io::Result<IdleObservation> {
93 let start = process_usage()?;
94 let began = std::time::Instant::now();
95 std::thread::sleep(duration);
96 let end = process_usage()?;
97 let elapsed = began.elapsed();
98 Ok(IdleObservation {
99 elapsed,
100 start,
101 end,
102 rates: end.rates_since(&start, elapsed),
103 })
104}
105
106#[derive(Clone, Copy, Debug)]
108pub struct BudgetLabels<'a> {
109 pub krate: &'a str,
111 pub version: &'a str,
112 pub scale_unit: &'a str,
114 pub scale: u64,
116 pub what: &'a str,
118 pub scope: &'a str,
120}
121
122impl IdleObservation {
123 pub fn result_lines(&self, warm: &ProcessUsage, labels: &BudgetLabels<'_>) -> String {
138 let BudgetLabels {
139 krate,
140 version,
141 scale_unit,
142 scale,
143 what,
144 scope,
145 } = *labels;
146 let mut out = String::new();
147 let mut line = |text: String| {
148 out.push_str("RESULT ");
149 out.push_str(&text);
150 out.push('\n');
151 };
152 let resident_max = warm.resident_bytes.max(self.end.resident_bytes);
153 line(format!("warm_resident_bytes={} bytes", warm.resident_bytes));
154 line(format!(
155 "idle_resident_bytes={} bytes",
156 self.end.resident_bytes
157 ));
158 if resident_max > 0 {
159 line(format!("resident_memory_bytes={resident_max} bytes"));
160 } else {
161 line("resident_memory_bytes=unavailable host reported a zero resident set".to_string());
162 }
163 line(format!(
164 "footprint_bytes={} bytes",
165 warm.footprint_bytes.max(self.end.footprint_bytes)
166 ));
167 line(format!(
168 "idle_window_seconds={:.1} seconds",
169 self.elapsed.as_secs_f64()
170 ));
171 line(format!(
172 "idle_cpu_percent={:.3} percent",
173 self.rates.cpu_percent
174 ));
175 let opt = |name: &str,
176 value: Option<f64>,
177 unit: &str,
178 reason: &str,
179 line: &mut dyn FnMut(String)| match value {
180 Some(v) => line(format!("{name}={v:.3} {unit}")),
181 None => line(format!("{name}=unavailable {reason}")),
182 };
183 opt(
184 "wakeups_per_second",
185 self.rates.wakeups_per_second,
186 "per_second",
187 "the host exposes no process wake-up counter",
188 &mut line,
189 );
190 opt(
191 "disk_ops_per_second",
192 self.rates.disk_ops_per_second,
193 "per_second",
194 "the host exposes no process disk-operation counter",
195 &mut line,
196 );
197 opt(
198 "disk_read_bytes_per_second",
199 self.rates.disk_read_bytes_per_second,
200 "bytes_per_second",
201 "the host exposes no process disk-byte counter",
202 &mut line,
203 );
204 opt(
205 "disk_write_bytes_per_second",
206 self.rates.disk_write_bytes_per_second,
207 "bytes_per_second",
208 "the host exposes no process disk-byte counter",
209 &mut line,
210 );
211 line(format!("scale={scale} {what}"));
212 line(format!("scope={scope}"));
213 let num =
214 |v: Option<f64>| v.map_or_else(|| "unavailable".to_string(), |v| format!("{v:.3}"));
215 let resident = if resident_max > 0 {
216 resident_max.to_string()
217 } else {
218 "unavailable".to_string()
219 };
220 line(format!(
221 "budget crate={krate} version={version} resident_bytes={resident} idle_cpu_percent={:.3} wakeups_per_s={} disk_ops_per_s={} scale_unit={scale_unit} scale_count={scale}",
222 self.rates.cpu_percent,
223 num(self.rates.wakeups_per_second),
224 num(self.rates.disk_ops_per_second),
225 ));
226 out
227 }
228}
229
230#[cfg(target_os = "macos")]
231mod imp {
232 use super::ProcessUsage;
233 use std::io;
234
235 extern "C" {
236 fn proc_pid_rusage(pid: i32, flavor: i32, buffer: *mut u8) -> i32;
237 }
238
239 const W_INTERRUPT_WKUPS: usize = 2 + 3;
242 const W_RESIDENT: usize = 2 + 6;
243 const W_FOOTPRINT: usize = 2 + 7;
244 const W_DISK_READ: usize = 2 + 16;
245 const W_DISK_WRITE: usize = 2 + 17;
246
247 pub fn sample() -> io::Result<ProcessUsage> {
248 let mut words = [0u64; 64];
249 let rc = unsafe { proc_pid_rusage(libc::getpid(), 2, words.as_mut_ptr().cast()) };
251 if rc != 0 {
252 return Err(io::Error::last_os_error());
253 }
254 let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
255 if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) } != 0 {
256 return Err(io::Error::last_os_error());
257 }
258 let secs = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 / 1e6;
259 Ok(ProcessUsage {
260 resident_bytes: words[W_RESIDENT],
261 footprint_bytes: words[W_FOOTPRINT],
262 cpu_seconds: secs(ru.ru_utime) + secs(ru.ru_stime),
263 wakeups_total: Some(words[W_INTERRUPT_WKUPS]),
264 disk_ops_total: None,
265 disk_read_bytes: Some(words[W_DISK_READ]),
266 disk_write_bytes: Some(words[W_DISK_WRITE]),
267 })
268 }
269}
270
271#[cfg(all(
272 unix,
273 not(target_os = "macos"),
274 not(any(target_os = "linux", target_os = "android"))
275))]
276mod imp {
277 use super::ProcessUsage;
278 use std::io;
279
280 pub fn sample() -> io::Result<ProcessUsage> {
283 Err(io::Error::new(
284 io::ErrorKind::Unsupported,
285 "process usage sampling is implemented for macOS, Linux, Android and Windows only",
286 ))
287 }
288}
289
290#[cfg(any(target_os = "linux", target_os = "android"))]
291mod imp {
292 use super::ProcessUsage;
293 use std::io;
294
295 pub fn sample() -> io::Result<ProcessUsage> {
296 let statm = std::fs::read_to_string("/proc/self/statm")?;
297 let pages: u64 = statm
298 .split_whitespace()
299 .nth(1)
300 .and_then(|v| v.parse().ok())
301 .ok_or_else(|| {
302 io::Error::new(io::ErrorKind::InvalidData, "unreadable /proc/self/statm")
303 })?;
304 let page = u64::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(1))
305 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "page size out of range"))?;
306 let overflow = || io::Error::new(io::ErrorKind::InvalidData, "memory size overflows u64");
307 let resident = pages.checked_mul(page).ok_or_else(overflow)?;
308 let blocks = |count: libc::c_long| u64::try_from(count.max(0)).unwrap_or(0);
310 let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
311 if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) } != 0 {
312 return Err(io::Error::last_os_error());
313 }
314 let secs = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 / 1e6;
315 Ok(ProcessUsage {
316 resident_bytes: resident,
317 footprint_bytes: resident,
318 cpu_seconds: secs(ru.ru_utime) + secs(ru.ru_stime),
319 wakeups_total: None,
320 disk_ops_total: blocks(ru.ru_inblock).checked_add(blocks(ru.ru_oublock)),
321 disk_read_bytes: None,
322 disk_write_bytes: None,
323 })
324 }
325}
326
327#[cfg(windows)]
328mod imp {
329 use super::ProcessUsage;
330 use std::io;
331 use windows::Win32::Foundation::FILETIME;
332 use windows::Win32::System::ProcessStatus::{
333 GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX,
334 };
335 use windows::Win32::System::Threading::{
336 GetCurrentProcess, GetProcessIoCounters, GetProcessTimes, IO_COUNTERS,
337 };
338
339 fn os(error: windows::core::Error) -> io::Error {
340 io::Error::from_raw_os_error(error.code().0 & 0xFFFF)
341 }
342
343 fn ticks(t: FILETIME) -> u64 {
344 ((t.dwHighDateTime as u64) << 32) | t.dwLowDateTime as u64
345 }
346
347 pub fn sample() -> io::Result<ProcessUsage> {
348 unsafe {
349 let process = GetCurrentProcess();
350 let mut mem = PROCESS_MEMORY_COUNTERS_EX::default();
351 GetProcessMemoryInfo(
352 process,
353 (&mut mem as *mut PROCESS_MEMORY_COUNTERS_EX).cast::<PROCESS_MEMORY_COUNTERS>(),
354 std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32,
355 )
356 .map_err(os)?;
357 let (mut created, mut exited, mut kernel, mut user) = (
358 FILETIME::default(),
359 FILETIME::default(),
360 FILETIME::default(),
361 FILETIME::default(),
362 );
363 GetProcessTimes(process, &mut created, &mut exited, &mut kernel, &mut user)
364 .map_err(os)?;
365 let mut io_counters = IO_COUNTERS::default();
366 GetProcessIoCounters(process, &mut io_counters).map_err(os)?;
367 Ok(ProcessUsage {
368 resident_bytes: mem.WorkingSetSize as u64,
369 footprint_bytes: mem.PrivateUsage as u64,
370 cpu_seconds: (ticks(kernel) + ticks(user)) as f64 / 1e7,
372 wakeups_total: None,
373 disk_ops_total: Some(
374 io_counters.ReadOperationCount + io_counters.WriteOperationCount,
375 ),
376 disk_read_bytes: Some(io_counters.ReadTransferCount),
377 disk_write_bytes: Some(io_counters.WriteTransferCount),
378 })
379 }
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[cfg(any(
389 target_os = "macos",
390 target_os = "linux",
391 target_os = "android",
392 windows
393 ))]
394 #[test]
395 fn sample_reports_resident_memory_and_cpu() {
396 let first = process_usage().unwrap();
397 assert!(first.resident_bytes > 0);
398 assert!(first.footprint_bytes > 0);
399 let mut sink = 0u64;
400 for i in 0..20_000_000u64 {
401 sink = sink.wrapping_add(i * 31);
402 }
403 std::hint::black_box(sink);
404 let second = process_usage().unwrap();
405 assert!(second.cpu_seconds >= first.cpu_seconds);
406 let rates = second.rates_since(&first, Duration::from_secs(1));
407 assert!(rates.cpu_percent >= 0.0);
408 }
409
410 #[cfg(all(
412 unix,
413 not(target_os = "macos"),
414 not(any(target_os = "linux", target_os = "android"))
415 ))]
416 #[test]
417 fn unsupported_unix_targets_report_unsupported_not_a_sample() {
418 let error = process_usage().expect_err("no sampler exists for this target");
419 assert_eq!(error.kind(), io::ErrorKind::Unsupported);
420 let error = observe_idle(Duration::from_millis(1)).expect_err("idle needs sampling");
421 assert_eq!(error.kind(), io::ErrorKind::Unsupported);
422 }
423
424 #[test]
425 fn rates_are_none_without_counters_and_zero_for_no_time() {
426 let a = ProcessUsage {
427 wakeups_total: None,
428 ..Default::default()
429 };
430 let b = ProcessUsage {
431 wakeups_total: Some(10),
432 ..Default::default()
433 };
434 assert_eq!(
435 b.rates_since(&a, Duration::from_secs(1)).wakeups_per_second,
436 None
437 );
438 let c = ProcessUsage {
439 wakeups_total: Some(30),
440 ..Default::default()
441 };
442 assert_eq!(
443 c.rates_since(&b, Duration::from_secs(2)).wakeups_per_second,
444 Some(10.0)
445 );
446 assert_eq!(c.rates_since(&b, Duration::ZERO).cpu_percent, 0.0);
447 }
448}