1use std::io;
26use std::time::Duration;
27
28#[derive(Clone, Copy, Debug, Default, PartialEq)]
30pub struct ProcessUsage {
31 pub resident_bytes: u64,
33 pub footprint_bytes: u64,
35 pub cpu_seconds: f64,
37 pub wakeups_total: Option<u64>,
39 pub disk_ops_total: Option<u64>,
41 pub disk_read_bytes: Option<u64>,
43 pub disk_write_bytes: Option<u64>,
45}
46
47#[derive(Clone, Copy, Debug, Default, PartialEq)]
49pub struct UsageRates {
50 pub cpu_percent: f64,
52 pub wakeups_per_second: Option<f64>,
53 pub disk_ops_per_second: Option<f64>,
54 pub disk_read_bytes_per_second: Option<f64>,
55 pub disk_write_bytes_per_second: Option<f64>,
56}
57
58impl ProcessUsage {
59 pub fn rates_since(&self, earlier: &ProcessUsage, elapsed: Duration) -> UsageRates {
62 let secs = elapsed.as_secs_f64();
63 let rate = |now: Option<u64>, then: Option<u64>| match (now, then) {
64 (Some(now), Some(then)) if secs > 0.0 => Some(now.saturating_sub(then) as f64 / secs),
65 (Some(_), Some(_)) => Some(0.0),
66 _ => None,
67 };
68 UsageRates {
69 cpu_percent: if secs > 0.0 {
70 (self.cpu_seconds - earlier.cpu_seconds).max(0.0) / secs * 100.0
71 } else {
72 0.0
73 },
74 wakeups_per_second: rate(self.wakeups_total, earlier.wakeups_total),
75 disk_ops_per_second: rate(self.disk_ops_total, earlier.disk_ops_total),
76 disk_read_bytes_per_second: rate(self.disk_read_bytes, earlier.disk_read_bytes),
77 disk_write_bytes_per_second: rate(self.disk_write_bytes, earlier.disk_write_bytes),
78 }
79 }
80}
81
82pub fn process_usage() -> io::Result<ProcessUsage> {
84 imp::sample()
85}
86
87#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
91pub struct TreeUsage {
92 pub processes: usize,
94 pub resident_bytes: u64,
96 pub unreadable: usize,
98}
99
100pub fn tree_usage(root: &crate::AdoptedIdentity) -> io::Result<TreeUsage> {
106 if root.verify()? != crate::IdentityCheck::Same {
107 return Ok(TreeUsage::default());
108 }
109 let mut members = vec![root.clone()];
110 members.extend(root.descendants()?);
111 let table = crate::process_table()?;
112 let mut usage = TreeUsage::default();
113 for member in &members {
114 if member.verify()? != crate::IdentityCheck::Same {
115 continue;
116 }
117 usage.processes += 1;
118 match table
119 .iter()
120 .find(|entry| entry.pid == member.pid())
121 .and_then(|entry| entry.working_set_bytes)
122 {
123 Some(bytes) => usage.resident_bytes = usage.resident_bytes.saturating_add(bytes),
124 None => usage.unreadable += 1,
125 }
126 }
127 Ok(usage)
128}
129
130#[derive(Clone, Copy, Debug)]
132pub struct IdleObservation {
133 pub elapsed: Duration,
134 pub start: ProcessUsage,
135 pub end: ProcessUsage,
136 pub rates: UsageRates,
137}
138
139pub fn observe_idle(duration: Duration) -> io::Result<IdleObservation> {
142 let start = process_usage()?;
143 let began = std::time::Instant::now();
144 std::thread::sleep(duration);
145 let end = process_usage()?;
146 let elapsed = began.elapsed();
147 Ok(IdleObservation {
148 elapsed,
149 start,
150 end,
151 rates: end.rates_since(&start, elapsed),
152 })
153}
154
155#[derive(Clone, Debug, PartialEq)]
159#[non_exhaustive]
160pub struct ThreadSample {
161 pub thread_id: u64,
163 pub started: Option<u64>,
166 pub wakeups: Option<u64>,
169 pub cpu_seconds: Option<f64>,
171 pub name: Option<String>,
174 pub start_address: Option<u64>,
178}
179
180#[derive(Clone, Debug, PartialEq)]
182pub struct ProcessThreads {
183 pub usage: ProcessUsage,
184 pub threads: Vec<ThreadSample>,
187}
188
189impl ProcessThreads {
190 pub fn thread_wakeups_sum(&self) -> Option<u64> {
192 sum_wakeups(&self.threads)
193 }
194}
195
196fn sum_wakeups(threads: &[ThreadSample]) -> Option<u64> {
197 threads
198 .iter()
199 .try_fold(0u64, |total, thread| total.checked_add(thread.wakeups?))
200}
201
202pub fn process_usage_with_threads() -> io::Result<ProcessThreads> {
209 let (usage, mut threads) = imp::sample_threads()?;
210 imp::enrich(&mut threads);
211 Ok(ProcessThreads { usage, threads })
212}
213
214#[derive(Clone, Debug, PartialEq)]
216pub struct ThreadDelta {
217 pub thread_id: u64,
218 pub wakeups: Option<u64>,
220 pub cpu_seconds: Option<f64>,
222 pub name: Option<String>,
223 pub start_address: Option<u64>,
224 pub started_in_window: bool,
227}
228
229#[derive(Clone, Debug, PartialEq)]
231pub struct ThreadDeltas {
232 pub threads: Vec<ThreadDelta>,
235 pub exited: Vec<ThreadSample>,
238}
239
240pub fn thread_deltas(start: &[ThreadSample], end: &[ThreadSample]) -> ThreadDeltas {
243 use std::collections::HashMap;
244 let key = |thread: &ThreadSample| (thread.thread_id, thread.started);
245 let mut earlier: HashMap<(u64, Option<u64>), &ThreadSample> =
246 start.iter().map(|thread| (key(thread), thread)).collect();
247 let mut threads: Vec<ThreadDelta> = end
248 .iter()
249 .map(|now| {
250 let before = earlier.remove(&key(now));
251 ThreadDelta {
252 thread_id: now.thread_id,
253 wakeups: match before {
254 Some(then) => counter_delta(now.wakeups, then.wakeups),
255 None => now.wakeups,
256 },
257 cpu_seconds: match before {
258 Some(then) => match (now.cpu_seconds, then.cpu_seconds) {
259 (Some(now), Some(then)) => Some((now - then).max(0.0)),
260 _ => None,
261 },
262 None => now.cpu_seconds,
263 },
264 name: now
265 .name
266 .clone()
267 .or_else(|| before.and_then(|then| then.name.clone())),
268 start_address: now
269 .start_address
270 .or_else(|| before.and_then(|then| then.start_address)),
271 started_in_window: before.is_none(),
272 }
273 })
274 .collect();
275 threads.sort_by(|a, b| {
276 b.wakeups
277 .cmp(&a.wakeups)
278 .then(a.thread_id.cmp(&b.thread_id))
279 });
280 let mut exited: Vec<ThreadSample> = earlier.into_values().cloned().collect();
281 exited.sort_by_key(|thread| thread.thread_id);
282 ThreadDeltas { threads, exited }
283}
284
285fn counter_delta(now: Option<u64>, then: Option<u64>) -> Option<u64> {
286 Some(now?.saturating_sub(then?))
287}
288
289impl ThreadDeltas {
290 pub fn total_wakeups(&self) -> Option<u64> {
294 self.threads
295 .iter()
296 .try_fold(0u64, |total, thread| total.checked_add(thread.wakeups?))
297 }
298
299 pub fn exited_wakeups_at_start(&self) -> Option<u64> {
301 sum_wakeups(&self.exited)
302 }
303
304 pub fn top(&self, n: usize) -> &[ThreadDelta] {
306 &self.threads[..n.min(self.threads.len())]
307 }
308
309 pub fn result_lines(&self, elapsed: Duration, top: usize) -> String {
315 let secs = elapsed.as_secs_f64();
316 let count = |v: Option<u64>| v.map_or_else(|| "unavailable".to_string(), |v| v.to_string());
317 let mut out = format!(
318 "RESULT thread_wakeups_summary threads={} born={} exited={} listed_wakeups={} exited_wakeups_at_start={} window_seconds={:.1}\n",
319 self.threads.len(),
320 self.threads.iter().filter(|t| t.started_in_window).count(),
321 self.exited.len(),
322 count(self.total_wakeups()),
323 count(self.exited_wakeups_at_start()),
324 secs,
325 );
326 let listed = self
327 .threads
328 .iter()
329 .filter(|t| t.wakeups != Some(0))
330 .take(top);
331 for (index, thread) in listed.enumerate() {
332 let per_second = match thread.wakeups {
333 Some(w) if secs > 0.0 => format!("{:.3}", w as f64 / secs),
334 Some(_) => "0.000".to_string(),
335 None => "unavailable".to_string(),
336 };
337 out.push_str(&format!(
338 "RESULT thread_wakeups rank={} tid={} wakeups={} per_second={per_second} cpu_ms={} born={} start={} name={}\n",
339 index + 1,
340 thread.thread_id,
341 count(thread.wakeups),
342 thread
343 .cpu_seconds
344 .map_or_else(|| "unavailable".to_string(), |s| format!("{:.1}", s * 1e3)),
345 u8::from(thread.started_in_window),
346 thread
347 .start_address
348 .map_or_else(|| "unavailable".to_string(), |a| format!("0x{a:x}")),
349 thread
350 .name
351 .as_ref()
352 .map_or_else(|| "unavailable".to_string(), |n| format!("{n:?}")),
353 ));
354 }
355 out
356 }
357}
358
359#[derive(Clone, Debug)]
361pub struct IdleThreadObservation {
362 pub observation: IdleObservation,
364 pub threads: ThreadDeltas,
365}
366
367pub fn observe_idle_with_threads(duration: Duration) -> io::Result<IdleThreadObservation> {
371 let (start, mut start_threads) = imp::sample_threads()?;
372 let began = std::time::Instant::now();
373 imp::enrich(&mut start_threads);
374 std::thread::sleep(duration.saturating_sub(began.elapsed()));
375 let (end, mut end_threads) = imp::sample_threads()?;
376 let elapsed = began.elapsed();
377 imp::enrich(&mut end_threads);
378 Ok(IdleThreadObservation {
379 observation: IdleObservation {
380 elapsed,
381 start,
382 end,
383 rates: end.rates_since(&start, elapsed),
384 },
385 threads: thread_deltas(&start_threads, &end_threads),
386 })
387}
388
389#[derive(Clone, Copy, Debug)]
391pub struct BudgetLabels<'a> {
392 pub krate: &'a str,
394 pub version: &'a str,
395 pub scale_unit: &'a str,
397 pub scale: u64,
399 pub what: &'a str,
401 pub scope: &'a str,
403}
404
405impl IdleObservation {
406 pub fn result_lines(&self, warm: &ProcessUsage, labels: &BudgetLabels<'_>) -> String {
421 let BudgetLabels {
422 krate,
423 version,
424 scale_unit,
425 scale,
426 what,
427 scope,
428 } = *labels;
429 let mut out = String::new();
430 let mut line = |text: String| {
431 out.push_str("RESULT ");
432 out.push_str(&text);
433 out.push('\n');
434 };
435 let resident_max = warm.resident_bytes.max(self.end.resident_bytes);
436 line(format!("warm_resident_bytes={} bytes", warm.resident_bytes));
437 line(format!(
438 "idle_resident_bytes={} bytes",
439 self.end.resident_bytes
440 ));
441 if resident_max > 0 {
442 line(format!("resident_memory_bytes={resident_max} bytes"));
443 } else {
444 line("resident_memory_bytes=unavailable host reported a zero resident set".to_string());
445 }
446 line(format!(
447 "footprint_bytes={} bytes",
448 warm.footprint_bytes.max(self.end.footprint_bytes)
449 ));
450 line(format!(
451 "idle_window_seconds={:.1} seconds",
452 self.elapsed.as_secs_f64()
453 ));
454 line(format!(
455 "idle_cpu_percent={:.3} percent",
456 self.rates.cpu_percent
457 ));
458 let opt = |name: &str,
459 value: Option<f64>,
460 unit: &str,
461 reason: &str,
462 line: &mut dyn FnMut(String)| match value {
463 Some(v) => line(format!("{name}={v:.3} {unit}")),
464 None => line(format!("{name}=unavailable {reason}")),
465 };
466 opt(
467 "wakeups_per_second",
468 self.rates.wakeups_per_second,
469 "per_second",
470 "the host exposes no process wake-up counter",
471 &mut line,
472 );
473 opt(
474 "disk_ops_per_second",
475 self.rates.disk_ops_per_second,
476 "per_second",
477 "the host exposes no process disk-operation counter",
478 &mut line,
479 );
480 opt(
481 "disk_read_bytes_per_second",
482 self.rates.disk_read_bytes_per_second,
483 "bytes_per_second",
484 "the host exposes no process disk-byte counter",
485 &mut line,
486 );
487 opt(
488 "disk_write_bytes_per_second",
489 self.rates.disk_write_bytes_per_second,
490 "bytes_per_second",
491 "the host exposes no process disk-byte counter",
492 &mut line,
493 );
494 line(format!("scale={scale} {what}"));
495 line(format!("scope={scope}"));
496 let num =
497 |v: Option<f64>| v.map_or_else(|| "unavailable".to_string(), |v| format!("{v:.3}"));
498 let resident = if resident_max > 0 {
499 resident_max.to_string()
500 } else {
501 "unavailable".to_string()
502 };
503 line(format!(
504 "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}",
505 self.rates.cpu_percent,
506 num(self.rates.wakeups_per_second),
507 num(self.rates.disk_ops_per_second),
508 ));
509 out
510 }
511}
512
513#[cfg(target_os = "macos")]
514mod imp {
515 use super::{ProcessUsage, ThreadSample};
516 use std::io;
517
518 extern "C" {
519 fn proc_pid_rusage(pid: i32, flavor: i32, buffer: *mut u8) -> i32;
520 }
521
522 const W_INTERRUPT_WKUPS: usize = 2 + 3;
525 const W_RESIDENT: usize = 2 + 6;
526 const W_FOOTPRINT: usize = 2 + 7;
527 const W_DISK_READ: usize = 2 + 16;
528 const W_DISK_WRITE: usize = 2 + 17;
529
530 pub fn sample() -> io::Result<ProcessUsage> {
531 let mut words = [0u64; 64];
532 let rc = unsafe { proc_pid_rusage(libc::getpid(), 2, words.as_mut_ptr().cast()) };
534 if rc != 0 {
535 return Err(io::Error::last_os_error());
536 }
537 let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
538 if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) } != 0 {
539 return Err(io::Error::last_os_error());
540 }
541 let secs = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 / 1e6;
542 Ok(ProcessUsage {
543 resident_bytes: words[W_RESIDENT],
544 footprint_bytes: words[W_FOOTPRINT],
545 cpu_seconds: secs(ru.ru_utime) + secs(ru.ru_stime),
546 wakeups_total: Some(words[W_INTERRUPT_WKUPS]),
547 disk_ops_total: None,
548 disk_read_bytes: Some(words[W_DISK_READ]),
549 disk_write_bytes: Some(words[W_DISK_WRITE]),
550 })
551 }
552
553 const PROC_PIDLISTTHREADS: i32 = 6;
556 const MAX_THREADS: usize = 1 << 16;
558
559 fn threads() -> io::Result<Vec<ThreadSample>> {
563 let pid = unsafe { libc::getpid() };
564 let mut capacity = 64usize;
565 let handles = loop {
566 let mut handles = vec![0u64; capacity];
567 let bytes = i32::try_from(capacity * std::mem::size_of::<u64>())
568 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "thread list too large"))?;
569 let written = unsafe {
571 libc::proc_pidinfo(
572 pid,
573 PROC_PIDLISTTHREADS,
574 0,
575 handles.as_mut_ptr().cast(),
576 bytes,
577 )
578 };
579 if written <= 0 {
580 return Err(io::Error::last_os_error());
581 }
582 let count = written as usize / std::mem::size_of::<u64>();
583 if count < capacity {
585 handles.truncate(count);
586 break handles;
587 }
588 capacity *= 2;
589 if capacity > MAX_THREADS {
590 return Err(io::Error::new(
591 io::ErrorKind::InvalidData,
592 "too many threads to list",
593 ));
594 }
595 };
596 let size = std::mem::size_of::<libc::proc_threadinfo>() as i32;
597 let mut threads = Vec::with_capacity(handles.len());
598 for handle in handles {
599 let mut info: libc::proc_threadinfo = unsafe { std::mem::zeroed() };
600 let read = unsafe {
602 libc::proc_pidinfo(
603 pid,
604 libc::PROC_PIDTHREADINFO,
605 handle,
606 (&mut info as *mut libc::proc_threadinfo).cast(),
607 size,
608 )
609 };
610 if read != size {
611 continue;
612 }
613 let bytes: Vec<u8> = info
614 .pth_name
615 .iter()
616 .map(|&c| c as u8)
617 .take_while(|&c| c != 0)
618 .collect();
619 let name = String::from_utf8_lossy(&bytes).into_owned();
620 threads.push(ThreadSample {
621 thread_id: handle,
622 started: None,
623 wakeups: None,
624 cpu_seconds: None,
625 name: (!name.is_empty()).then_some(name),
626 start_address: None,
627 });
628 }
629 Ok(threads)
630 }
631
632 pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
635 let usage = sample()?;
636 Ok((usage, threads()?))
637 }
638
639 pub fn enrich(_threads: &mut [ThreadSample]) {}
641}
642
643#[cfg(all(
644 unix,
645 not(target_os = "macos"),
646 not(any(target_os = "linux", target_os = "android"))
647))]
648mod imp {
649 use super::{ProcessUsage, ThreadSample};
650 use std::io;
651
652 pub fn sample() -> io::Result<ProcessUsage> {
655 Err(io::Error::new(
656 io::ErrorKind::Unsupported,
657 "process usage sampling is implemented for macOS, Linux, Android and Windows only",
658 ))
659 }
660
661 pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
662 sample().map(|usage| (usage, Vec::new()))
663 }
664
665 pub fn enrich(_threads: &mut [ThreadSample]) {}
666}
667
668#[cfg(any(target_os = "linux", target_os = "android"))]
669mod imp {
670 use super::{ProcessUsage, ThreadSample};
671 use std::io;
672
673 pub fn sample() -> io::Result<ProcessUsage> {
674 let statm = std::fs::read_to_string("/proc/self/statm")?;
675 let pages: u64 = statm
676 .split_whitespace()
677 .nth(1)
678 .and_then(|v| v.parse().ok())
679 .ok_or_else(|| {
680 io::Error::new(io::ErrorKind::InvalidData, "unreadable /proc/self/statm")
681 })?;
682 let page = u64::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(1))
683 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "page size out of range"))?;
684 let overflow = || io::Error::new(io::ErrorKind::InvalidData, "memory size overflows u64");
685 let resident = pages.checked_mul(page).ok_or_else(overflow)?;
686 let blocks = |count: libc::c_long| u64::try_from(count.max(0)).unwrap_or(0);
688 let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
689 if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) } != 0 {
690 return Err(io::Error::last_os_error());
691 }
692 let secs = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 / 1e6;
693 Ok(ProcessUsage {
694 resident_bytes: resident,
695 footprint_bytes: resident,
696 cpu_seconds: secs(ru.ru_utime) + secs(ru.ru_stime),
697 wakeups_total: None,
698 disk_ops_total: blocks(ru.ru_inblock).checked_add(blocks(ru.ru_oublock)),
699 disk_read_bytes: None,
700 disk_write_bytes: None,
701 })
702 }
703
704 fn threads() -> io::Result<Vec<ThreadSample>> {
708 let mut threads = Vec::new();
709 for entry in std::fs::read_dir("/proc/self/task")? {
710 let Ok(entry) = entry else { continue };
711 let Some(tid) = entry
712 .file_name()
713 .to_str()
714 .and_then(|n| n.parse::<u64>().ok())
715 else {
716 continue;
717 };
718 let Ok(comm) = std::fs::read_to_string(entry.path().join("comm")) else {
719 continue;
720 };
721 let name = comm.trim_end().to_string();
722 threads.push(ThreadSample {
723 thread_id: tid,
724 started: None,
725 wakeups: None,
726 cpu_seconds: None,
727 name: (!name.is_empty()).then_some(name),
728 start_address: None,
729 });
730 }
731 Ok(threads)
732 }
733
734 pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
735 let usage = sample()?;
736 Ok((usage, threads()?))
737 }
738
739 pub fn enrich(_threads: &mut [ThreadSample]) {}
741}
742
743#[cfg(windows)]
744mod imp {
745 use super::{ProcessUsage, ThreadSample};
746 use std::io;
747 use windows::Wdk::System::SystemInformation::{
748 NtQuerySystemInformation, SystemProcessInformation,
749 };
750 use windows::Win32::Foundation::{
751 CloseHandle, LocalFree, FILETIME, HANDLE, HLOCAL, STATUS_INFO_LENGTH_MISMATCH,
752 };
753 use windows::Win32::System::ProcessStatus::{
754 GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX,
755 };
756 use windows::Win32::System::Threading::{
757 GetCurrentProcess, GetCurrentProcessId, GetProcessIoCounters, GetProcessTimes,
758 GetThreadDescription, GetThreadTimes, OpenThread, IO_COUNTERS, THREAD_QUERY_INFORMATION,
759 THREAD_QUERY_LIMITED_INFORMATION,
760 };
761 use windows::Win32::System::WindowsProgramming::{
762 SYSTEM_PROCESS_INFORMATION, SYSTEM_THREAD_INFORMATION,
763 };
764
765 fn os(error: windows::core::Error) -> io::Error {
766 io::Error::from_raw_os_error(error.code().0 & 0xFFFF)
767 }
768
769 fn ticks(t: FILETIME) -> u64 {
770 ((t.dwHighDateTime as u64) << 32) | t.dwLowDateTime as u64
771 }
772
773 #[cfg(target_pointer_width = "64")]
777 const THREADS_OFFSET: usize = 0x100;
778 #[cfg(target_pointer_width = "64")]
780 const MAX_QUERY_BYTES: usize = 256 * 1024 * 1024;
781
782 #[cfg(target_pointer_width = "64")]
789 fn thread_table() -> Option<Vec<ThreadSample>> {
790 let mut words: Vec<u64> = vec![0; (1 << 20) / 8];
792 let mut length: u32 = 0;
793 let mut attempts = 0;
794 loop {
795 let capacity = words.len() * 8;
796 let status = unsafe {
797 NtQuerySystemInformation(
798 SystemProcessInformation,
799 words.as_mut_ptr().cast(),
800 u32::try_from(capacity).ok()?,
801 &mut length,
802 )
803 };
804 if status.0 >= 0 {
805 break;
806 }
807 attempts += 1;
808 if status != STATUS_INFO_LENGTH_MISMATCH || attempts > 8 {
809 return None;
810 }
811 let want = (length as usize).checked_add(256 * 1024)?;
813 if want <= capacity || want > MAX_QUERY_BYTES {
814 return None;
815 }
816 words = vec![0; want.div_ceil(8)];
817 }
818 let valid = (length as usize).min(words.len() * 8);
819 let base = words.as_ptr().cast::<u8>();
820 let pid = unsafe { GetCurrentProcessId() } as usize;
821 let thread_size = std::mem::size_of::<SYSTEM_THREAD_INFORMATION>();
822 let mut offset = 0usize;
823 loop {
824 if offset.checked_add(THREADS_OFFSET)? > valid {
825 return None;
826 }
827 let entry = unsafe {
829 std::ptr::read_unaligned(base.add(offset).cast::<SYSTEM_PROCESS_INFORMATION>())
830 };
831 if entry.UniqueProcessId.0 as usize == pid {
832 let count = entry.NumberOfThreads as usize;
833 let first = offset + THREADS_OFFSET;
834 let end = first.checked_add(count.checked_mul(thread_size)?)?;
835 if end > valid {
836 return None;
837 }
838 let mut threads = Vec::with_capacity(count);
839 for i in 0..count {
840 let thread = unsafe {
842 std::ptr::read_unaligned(
843 base.add(first + i * thread_size)
844 .cast::<SYSTEM_THREAD_INFORMATION>(),
845 )
846 };
847 if thread.ClientId.UniqueProcess.0 as usize != pid {
849 return None;
850 }
851 let [kernel, user, created] = thread.Reserved1;
852 threads.push(ThreadSample {
853 thread_id: thread.ClientId.UniqueThread.0 as usize as u64,
854 started: u64::try_from(created).ok(),
855 wakeups: Some(u64::from(thread.Reserved3)),
856 cpu_seconds: Some(
858 (kernel.max(0) as u64).saturating_add(user.max(0) as u64) as f64 / 1e7,
859 ),
860 name: None,
861 start_address: None,
862 });
863 }
864 return Some(threads);
865 }
866 let next = entry.NextEntryOffset as usize;
867 if next == 0 {
868 return None;
869 }
870 offset = offset.checked_add(next)?;
871 }
872 }
873
874 #[cfg(not(target_pointer_width = "64"))]
875 fn thread_table() -> Option<Vec<ThreadSample>> {
876 None
877 }
878
879 struct ThreadHandle(HANDLE);
881
882 impl Drop for ThreadHandle {
883 fn drop(&mut self) {
884 let _ = unsafe { CloseHandle(self.0) };
886 }
887 }
888
889 fn open_thread(id: u32) -> Option<ThreadHandle> {
894 let full = THREAD_QUERY_INFORMATION | THREAD_QUERY_LIMITED_INFORMATION;
895 let opened = unsafe { OpenThread(full, false, id) }
897 .or_else(|_| unsafe { OpenThread(THREAD_QUERY_LIMITED_INFORMATION, false, id) });
898 opened.ok().map(ThreadHandle)
899 }
900
901 fn thread_created(thread: HANDLE) -> Option<u64> {
903 let (mut created, mut exited, mut kernel, mut user) = (
904 FILETIME::default(),
905 FILETIME::default(),
906 FILETIME::default(),
907 FILETIME::default(),
908 );
909 let read =
911 unsafe { GetThreadTimes(thread, &mut created, &mut exited, &mut kernel, &mut user) };
912 read.ok()?;
913 Some(ticks(created))
914 }
915
916 fn thread_name(thread: HANDLE) -> Option<String> {
918 let description = unsafe { GetThreadDescription(thread) }.ok()?;
920 if description.is_null() {
921 return None;
922 }
923 let name = unsafe { description.to_string() }.ok();
925 unsafe { LocalFree(Some(HLOCAL(description.0.cast()))) };
927 name.filter(|name| !name.is_empty())
928 }
929
930 #[cfg(target_pointer_width = "64")]
934 const THREAD_QUERY_SET_WIN32_START_ADDRESS: u32 = 9;
935
936 #[cfg(target_pointer_width = "64")]
939 #[link(name = "ntdll")]
940 extern "system" {
941 fn NtQueryInformationThread(
942 thread: HANDLE,
943 class: u32,
944 information: *mut std::ffi::c_void,
945 length: u32,
946 returned: *mut u32,
947 ) -> i32;
948 }
949
950 #[cfg(target_pointer_width = "64")]
951 fn win32_start_address(thread: HANDLE) -> Option<u64> {
952 let mut address: usize = 0;
953 let status = unsafe {
955 NtQueryInformationThread(
956 thread,
957 THREAD_QUERY_SET_WIN32_START_ADDRESS,
958 (&mut address as *mut usize).cast(),
959 std::mem::size_of::<usize>() as u32,
960 std::ptr::null_mut(),
961 )
962 };
963 (status >= 0 && address != 0).then_some(address as u64)
964 }
965
966 #[cfg(not(target_pointer_width = "64"))]
967 fn win32_start_address(_thread: HANDLE) -> Option<u64> {
968 None
969 }
970
971 pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
974 let threads = thread_table().ok_or_else(|| io::Error::other("thread table unavailable"))?;
975 let usage = sample_with(super::sum_wakeups(&threads))?;
976 Ok((usage, threads))
977 }
978
979 pub fn enrich(threads: &mut [ThreadSample]) {
983 for thread in threads.iter_mut() {
984 let Some(handle) = u32::try_from(thread.thread_id).ok().and_then(open_thread) else {
985 continue;
986 };
987 if thread.started.is_some() && thread_created(handle.0) != thread.started {
988 continue;
989 }
990 thread.name = thread_name(handle.0);
991 thread.start_address = win32_start_address(handle.0);
992 }
993 }
994
995 pub fn sample() -> io::Result<ProcessUsage> {
996 let table = thread_table();
997 sample_with(table.as_deref().and_then(super::sum_wakeups))
998 }
999
1000 fn sample_with(wakeups_total: Option<u64>) -> io::Result<ProcessUsage> {
1001 unsafe {
1002 let process = GetCurrentProcess();
1003 let mut mem = PROCESS_MEMORY_COUNTERS_EX::default();
1004 GetProcessMemoryInfo(
1005 process,
1006 (&mut mem as *mut PROCESS_MEMORY_COUNTERS_EX).cast::<PROCESS_MEMORY_COUNTERS>(),
1007 std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32,
1008 )
1009 .map_err(os)?;
1010 let (mut created, mut exited, mut kernel, mut user) = (
1011 FILETIME::default(),
1012 FILETIME::default(),
1013 FILETIME::default(),
1014 FILETIME::default(),
1015 );
1016 GetProcessTimes(process, &mut created, &mut exited, &mut kernel, &mut user)
1017 .map_err(os)?;
1018 let mut io_counters = IO_COUNTERS::default();
1019 GetProcessIoCounters(process, &mut io_counters).map_err(os)?;
1020 Ok(ProcessUsage {
1021 resident_bytes: mem.WorkingSetSize as u64,
1022 footprint_bytes: mem.PrivateUsage as u64,
1023 cpu_seconds: (ticks(kernel) + ticks(user)) as f64 / 1e7,
1025 wakeups_total,
1026 disk_ops_total: Some(
1027 io_counters.ReadOperationCount + io_counters.WriteOperationCount,
1028 ),
1029 disk_read_bytes: Some(io_counters.ReadTransferCount),
1030 disk_write_bytes: Some(io_counters.WriteTransferCount),
1031 })
1032 }
1033 }
1034}