use std::io;
use std::time::Duration;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ProcessUsage {
pub resident_bytes: u64,
pub footprint_bytes: u64,
pub cpu_seconds: f64,
pub wakeups_total: Option<u64>,
pub disk_ops_total: Option<u64>,
pub disk_read_bytes: Option<u64>,
pub disk_write_bytes: Option<u64>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct UsageRates {
pub cpu_percent: f64,
pub wakeups_per_second: Option<f64>,
pub disk_ops_per_second: Option<f64>,
pub disk_read_bytes_per_second: Option<f64>,
pub disk_write_bytes_per_second: Option<f64>,
}
impl ProcessUsage {
pub fn rates_since(&self, earlier: &ProcessUsage, elapsed: Duration) -> UsageRates {
let secs = elapsed.as_secs_f64();
let rate = |now: Option<u64>, then: Option<u64>| match (now, then) {
(Some(now), Some(then)) if secs > 0.0 => Some(now.saturating_sub(then) as f64 / secs),
(Some(_), Some(_)) => Some(0.0),
_ => None,
};
UsageRates {
cpu_percent: if secs > 0.0 {
(self.cpu_seconds - earlier.cpu_seconds).max(0.0) / secs * 100.0
} else {
0.0
},
wakeups_per_second: rate(self.wakeups_total, earlier.wakeups_total),
disk_ops_per_second: rate(self.disk_ops_total, earlier.disk_ops_total),
disk_read_bytes_per_second: rate(self.disk_read_bytes, earlier.disk_read_bytes),
disk_write_bytes_per_second: rate(self.disk_write_bytes, earlier.disk_write_bytes),
}
}
}
pub fn process_usage() -> io::Result<ProcessUsage> {
imp::sample()
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct TreeUsage {
pub processes: usize,
pub resident_bytes: u64,
pub unreadable: usize,
}
pub fn tree_usage(root: &crate::AdoptedIdentity) -> io::Result<TreeUsage> {
if root.verify()? != crate::IdentityCheck::Same {
return Ok(TreeUsage::default());
}
let mut members = vec![root.clone()];
members.extend(root.descendants()?);
let table = crate::process_table()?;
let mut usage = TreeUsage::default();
for member in &members {
if member.verify()? != crate::IdentityCheck::Same {
continue;
}
usage.processes += 1;
match table
.iter()
.find(|entry| entry.pid == member.pid())
.and_then(|entry| entry.working_set_bytes)
{
Some(bytes) => usage.resident_bytes = usage.resident_bytes.saturating_add(bytes),
None => usage.unreadable += 1,
}
}
Ok(usage)
}
#[derive(Clone, Copy, Debug)]
pub struct IdleObservation {
pub elapsed: Duration,
pub start: ProcessUsage,
pub end: ProcessUsage,
pub rates: UsageRates,
}
pub fn observe_idle(duration: Duration) -> io::Result<IdleObservation> {
let start = process_usage()?;
let began = std::time::Instant::now();
std::thread::sleep(duration);
let end = process_usage()?;
let elapsed = began.elapsed();
Ok(IdleObservation {
elapsed,
start,
end,
rates: end.rates_since(&start, elapsed),
})
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct ThreadSample {
pub thread_id: u64,
pub started: Option<u64>,
pub wakeups: Option<u64>,
pub cpu_seconds: Option<f64>,
pub name: Option<String>,
pub start_address: Option<u64>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProcessThreads {
pub usage: ProcessUsage,
pub threads: Vec<ThreadSample>,
}
impl ProcessThreads {
pub fn thread_wakeups_sum(&self) -> Option<u64> {
sum_wakeups(&self.threads)
}
}
fn sum_wakeups(threads: &[ThreadSample]) -> Option<u64> {
threads
.iter()
.try_fold(0u64, |total, thread| total.checked_add(thread.wakeups?))
}
pub fn process_usage_with_threads() -> io::Result<ProcessThreads> {
let (usage, mut threads) = imp::sample_threads()?;
imp::enrich(&mut threads);
Ok(ProcessThreads { usage, threads })
}
#[derive(Clone, Debug, PartialEq)]
pub struct ThreadDelta {
pub thread_id: u64,
pub wakeups: Option<u64>,
pub cpu_seconds: Option<f64>,
pub name: Option<String>,
pub start_address: Option<u64>,
pub started_in_window: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ThreadDeltas {
pub threads: Vec<ThreadDelta>,
pub exited: Vec<ThreadSample>,
}
pub fn thread_deltas(start: &[ThreadSample], end: &[ThreadSample]) -> ThreadDeltas {
use std::collections::HashMap;
let key = |thread: &ThreadSample| (thread.thread_id, thread.started);
let mut earlier: HashMap<(u64, Option<u64>), &ThreadSample> =
start.iter().map(|thread| (key(thread), thread)).collect();
let mut threads: Vec<ThreadDelta> = end
.iter()
.map(|now| {
let before = earlier.remove(&key(now));
ThreadDelta {
thread_id: now.thread_id,
wakeups: match before {
Some(then) => counter_delta(now.wakeups, then.wakeups),
None => now.wakeups,
},
cpu_seconds: match before {
Some(then) => match (now.cpu_seconds, then.cpu_seconds) {
(Some(now), Some(then)) => Some((now - then).max(0.0)),
_ => None,
},
None => now.cpu_seconds,
},
name: now
.name
.clone()
.or_else(|| before.and_then(|then| then.name.clone())),
start_address: now
.start_address
.or_else(|| before.and_then(|then| then.start_address)),
started_in_window: before.is_none(),
}
})
.collect();
threads.sort_by(|a, b| {
b.wakeups
.cmp(&a.wakeups)
.then(a.thread_id.cmp(&b.thread_id))
});
let mut exited: Vec<ThreadSample> = earlier.into_values().cloned().collect();
exited.sort_by_key(|thread| thread.thread_id);
ThreadDeltas { threads, exited }
}
fn counter_delta(now: Option<u64>, then: Option<u64>) -> Option<u64> {
Some(now?.saturating_sub(then?))
}
impl ThreadDeltas {
pub fn total_wakeups(&self) -> Option<u64> {
self.threads
.iter()
.try_fold(0u64, |total, thread| total.checked_add(thread.wakeups?))
}
pub fn exited_wakeups_at_start(&self) -> Option<u64> {
sum_wakeups(&self.exited)
}
pub fn top(&self, n: usize) -> &[ThreadDelta] {
&self.threads[..n.min(self.threads.len())]
}
pub fn result_lines(&self, elapsed: Duration, top: usize) -> String {
let secs = elapsed.as_secs_f64();
let count = |v: Option<u64>| v.map_or_else(|| "unavailable".to_string(), |v| v.to_string());
let mut out = format!(
"RESULT thread_wakeups_summary threads={} born={} exited={} listed_wakeups={} exited_wakeups_at_start={} window_seconds={:.1}\n",
self.threads.len(),
self.threads.iter().filter(|t| t.started_in_window).count(),
self.exited.len(),
count(self.total_wakeups()),
count(self.exited_wakeups_at_start()),
secs,
);
let listed = self
.threads
.iter()
.filter(|t| t.wakeups != Some(0))
.take(top);
for (index, thread) in listed.enumerate() {
let per_second = match thread.wakeups {
Some(w) if secs > 0.0 => format!("{:.3}", w as f64 / secs),
Some(_) => "0.000".to_string(),
None => "unavailable".to_string(),
};
out.push_str(&format!(
"RESULT thread_wakeups rank={} tid={} wakeups={} per_second={per_second} cpu_ms={} born={} start={} name={}\n",
index + 1,
thread.thread_id,
count(thread.wakeups),
thread
.cpu_seconds
.map_or_else(|| "unavailable".to_string(), |s| format!("{:.1}", s * 1e3)),
u8::from(thread.started_in_window),
thread
.start_address
.map_or_else(|| "unavailable".to_string(), |a| format!("0x{a:x}")),
thread
.name
.as_ref()
.map_or_else(|| "unavailable".to_string(), |n| format!("{n:?}")),
));
}
out
}
}
#[derive(Clone, Debug)]
pub struct IdleThreadObservation {
pub observation: IdleObservation,
pub threads: ThreadDeltas,
}
pub fn observe_idle_with_threads(duration: Duration) -> io::Result<IdleThreadObservation> {
let (start, mut start_threads) = imp::sample_threads()?;
let began = std::time::Instant::now();
imp::enrich(&mut start_threads);
std::thread::sleep(duration.saturating_sub(began.elapsed()));
let (end, mut end_threads) = imp::sample_threads()?;
let elapsed = began.elapsed();
imp::enrich(&mut end_threads);
Ok(IdleThreadObservation {
observation: IdleObservation {
elapsed,
start,
end,
rates: end.rates_since(&start, elapsed),
},
threads: thread_deltas(&start_threads, &end_threads),
})
}
#[derive(Clone, Copy, Debug)]
pub struct BudgetLabels<'a> {
pub krate: &'a str,
pub version: &'a str,
pub scale_unit: &'a str,
pub scale: u64,
pub what: &'a str,
pub scope: &'a str,
}
impl IdleObservation {
pub fn result_lines(&self, warm: &ProcessUsage, labels: &BudgetLabels<'_>) -> String {
let BudgetLabels {
krate,
version,
scale_unit,
scale,
what,
scope,
} = *labels;
let mut out = String::new();
let mut line = |text: String| {
out.push_str("RESULT ");
out.push_str(&text);
out.push('\n');
};
let resident_max = warm.resident_bytes.max(self.end.resident_bytes);
line(format!("warm_resident_bytes={} bytes", warm.resident_bytes));
line(format!(
"idle_resident_bytes={} bytes",
self.end.resident_bytes
));
if resident_max > 0 {
line(format!("resident_memory_bytes={resident_max} bytes"));
} else {
line("resident_memory_bytes=unavailable host reported a zero resident set".to_string());
}
line(format!(
"footprint_bytes={} bytes",
warm.footprint_bytes.max(self.end.footprint_bytes)
));
line(format!(
"idle_window_seconds={:.1} seconds",
self.elapsed.as_secs_f64()
));
line(format!(
"idle_cpu_percent={:.3} percent",
self.rates.cpu_percent
));
let opt = |name: &str,
value: Option<f64>,
unit: &str,
reason: &str,
line: &mut dyn FnMut(String)| match value {
Some(v) => line(format!("{name}={v:.3} {unit}")),
None => line(format!("{name}=unavailable {reason}")),
};
opt(
"wakeups_per_second",
self.rates.wakeups_per_second,
"per_second",
"the host exposes no process wake-up counter",
&mut line,
);
opt(
"disk_ops_per_second",
self.rates.disk_ops_per_second,
"per_second",
"the host exposes no process disk-operation counter",
&mut line,
);
opt(
"disk_read_bytes_per_second",
self.rates.disk_read_bytes_per_second,
"bytes_per_second",
"the host exposes no process disk-byte counter",
&mut line,
);
opt(
"disk_write_bytes_per_second",
self.rates.disk_write_bytes_per_second,
"bytes_per_second",
"the host exposes no process disk-byte counter",
&mut line,
);
line(format!("scale={scale} {what}"));
line(format!("scope={scope}"));
let num =
|v: Option<f64>| v.map_or_else(|| "unavailable".to_string(), |v| format!("{v:.3}"));
let resident = if resident_max > 0 {
resident_max.to_string()
} else {
"unavailable".to_string()
};
line(format!(
"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}",
self.rates.cpu_percent,
num(self.rates.wakeups_per_second),
num(self.rates.disk_ops_per_second),
));
out
}
}
#[cfg(target_os = "macos")]
mod imp {
use super::{ProcessUsage, ThreadSample};
use std::io;
extern "C" {
fn proc_pid_rusage(pid: i32, flavor: i32, buffer: *mut u8) -> i32;
}
const W_INTERRUPT_WKUPS: usize = 2 + 3;
const W_RESIDENT: usize = 2 + 6;
const W_FOOTPRINT: usize = 2 + 7;
const W_DISK_READ: usize = 2 + 16;
const W_DISK_WRITE: usize = 2 + 17;
pub fn sample() -> io::Result<ProcessUsage> {
let mut words = [0u64; 64];
let rc = unsafe { proc_pid_rusage(libc::getpid(), 2, words.as_mut_ptr().cast()) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) } != 0 {
return Err(io::Error::last_os_error());
}
let secs = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 / 1e6;
Ok(ProcessUsage {
resident_bytes: words[W_RESIDENT],
footprint_bytes: words[W_FOOTPRINT],
cpu_seconds: secs(ru.ru_utime) + secs(ru.ru_stime),
wakeups_total: Some(words[W_INTERRUPT_WKUPS]),
disk_ops_total: None,
disk_read_bytes: Some(words[W_DISK_READ]),
disk_write_bytes: Some(words[W_DISK_WRITE]),
})
}
const PROC_PIDLISTTHREADS: i32 = 6;
const MAX_THREADS: usize = 1 << 16;
fn threads() -> io::Result<Vec<ThreadSample>> {
let pid = unsafe { libc::getpid() };
let mut capacity = 64usize;
let handles = loop {
let mut handles = vec![0u64; capacity];
let bytes = i32::try_from(capacity * std::mem::size_of::<u64>())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "thread list too large"))?;
let written = unsafe {
libc::proc_pidinfo(
pid,
PROC_PIDLISTTHREADS,
0,
handles.as_mut_ptr().cast(),
bytes,
)
};
if written <= 0 {
return Err(io::Error::last_os_error());
}
let count = written as usize / std::mem::size_of::<u64>();
if count < capacity {
handles.truncate(count);
break handles;
}
capacity *= 2;
if capacity > MAX_THREADS {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"too many threads to list",
));
}
};
let size = std::mem::size_of::<libc::proc_threadinfo>() as i32;
let mut threads = Vec::with_capacity(handles.len());
for handle in handles {
let mut info: libc::proc_threadinfo = unsafe { std::mem::zeroed() };
let read = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDTHREADINFO,
handle,
(&mut info as *mut libc::proc_threadinfo).cast(),
size,
)
};
if read != size {
continue;
}
let bytes: Vec<u8> = info
.pth_name
.iter()
.map(|&c| c as u8)
.take_while(|&c| c != 0)
.collect();
let name = String::from_utf8_lossy(&bytes).into_owned();
threads.push(ThreadSample {
thread_id: handle,
started: None,
wakeups: None,
cpu_seconds: None,
name: (!name.is_empty()).then_some(name),
start_address: None,
});
}
Ok(threads)
}
pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
let usage = sample()?;
Ok((usage, threads()?))
}
pub fn enrich(_threads: &mut [ThreadSample]) {}
}
#[cfg(all(
unix,
not(target_os = "macos"),
not(any(target_os = "linux", target_os = "android"))
))]
mod imp {
use super::{ProcessUsage, ThreadSample};
use std::io;
pub fn sample() -> io::Result<ProcessUsage> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"process usage sampling is implemented for macOS, Linux, Android and Windows only",
))
}
pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
sample().map(|usage| (usage, Vec::new()))
}
pub fn enrich(_threads: &mut [ThreadSample]) {}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
mod imp {
use super::{ProcessUsage, ThreadSample};
use std::io;
pub fn sample() -> io::Result<ProcessUsage> {
let statm = std::fs::read_to_string("/proc/self/statm")?;
let pages: u64 = statm
.split_whitespace()
.nth(1)
.and_then(|v| v.parse().ok())
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "unreadable /proc/self/statm")
})?;
let page = u64::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(1))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "page size out of range"))?;
let overflow = || io::Error::new(io::ErrorKind::InvalidData, "memory size overflows u64");
let resident = pages.checked_mul(page).ok_or_else(overflow)?;
let blocks = |count: libc::c_long| u64::try_from(count.max(0)).unwrap_or(0);
let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) } != 0 {
return Err(io::Error::last_os_error());
}
let secs = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 / 1e6;
Ok(ProcessUsage {
resident_bytes: resident,
footprint_bytes: resident,
cpu_seconds: secs(ru.ru_utime) + secs(ru.ru_stime),
wakeups_total: None,
disk_ops_total: blocks(ru.ru_inblock).checked_add(blocks(ru.ru_oublock)),
disk_read_bytes: None,
disk_write_bytes: None,
})
}
fn threads() -> io::Result<Vec<ThreadSample>> {
let mut threads = Vec::new();
for entry in std::fs::read_dir("/proc/self/task")? {
let Ok(entry) = entry else { continue };
let Some(tid) = entry
.file_name()
.to_str()
.and_then(|n| n.parse::<u64>().ok())
else {
continue;
};
let Ok(comm) = std::fs::read_to_string(entry.path().join("comm")) else {
continue;
};
let name = comm.trim_end().to_string();
threads.push(ThreadSample {
thread_id: tid,
started: None,
wakeups: None,
cpu_seconds: None,
name: (!name.is_empty()).then_some(name),
start_address: None,
});
}
Ok(threads)
}
pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
let usage = sample()?;
Ok((usage, threads()?))
}
pub fn enrich(_threads: &mut [ThreadSample]) {}
}
#[cfg(windows)]
mod imp {
use super::{ProcessUsage, ThreadSample};
use std::io;
use windows::Wdk::System::SystemInformation::{
NtQuerySystemInformation, SystemProcessInformation,
};
use windows::Win32::Foundation::{
CloseHandle, LocalFree, FILETIME, HANDLE, HLOCAL, STATUS_INFO_LENGTH_MISMATCH,
};
use windows::Win32::System::ProcessStatus::{
GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX,
};
use windows::Win32::System::Threading::{
GetCurrentProcess, GetCurrentProcessId, GetProcessIoCounters, GetProcessTimes,
GetThreadDescription, GetThreadTimes, OpenThread, IO_COUNTERS, THREAD_QUERY_INFORMATION,
THREAD_QUERY_LIMITED_INFORMATION,
};
use windows::Win32::System::WindowsProgramming::{
SYSTEM_PROCESS_INFORMATION, SYSTEM_THREAD_INFORMATION,
};
fn os(error: windows::core::Error) -> io::Error {
io::Error::from_raw_os_error(error.code().0 & 0xFFFF)
}
fn ticks(t: FILETIME) -> u64 {
((t.dwHighDateTime as u64) << 32) | t.dwLowDateTime as u64
}
#[cfg(target_pointer_width = "64")]
const THREADS_OFFSET: usize = 0x100;
#[cfg(target_pointer_width = "64")]
const MAX_QUERY_BYTES: usize = 256 * 1024 * 1024;
#[cfg(target_pointer_width = "64")]
fn thread_table() -> Option<Vec<ThreadSample>> {
let mut words: Vec<u64> = vec![0; (1 << 20) / 8];
let mut length: u32 = 0;
let mut attempts = 0;
loop {
let capacity = words.len() * 8;
let status = unsafe {
NtQuerySystemInformation(
SystemProcessInformation,
words.as_mut_ptr().cast(),
u32::try_from(capacity).ok()?,
&mut length,
)
};
if status.0 >= 0 {
break;
}
attempts += 1;
if status != STATUS_INFO_LENGTH_MISMATCH || attempts > 8 {
return None;
}
let want = (length as usize).checked_add(256 * 1024)?;
if want <= capacity || want > MAX_QUERY_BYTES {
return None;
}
words = vec![0; want.div_ceil(8)];
}
let valid = (length as usize).min(words.len() * 8);
let base = words.as_ptr().cast::<u8>();
let pid = unsafe { GetCurrentProcessId() } as usize;
let thread_size = std::mem::size_of::<SYSTEM_THREAD_INFORMATION>();
let mut offset = 0usize;
loop {
if offset.checked_add(THREADS_OFFSET)? > valid {
return None;
}
let entry = unsafe {
std::ptr::read_unaligned(base.add(offset).cast::<SYSTEM_PROCESS_INFORMATION>())
};
if entry.UniqueProcessId.0 as usize == pid {
let count = entry.NumberOfThreads as usize;
let first = offset + THREADS_OFFSET;
let end = first.checked_add(count.checked_mul(thread_size)?)?;
if end > valid {
return None;
}
let mut threads = Vec::with_capacity(count);
for i in 0..count {
let thread = unsafe {
std::ptr::read_unaligned(
base.add(first + i * thread_size)
.cast::<SYSTEM_THREAD_INFORMATION>(),
)
};
if thread.ClientId.UniqueProcess.0 as usize != pid {
return None;
}
let [kernel, user, created] = thread.Reserved1;
threads.push(ThreadSample {
thread_id: thread.ClientId.UniqueThread.0 as usize as u64,
started: u64::try_from(created).ok(),
wakeups: Some(u64::from(thread.Reserved3)),
cpu_seconds: Some(
(kernel.max(0) as u64).saturating_add(user.max(0) as u64) as f64 / 1e7,
),
name: None,
start_address: None,
});
}
return Some(threads);
}
let next = entry.NextEntryOffset as usize;
if next == 0 {
return None;
}
offset = offset.checked_add(next)?;
}
}
#[cfg(not(target_pointer_width = "64"))]
fn thread_table() -> Option<Vec<ThreadSample>> {
None
}
struct ThreadHandle(HANDLE);
impl Drop for ThreadHandle {
fn drop(&mut self) {
let _ = unsafe { CloseHandle(self.0) };
}
}
fn open_thread(id: u32) -> Option<ThreadHandle> {
let full = THREAD_QUERY_INFORMATION | THREAD_QUERY_LIMITED_INFORMATION;
let opened = unsafe { OpenThread(full, false, id) }
.or_else(|_| unsafe { OpenThread(THREAD_QUERY_LIMITED_INFORMATION, false, id) });
opened.ok().map(ThreadHandle)
}
fn thread_created(thread: HANDLE) -> Option<u64> {
let (mut created, mut exited, mut kernel, mut user) = (
FILETIME::default(),
FILETIME::default(),
FILETIME::default(),
FILETIME::default(),
);
let read =
unsafe { GetThreadTimes(thread, &mut created, &mut exited, &mut kernel, &mut user) };
read.ok()?;
Some(ticks(created))
}
fn thread_name(thread: HANDLE) -> Option<String> {
let description = unsafe { GetThreadDescription(thread) }.ok()?;
if description.is_null() {
return None;
}
let name = unsafe { description.to_string() }.ok();
unsafe { LocalFree(Some(HLOCAL(description.0.cast()))) };
name.filter(|name| !name.is_empty())
}
#[cfg(target_pointer_width = "64")]
const THREAD_QUERY_SET_WIN32_START_ADDRESS: u32 = 9;
#[cfg(target_pointer_width = "64")]
#[link(name = "ntdll")]
extern "system" {
fn NtQueryInformationThread(
thread: HANDLE,
class: u32,
information: *mut std::ffi::c_void,
length: u32,
returned: *mut u32,
) -> i32;
}
#[cfg(target_pointer_width = "64")]
fn win32_start_address(thread: HANDLE) -> Option<u64> {
let mut address: usize = 0;
let status = unsafe {
NtQueryInformationThread(
thread,
THREAD_QUERY_SET_WIN32_START_ADDRESS,
(&mut address as *mut usize).cast(),
std::mem::size_of::<usize>() as u32,
std::ptr::null_mut(),
)
};
(status >= 0 && address != 0).then_some(address as u64)
}
#[cfg(not(target_pointer_width = "64"))]
fn win32_start_address(_thread: HANDLE) -> Option<u64> {
None
}
pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
let threads = thread_table().ok_or_else(|| io::Error::other("thread table unavailable"))?;
let usage = sample_with(super::sum_wakeups(&threads))?;
Ok((usage, threads))
}
pub fn enrich(threads: &mut [ThreadSample]) {
for thread in threads.iter_mut() {
let Some(handle) = u32::try_from(thread.thread_id).ok().and_then(open_thread) else {
continue;
};
if thread.started.is_some() && thread_created(handle.0) != thread.started {
continue;
}
thread.name = thread_name(handle.0);
thread.start_address = win32_start_address(handle.0);
}
}
pub fn sample() -> io::Result<ProcessUsage> {
let table = thread_table();
sample_with(table.as_deref().and_then(super::sum_wakeups))
}
fn sample_with(wakeups_total: Option<u64>) -> io::Result<ProcessUsage> {
unsafe {
let process = GetCurrentProcess();
let mut mem = PROCESS_MEMORY_COUNTERS_EX::default();
GetProcessMemoryInfo(
process,
(&mut mem as *mut PROCESS_MEMORY_COUNTERS_EX).cast::<PROCESS_MEMORY_COUNTERS>(),
std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32,
)
.map_err(os)?;
let (mut created, mut exited, mut kernel, mut user) = (
FILETIME::default(),
FILETIME::default(),
FILETIME::default(),
FILETIME::default(),
);
GetProcessTimes(process, &mut created, &mut exited, &mut kernel, &mut user)
.map_err(os)?;
let mut io_counters = IO_COUNTERS::default();
GetProcessIoCounters(process, &mut io_counters).map_err(os)?;
Ok(ProcessUsage {
resident_bytes: mem.WorkingSetSize as u64,
footprint_bytes: mem.PrivateUsage as u64,
cpu_seconds: (ticks(kernel) + ticks(user)) as f64 / 1e7,
wakeups_total,
disk_ops_total: Some(
io_counters.ReadOperationCount + io_counters.WriteOperationCount,
),
disk_read_bytes: Some(io_counters.ReadTransferCount),
disk_write_bytes: Some(io_counters.WriteTransferCount),
})
}
}
}