use super::ids::{
AdapterLuid, GpuAdapterMemoryInstance, GpuEngineInstance, GpuProcessMemoryInstance,
aggregate_adapter_memory, aggregate_engine_utilization, parse_gpu_adapter_memory_instance,
parse_gpu_engine_instance, parse_gpu_process_memory_instance,
};
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::sync::Mutex;
use windows::Win32::System::Performance::{
PDH_FMT_COUNTERVALUE_ITEM_W, PDH_FMT_DOUBLE, PDH_HCOUNTER, PDH_HQUERY, PdhAddEnglishCounterW,
PdhCloseQuery, PdhCollectQueryData, PdhGetFormattedCounterArrayW, PdhOpenQueryW,
};
use windows::core::{PCWSTR, w};
const PDH_CSTATUS_VALID_DATA: u32 = 0;
const PDH_CSTATUS_NEW_DATA: u32 = 1;
const PDH_MORE_DATA: u32 = 0x8000_07D2;
#[derive(Debug, Default)]
pub struct PdhSample {
pub utilization: HashMap<AdapterLuid, f64>,
pub adapter_memory: HashMap<AdapterLuid, u64>,
pub process_memory: Vec<(GpuProcessMemoryInstance, u64)>,
pub adapter_shared_memory: HashMap<AdapterLuid, u64>,
pub process_shared_memory: Vec<(GpuProcessMemoryInstance, u64)>,
}
struct GpuCounterQuery {
query: PDH_HQUERY,
engine: PDH_HCOUNTER,
adapter_memory: PDH_HCOUNTER,
process_memory: PDH_HCOUNTER,
adapter_shared_memory: PDH_HCOUNTER,
process_shared_memory: PDH_HCOUNTER,
shared_added: bool,
primed: bool,
}
unsafe impl Send for GpuCounterQuery {}
impl Drop for GpuCounterQuery {
fn drop(&mut self) {
unsafe {
let _ = PdhCloseQuery(self.query);
}
}
}
impl GpuCounterQuery {
fn open() -> Option<Self> {
let mut query = PDH_HQUERY::default();
if unsafe { PdhOpenQueryW(PCWSTR::null(), 0, &mut query) } != 0 {
return None;
}
let engine = match add_counter(query, w!("\\GPU Engine(*)\\Utilization Percentage")) {
Some(counter) => counter,
None => {
unsafe {
let _ = PdhCloseQuery(query);
}
return None;
}
};
let adapter_memory =
add_counter(query, w!("\\GPU Adapter Memory(*)\\Dedicated Usage")).unwrap_or_default();
let process_memory =
add_counter(query, w!("\\GPU Process Memory(*)\\Dedicated Usage")).unwrap_or_default();
Some(Self {
query,
engine,
adapter_memory,
process_memory,
adapter_shared_memory: PDH_HCOUNTER::default(),
process_shared_memory: PDH_HCOUNTER::default(),
shared_added: false,
primed: false,
})
}
fn ensure_shared_counters(&mut self) {
if self.shared_added {
return;
}
self.shared_added = true;
self.adapter_shared_memory =
add_counter(self.query, w!("\\GPU Adapter Memory(*)\\Shared Usage"))
.unwrap_or_default();
self.process_shared_memory =
add_counter(self.query, w!("\\GPU Process Memory(*)\\Shared Usage"))
.unwrap_or_default();
}
fn collect(&mut self, include_shared: bool) -> PdhSample {
if include_shared {
self.ensure_shared_counters();
}
if unsafe { PdhCollectQueryData(self.query) } != 0 {
return PdhSample::default();
}
let was_primed = self.primed;
self.primed = true;
let mut sample = PdhSample::default();
if was_primed {
let engine_samples = read_counter_array(self.engine)
.into_iter()
.filter_map(|(name, value)| {
parse_gpu_engine_instance(&name).map(|instance| (instance, value))
})
.collect::<Vec<(GpuEngineInstance, f64)>>();
sample.utilization = aggregate_engine_utilization(engine_samples);
}
sample.adapter_memory = read_adapter_memory(self.adapter_memory);
sample.process_memory = read_process_memory(self.process_memory);
if include_shared {
sample.adapter_shared_memory = read_adapter_memory(self.adapter_shared_memory);
sample.process_shared_memory = read_process_memory(self.process_shared_memory);
}
sample
}
}
fn read_adapter_memory(counter: PDH_HCOUNTER) -> HashMap<AdapterLuid, u64> {
if counter.is_invalid() {
return HashMap::new();
}
let samples = read_counter_array(counter)
.into_iter()
.filter_map(|(name, value)| {
parse_gpu_adapter_memory_instance(&name).map(|instance| (instance, value))
})
.collect::<Vec<(GpuAdapterMemoryInstance, f64)>>();
aggregate_adapter_memory(samples)
}
fn read_process_memory(counter: PDH_HCOUNTER) -> Vec<(GpuProcessMemoryInstance, u64)> {
if counter.is_invalid() {
return Vec::new();
}
read_counter_array(counter)
.into_iter()
.filter_map(|(name, value)| {
if !value.is_finite() || value < 0.0 {
return None;
}
parse_gpu_process_memory_instance(&name).map(|instance| (instance, value as u64))
})
.collect()
}
fn add_counter(query: PDH_HQUERY, path: PCWSTR) -> Option<PDH_HCOUNTER> {
let mut counter = PDH_HCOUNTER::default();
if unsafe { PdhAddEnglishCounterW(query, path, 0, &mut counter) } != 0 {
return None;
}
Some(counter)
}
fn read_counter_array(counter: PDH_HCOUNTER) -> Vec<(String, f64)> {
if counter.is_invalid() {
return Vec::new();
}
for _ in 0..2 {
let mut buffer_size = 0u32;
let mut item_count = 0u32;
let status = unsafe {
PdhGetFormattedCounterArrayW(
counter,
PDH_FMT_DOUBLE,
&mut buffer_size,
&mut item_count,
None,
)
};
if status != PDH_MORE_DATA || buffer_size == 0 {
return Vec::new();
}
let item_size = std::mem::size_of::<PDH_FMT_COUNTERVALUE_ITEM_W>();
let capacity = (buffer_size as usize).div_ceil(item_size);
let mut items: Vec<PDH_FMT_COUNTERVALUE_ITEM_W> = Vec::with_capacity(capacity);
let status = unsafe {
PdhGetFormattedCounterArrayW(
counter,
PDH_FMT_DOUBLE,
&mut buffer_size,
&mut item_count,
Some(items.as_mut_ptr()),
)
};
if status == PDH_MORE_DATA {
continue;
}
if status != 0 {
return Vec::new();
}
let item_count = (item_count as usize).min(capacity);
unsafe { items.set_len(item_count) };
return items
.iter()
.filter_map(|item| {
if !matches!(
item.FmtValue.CStatus,
PDH_CSTATUS_VALID_DATA | PDH_CSTATUS_NEW_DATA
) {
return None;
}
if item.szName.is_null() {
return None;
}
let value = unsafe { item.FmtValue.Anonymous.doubleValue };
let name = unsafe { item.szName.to_string() }.ok()?;
Some((name, value))
})
.collect();
}
Vec::new()
}
static SAMPLER: OnceCell<Mutex<Option<GpuCounterQuery>>> = OnceCell::new();
pub fn sample(include_shared: bool) -> PdhSample {
let cell = SAMPLER.get_or_init(|| Mutex::new(GpuCounterQuery::open()));
let mut guard = match cell.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
match guard.as_mut() {
Some(query) => query.collect(include_shared),
None => PdhSample::default(),
}
}
pub fn query_available() -> bool {
let cell = SAMPLER.get_or_init(|| Mutex::new(GpuCounterQuery::open()));
match cell.lock() {
Ok(guard) => guard.is_some(),
Err(poisoned) => poisoned.into_inner().is_some(),
}
}