use windows_sys::Win32::Foundation::NTSTATUS;
use corescout_core::error::{Error, Result};
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct ProcessorPerformance {
pub idle_time: i64,
pub kernel_time: i64,
pub user_time: i64,
pub dpc_time: i64,
pub interrupt_time: i64,
pub interrupt_count: u32,
}
const SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION: i32 = 8;
#[link(name = "ntdll")]
extern "system" {
fn NtQuerySystemInformation(
class: i32,
buffer: *mut core::ffi::c_void,
length: u32,
returned: *mut u32,
) -> NTSTATUS;
}
pub fn read(cpu_count: usize) -> Result<Vec<ProcessorPerformance>> {
if cpu_count == 0 {
return Ok(Vec::new());
}
let mut buffer = vec![ProcessorPerformance::default(); cpu_count];
let bytes = std::mem::size_of_val(buffer.as_slice()) as u32;
let mut returned: u32 = 0;
let status = unsafe {
NtQuerySystemInformation(
SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION,
buffer.as_mut_ptr().cast(),
bytes,
&mut returned,
)
};
if status < 0 {
return Err(Error::syscall(
"NtQuerySystemInformation(SystemProcessorPerformanceInformation)",
status,
));
}
let filled = returned as usize / std::mem::size_of::<ProcessorPerformance>();
buffer.truncate(filled.min(cpu_count));
Ok(buffer)
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct CpuTimes {
pub idle_ns: u64,
pub kernel_ns: u64,
pub kernel_including_idle_ns: u64,
pub user_ns: u64,
pub dpc_ns: u64,
pub interrupt_ns: u64,
pub interrupt_count: u64,
}
impl CpuTimes {
pub fn busy_ns(&self) -> u64 {
self.kernel_ns.saturating_add(self.user_ns)
}
pub fn total_ns(&self) -> u64 {
self.kernel_including_idle_ns.saturating_add(self.user_ns)
}
}
pub fn interpret(raw: &ProcessorPerformance) -> CpuTimes {
let ticks_to_ns = |ticks: i64| -> u64 { (ticks.max(0) as u64).saturating_mul(100) };
let idle_ns = ticks_to_ns(raw.idle_time);
let kernel_including_idle = ticks_to_ns(raw.kernel_time);
CpuTimes {
idle_ns,
kernel_ns: kernel_including_idle.saturating_sub(idle_ns),
kernel_including_idle_ns: kernel_including_idle,
user_ns: ticks_to_ns(raw.user_time),
dpc_ns: ticks_to_ns(raw.dpc_time),
interrupt_ns: ticks_to_ns(raw.interrupt_time),
interrupt_count: raw.interrupt_count as u64,
}
}
pub fn sample(cpu_count: usize) -> Result<Vec<CpuTimes>> {
Ok(read(cpu_count)?.iter().map(interpret).collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn cpu_count() -> usize {
super::super::topology::discover()
.map(|t| t.logical_cpus.len())
.unwrap_or(1)
}
#[test]
fn this_machine_reports_per_cpu_times() {
let times = sample(cpu_count()).expect("Windows reports processor times");
assert!(!times.is_empty());
for cpu in × {
assert!(
cpu.total_ns() > 0,
"a CPU reported no accounted time at all"
);
}
}
#[test]
fn kernel_time_has_idle_subtracted_out() {
let raw = ProcessorPerformance {
idle_time: 900,
kernel_time: 1000,
user_time: 50,
..ProcessorPerformance::default()
};
let times = interpret(&raw);
assert_eq!(times.idle_ns, 90_000);
assert_eq!(times.kernel_ns, 10_000, "kernel must exclude idle");
assert_eq!(times.busy_ns(), 15_000);
assert_eq!(times.total_ns(), 105_000);
}
#[test]
fn an_idle_machine_is_mostly_idle() {
let times = sample(cpu_count()).expect("times");
let idle: u64 = times.iter().map(|c| c.idle_ns).sum();
let total: u64 = times.iter().map(|c| c.total_ns()).sum();
assert!(total > 0);
let idle_share = idle as f64 / total as f64;
assert!(
idle_share > 0.20,
"this machine claims to be {:.0}% busy since boot, which almost \
certainly means idle is not being subtracted from kernel time",
(1.0 - idle_share) * 100.0
);
}
#[test]
fn counters_only_go_up() {
let first = sample(cpu_count()).expect("times");
std::thread::sleep(std::time::Duration::from_millis(60));
let second = sample(cpu_count()).expect("times");
assert_eq!(first.len(), second.len());
for (a, b) in first.iter().zip(second.iter()) {
assert!(b.idle_ns >= a.idle_ns, "idle went backwards");
assert!(b.user_ns >= a.user_ns, "user went backwards");
assert!(
b.kernel_including_idle_ns >= a.kernel_including_idle_ns,
"raw kernel time went backwards"
);
assert!(b.total_ns() >= a.total_ns(), "total went backwards");
}
}
#[test]
fn time_actually_passes_between_samples() {
let first = sample(cpu_count()).expect("times");
std::thread::sleep(std::time::Duration::from_millis(120));
let second = sample(cpu_count()).expect("times");
let before: u64 = first.iter().map(|c| c.total_ns()).sum();
let after: u64 = second.iter().map(|c| c.total_ns()).sum();
assert!(
after > before,
"no time was accounted across a 120 ms sleep; the sensor is not live"
);
}
#[test]
fn asking_for_no_cpus_is_not_an_error() {
assert!(read(0).expect("empty is fine").is_empty());
}
}