use crate::config::AppMatch;
use crate::platform::cpu_usage::AppMonitor;
use crate::platform::gpu::VendorGpu;
use crate::sensor::{
AppCpuUtilization, CpuUtilization, Platform, PowerSensor, ProcessCpuUtilization,
};
use std::time::Duration;
#[derive(Debug, Default)]
pub(crate) struct WindowsPlatform;
impl Platform for WindowsPlatform {
fn cpu(&self) -> Box<dyn PowerSensor> {
Box::new(rapl::RaplSensor::new(rapl::RaplDriver::open()))
}
fn gpu(&self) -> Box<dyn PowerSensor> {
Box::new(VendorGpu)
}
fn cpu_usage(&self) -> Box<dyn CpuUtilization> {
Box::new(cpu::cpu_usage())
}
fn process_cpu_usage(&self) -> Option<Box<dyn ProcessCpuUtilization>> {
Some(cpu::process_tracker())
}
fn app_cpu_usage(
&self,
refresh_interval: Duration,
app_match: AppMatch,
) -> Option<Box<dyn AppCpuUtilization>> {
Some(AppMonitor::boxed(
cpu::process_tracker,
refresh_interval,
app_match,
))
}
}
mod rapl {
use crate::platform::counter::{Interval, WrappingCounter, repeat_failure, warn_unavailable};
use crate::sensor::PowerSensor;
use crate::{Error, Result};
use std::mem;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Storage::FileSystem::{
CreateFileW, FILE_FLAG_OVERLAPPED, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ,
FILE_SHARE_WRITE, OPEN_EXISTING,
};
use windows::Win32::System::IO::DeviceIoControl;
use windows::core::w;
pub(crate) const SENSOR: &str = "Scaphandre RAPL driver";
const MSR_INTEL_RAPL_POWER_UNIT: u64 = 0x606;
const MSR_INTEL_PKG_ENERGY_STATUS: u64 = 0x611;
const MSR_AMD_RAPL_POWER_UNIT: u64 = 0xc001_0299;
const MSR_AMD_PKG_ENERGY_STATUS: u64 = 0xc001_029b;
const FILE_DEVICE_UNKNOWN: u32 = 0x0000_0022;
const METHOD_BUFFERED: u32 = 0;
const FILE_READ_DATA: u32 = 0x0001;
const FILE_WRITE_DATA: u32 = 0x0002;
const ENERGY_COUNTER_MODULUS: f64 = 4_294_967_296.0;
const fn ctl_code(device_type: u32, function: u32, method: u32, access: u32) -> u32 {
(device_type << 16) | (access << 14) | (function << 2) | method
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CpuVendor {
Intel,
Amd,
}
impl CpuVendor {
fn power_unit_msr(self) -> u64 {
match self {
CpuVendor::Intel => MSR_INTEL_RAPL_POWER_UNIT,
CpuVendor::Amd => MSR_AMD_RAPL_POWER_UNIT,
}
}
fn package_energy_msr(self) -> u64 {
match self {
CpuVendor::Intel => MSR_INTEL_PKG_ENERGY_STATUS,
CpuVendor::Amd => MSR_AMD_PKG_ENERGY_STATUS,
}
}
}
pub(crate) struct RaplDriver {
handle: HANDLE,
vendor: CpuVendor,
energy_unit: f64,
counter: WrappingCounter,
}
unsafe impl Send for RaplDriver {}
impl RaplDriver {
pub(crate) fn open() -> Result<Self> {
let handle = unsafe {
CreateFileW(
w!(r"\\.\ScaphandreDriver"),
FILE_GENERIC_READ.0 | FILE_GENERIC_WRITE.0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
None,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
None,
)
}
.map_err(|e| {
Error::sensor(
SENSOR,
format!("cannot open \\\\.\\ScaphandreDriver ({e}); is the driver installed?"),
)
})?;
let mut driver = Self {
handle,
vendor: CpuVendor::Intel,
energy_unit: 0.0,
counter: WrappingCounter::new(0.0, 0.0),
};
driver.vendor = driver.detect_vendor()?;
driver.energy_unit = driver.read_energy_unit()?;
if driver.read_msr(driver.vendor.package_energy_msr())? == 0 {
return Err(Error::sensor(
SENSOR,
"the CPU package energy counter reads zero",
));
}
driver.counter = WrappingCounter::new(
ENERGY_COUNTER_MODULUS * driver.energy_unit,
driver.read_energy()?,
);
Ok(driver)
}
fn read_msr(&self, msr: u64) -> Result<u64> {
let mut reply: u64 = 0;
let mut bytes_returned: u32 = 0;
let control_code = ctl_code(
FILE_DEVICE_UNKNOWN,
(msr & 0xFFF) as u32,
METHOD_BUFFERED,
FILE_READ_DATA | FILE_WRITE_DATA,
);
unsafe {
DeviceIoControl(
self.handle,
control_code,
Some(std::ptr::from_ref(&msr).cast()),
mem::size_of::<u64>() as u32,
Some(std::ptr::from_mut(&mut reply).cast()),
mem::size_of::<u64>() as u32,
Some(&mut bytes_returned),
None,
)
}
.map_err(|e| Error::sensor(SENSOR, format!("reading MSR {msr:#x} failed: {e}")))?;
Ok(reply)
}
fn detect_vendor(&self) -> Result<CpuVendor> {
for vendor in [CpuVendor::Intel, CpuVendor::Amd] {
if self.read_msr(vendor.power_unit_msr()).is_ok() {
return Ok(vendor);
}
}
Err(Error::sensor(
SENSOR,
"neither the Intel nor the AMD power unit register could be read",
))
}
fn read_energy_unit(&self) -> Result<f64> {
const ENERGY_MASK: u64 = 0x1F00;
let raw = self.read_msr(self.vendor.power_unit_msr())?;
let exponent = ((raw & ENERGY_MASK) >> 8) as i32;
let unit = 1.0 / 2.0_f64.powi(exponent);
if !unit.is_finite() || unit <= 0.0 {
return Err(Error::sensor(
SENSOR,
format!("implausible energy unit from register value {raw:#x}"),
));
}
Ok(unit)
}
fn read_energy(&self) -> Result<f64> {
let raw = self.read_msr(self.vendor.package_energy_msr())?;
Ok((raw & 0xFFFF_FFFF) as f64 * self.energy_unit)
}
}
pub(crate) struct RaplSensor {
driver: Result<RaplDriver>,
interval: Interval,
}
impl RaplSensor {
pub(crate) fn new(driver: Result<RaplDriver>) -> Self {
warn_unavailable(SENSOR, &driver);
Self {
driver,
interval: Interval::default(),
}
}
}
impl PowerSensor for RaplSensor {
fn power(&mut self) -> Result<f64> {
let driver = match &mut self.driver {
Ok(driver) => driver,
Err(e) => return Err(repeat_failure(SENSOR, e)),
};
let joules = driver.counter.delta(driver.read_energy()?);
let Some(seconds) = self.interval.tick() else {
return Ok(0.0);
};
Ok(joules / seconds)
}
}
impl Drop for RaplDriver {
fn drop(&mut self) {
unsafe {
let _ = CloseHandle(self.handle);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn control_codes_match_the_drivers_ctl_code_macro() {
let expected = (FILE_DEVICE_UNKNOWN << 16) | (3 << 14) | (0x606 << 2);
assert_eq!(
ctl_code(
FILE_DEVICE_UNKNOWN,
0x606,
METHOD_BUFFERED,
FILE_READ_DATA | FILE_WRITE_DATA
),
expected
);
}
#[test]
fn the_energy_counter_wraps_at_two_to_the_thirty_two() {
assert_eq!(ENERGY_COUNTER_MODULUS, 2f64.powi(32));
assert_eq!(ENERGY_COUNTER_MODULUS as u64, 0xFFFF_FFFFu64 + 1);
}
#[test]
fn each_vendor_uses_its_own_registers() {
assert_eq!(CpuVendor::Intel.power_unit_msr(), 0x606);
assert_eq!(CpuVendor::Intel.package_energy_msr(), 0x611);
assert_eq!(CpuVendor::Amd.power_unit_msr(), 0xc001_0299);
assert_eq!(CpuVendor::Amd.package_energy_msr(), 0xc001_029b);
}
}
}
pub(crate) mod cpu {
use crate::config::AppMatch;
use crate::platform::cpu_usage::{CpuTimeDelta, TotalsSampler, matches_app_name};
use crate::sensor::{CpuTotals, ProcessCpuUtilization};
use std::mem;
use windows::Win32::Foundation::{CloseHandle, FILETIME};
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS,
};
use windows::Win32::System::Threading::{
GetProcessTimes, GetSystemTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
};
pub(crate) fn cpu_usage() -> TotalsSampler {
TotalsSampler::new(system_cpu_totals)
}
pub(crate) fn process_tracker() -> Box<dyn ProcessCpuUtilization> {
Box::new(WindowsProcessTracker::default())
}
#[derive(Debug, Default)]
struct WindowsProcessTracker {
delta: CpuTimeDelta,
}
impl ProcessCpuUtilization for WindowsProcessTracker {
fn process_cpu_utilization(&mut self, pid: u32, cpu_total: Option<u64>) -> f64 {
let Some(cpu_total) = cpu_total.or_else(system_cpu_total) else {
return 0.0;
};
let Some(process_time) = process_time(pid) else {
return 0.0;
};
self.delta.share(cpu_total, process_time)
}
}
fn filetime_to_u64(time: &FILETIME) -> u64 {
((time.dwHighDateTime as u64) << 32) | (time.dwLowDateTime as u64)
}
fn system_cpu_totals() -> Option<CpuTotals> {
let mut idle = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
let result = unsafe { GetSystemTimes(Some(&mut idle), Some(&mut kernel), Some(&mut user)) };
if result.is_err() {
return None;
}
Some(CpuTotals::new(
filetime_to_u64(&kernel) + filetime_to_u64(&user),
filetime_to_u64(&idle),
))
}
fn system_cpu_total() -> Option<u64> {
system_cpu_totals().map(|totals| totals.total)
}
fn process_time(pid: u32) -> Option<u64> {
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }.ok()?;
let mut creation = FILETIME::default();
let mut exit = FILETIME::default();
let mut kernel = FILETIME::default();
let mut user = FILETIME::default();
let result =
unsafe { GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user) };
let _ = unsafe { CloseHandle(handle) };
result.ok()?;
Some(filetime_to_u64(&kernel) + filetime_to_u64(&user))
}
#[derive(Debug, Default)]
pub(crate) struct ToolhelpPids;
impl ToolhelpPids {
pub(crate) fn matching_pids(&mut self, app_name: &str, app_match: AppMatch) -> Vec<u32> {
snapshot_pids(app_name, app_match)
}
}
fn snapshot_pids(app_name: &str, app_match: AppMatch) -> Vec<u32> {
let Ok(snapshot) = (unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }) else {
return Vec::new();
};
let mut pids = Vec::new();
let mut entry = PROCESSENTRY32W {
dwSize: mem::size_of::<PROCESSENTRY32W>() as u32,
..Default::default()
};
if unsafe { Process32FirstW(snapshot, &mut entry) }.is_ok() {
loop {
let name_length = entry
.szExeFile
.iter()
.position(|&c| c == 0)
.unwrap_or(entry.szExeFile.len());
let name = String::from_utf16_lossy(&entry.szExeFile[..name_length]);
if matches_app_name(&name, app_name, app_match) {
pids.push(entry.th32ProcessID);
}
if unsafe { Process32NextW(snapshot, &mut entry) }.is_err() {
break;
}
}
}
let _ = unsafe { CloseHandle(snapshot) };
pids
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filetimes_are_read_as_one_64_bit_count() {
let time = FILETIME {
dwHighDateTime: 2,
dwLowDateTime: 5,
};
assert_eq!(filetime_to_u64(&time), (2 << 32) + 5);
}
#[test]
fn the_running_system_reports_cpu_time() {
let totals = system_cpu_totals().expect("GetSystemTimes");
assert!(totals.total > 0);
assert!(totals.idle <= totals.total);
}
#[test]
fn a_process_that_does_not_exist_reports_nothing() {
assert_eq!(process_time(0), None);
}
}
}