pub mod ids;
#[cfg(target_os = "windows")]
mod dxgi;
#[cfg(target_os = "windows")]
mod pdh;
use crate::device::types::{GpuInfo, ProcessInfo};
use ids::{AdapterIdentity, AdapterLuid};
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct AdapterMetrics {
pub identity: AdapterIdentity,
pub total_memory: Option<u64>,
pub memory_is_shared: bool,
pub used_memory: Option<u64>,
pub utilization: Option<f64>,
pub process_budget: Option<u64>,
pub process_current_usage: Option<u64>,
}
#[derive(Clone, Debug)]
pub struct ProcessGpuMemory {
pub pid: u32,
pub luid: AdapterLuid,
pub used_bytes: u64,
}
#[derive(Clone, Debug, Default)]
pub struct Snapshot {
pub adapters: Vec<AdapterMetrics>,
pub processes: Vec<ProcessGpuMemory>,
}
impl Snapshot {
#[cfg_attr(not(test), allow(dead_code))]
pub fn is_empty(&self) -> bool {
self.adapters.is_empty() && self.processes.is_empty()
}
pub fn identities(&self) -> Vec<AdapterIdentity> {
self.adapters
.iter()
.map(|adapter| adapter.identity.clone())
.collect()
}
pub fn adapter(&self, luid: AdapterLuid) -> Option<&AdapterMetrics> {
self.adapters
.iter()
.find(|adapter| adapter.identity.luid == luid)
}
}
#[cfg(target_os = "windows")]
const SNAPSHOT_COALESCE_WINDOW: std::time::Duration = std::time::Duration::from_millis(500);
#[cfg(target_os = "windows")]
pub fn snapshot() -> Snapshot {
if let Some(cached) = cached_snapshot(SNAPSHOT_COALESCE_WINDOW) {
return cached;
}
let dxgi_adapters = dxgi::enumerate();
let resolved: Vec<_> = dxgi_adapters
.into_iter()
.map(|adapter| {
let (total_memory, memory_is_shared) = resolve_adapter_memory(
adapter.dedicated_video_memory,
adapter.shared_system_memory,
);
(adapter, total_memory, memory_is_shared)
})
.collect();
let shared_luids: std::collections::HashSet<AdapterLuid> = resolved
.iter()
.filter(|(_, _, is_shared)| *is_shared)
.map(|(adapter, _, _)| adapter.identity.luid)
.collect();
let sample = pdh::sample(!shared_luids.is_empty());
let adapters = resolved
.into_iter()
.map(|(adapter, total_memory, memory_is_shared)| {
let luid = adapter.identity.luid;
let used_memory = select_adapter_usage(
memory_is_shared,
sample.adapter_memory.get(&luid).copied(),
sample.adapter_shared_memory.get(&luid).copied(),
);
AdapterMetrics {
identity: adapter.identity,
total_memory,
memory_is_shared,
used_memory,
utilization: sample.utilization.get(&luid).copied(),
process_budget: adapter.process_budget,
process_current_usage: adapter.process_current_usage,
}
})
.collect();
let processes = merge_process_rows(
sample.process_memory,
sample.process_shared_memory,
&shared_luids,
);
let snapshot = Snapshot {
adapters,
processes,
};
store_snapshot(&snapshot);
snapshot
}
#[cfg(not(target_os = "windows"))]
pub fn snapshot() -> Snapshot {
Snapshot::default()
}
#[cfg(target_os = "windows")]
type SnapshotCache = std::sync::Mutex<Option<(std::time::Instant, Snapshot)>>;
#[cfg(target_os = "windows")]
static LAST_SNAPSHOT: once_cell::sync::OnceCell<SnapshotCache> = once_cell::sync::OnceCell::new();
#[cfg(target_os = "windows")]
fn snapshot_cache() -> &'static SnapshotCache {
LAST_SNAPSHOT.get_or_init(|| std::sync::Mutex::new(None))
}
#[cfg(target_os = "windows")]
fn cached_snapshot(max_age: std::time::Duration) -> Option<Snapshot> {
let guard = match snapshot_cache().lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard
.as_ref()
.and_then(|(taken_at, snapshot)| (taken_at.elapsed() < max_age).then(|| snapshot.clone()))
}
#[cfg(target_os = "windows")]
fn store_snapshot(snapshot: &Snapshot) {
let mut guard = match snapshot_cache().lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
*guard = Some((std::time::Instant::now(), snapshot.clone()));
}
#[cfg(target_os = "windows")]
pub fn latest() -> Snapshot {
let guard = match snapshot_cache().lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard
.as_ref()
.map(|(_, snapshot)| snapshot.clone())
.unwrap_or_default()
}
#[cfg(not(target_os = "windows"))]
pub fn latest() -> Snapshot {
Snapshot::default()
}
#[cfg(target_os = "windows")]
pub fn pdh_query_available() -> bool {
pdh::query_available()
}
#[cfg(not(target_os = "windows"))]
pub fn pdh_query_available() -> bool {
false
}
pub use crate::device::readers::detail_keys::note_metrics_source;
const MIN_DISCRETE_DEDICATED_BYTES: u64 = 1024 * 1024 * 1024;
fn resolve_adapter_memory(dedicated: u64, shared: u64) -> (Option<u64>, bool) {
if dedicated >= MIN_DISCRETE_DEDICATED_BYTES {
return (Some(dedicated), false);
}
if shared > 0 {
return (Some(shared), true);
}
if dedicated > 0 {
return (Some(dedicated), false);
}
(None, false)
}
fn select_adapter_usage(
memory_is_shared: bool,
dedicated: Option<u64>,
shared: Option<u64>,
) -> Option<u64> {
if memory_is_shared { shared } else { dedicated }
}
fn merge_process_rows(
dedicated: Vec<(ids::GpuProcessMemoryInstance, u64)>,
shared: Vec<(ids::GpuProcessMemoryInstance, u64)>,
shared_luids: &std::collections::HashSet<AdapterLuid>,
) -> Vec<ProcessGpuMemory> {
dedicated
.into_iter()
.filter(|(instance, _)| !shared_luids.contains(&instance.luid))
.chain(
shared
.into_iter()
.filter(|(instance, _)| shared_luids.contains(&instance.luid)),
)
.filter(|(_, bytes)| *bytes > 0)
.map(|(instance, bytes)| ProcessGpuMemory {
pid: instance.pid,
luid: instance.luid,
used_bytes: bytes,
})
.collect()
}
pub fn apply_to_gpu_info(gpu: &mut GpuInfo, metrics: &AdapterMetrics) {
let mut touched_dxgi = false;
let mut touched_pdh = false;
if let Some(total) = metrics.total_memory {
gpu.total_memory = total;
gpu.detail.insert(
"Source: Memory".to_string(),
if metrics.memory_is_shared {
"DXGI (shared)"
} else {
"DXGI"
}
.to_string(),
);
gpu.detail.entry("Variant".to_string()).or_insert_with(|| {
if metrics.memory_is_shared {
"Integrated".to_string()
} else {
"Discrete".to_string()
}
});
touched_dxgi = true;
}
if let Some(budget) = metrics.process_budget {
gpu.detail.insert(
"VRAM Budget (this process)".to_string(),
format!("{budget} bytes"),
);
touched_dxgi = true;
}
if let Some(usage) = metrics.process_current_usage {
gpu.detail.insert(
"VRAM Usage (this process)".to_string(),
format!("{usage} bytes"),
);
touched_dxgi = true;
}
if let Some(used) = metrics.used_memory {
gpu.used_memory = used;
gpu.detail.insert(
"Source: Memory Used".to_string(),
if metrics.memory_is_shared {
"PDH (shared)"
} else {
"PDH"
}
.to_string(),
);
touched_pdh = true;
}
if let Some(utilization) = metrics.utilization {
gpu.utilization = utilization;
gpu.detail
.insert("Source: Utilization".to_string(), "PDH".to_string());
touched_pdh = true;
}
if touched_dxgi {
note_metrics_source(&mut gpu.detail, "DXGI");
}
if touched_pdh {
note_metrics_source(&mut gpu.detail, "PDH");
}
}
pub type AdapterIndex = HashMap<AdapterLuid, (usize, String)>;
pub fn pair_and_apply(gpus: &mut [GpuInfo], snapshot: &Snapshot) -> AdapterIndex {
let mut adapter_index = AdapterIndex::new();
if snapshot.adapters.is_empty() {
return adapter_index;
}
let identities = snapshot.identities();
for (ordinal, gpu) in gpus.iter_mut().enumerate() {
let Some(identity) =
ids::match_adapter(&identities, Some(gpu.uuid.as_str()), &gpu.name, ordinal)
else {
continue;
};
let luid = identity.luid;
if let Some(metrics) = snapshot.adapter(luid) {
apply_to_gpu_info(gpu, metrics);
}
adapter_index.insert(luid, (ordinal, gpu.uuid.clone()));
}
adapter_index
}
pub fn augment_gpus(gpus: &mut [GpuInfo]) -> AdapterIndex {
let snapshot = snapshot();
pair_and_apply(gpus, &snapshot)
}
pub fn process_rows_with<F>(adapter_index: &AdapterIndex, refresh: F) -> Vec<ProcessInfo>
where
F: FnOnce() -> AdapterIndex,
{
let owned;
let adapter_index = if adapter_index.is_empty() {
owned = refresh();
&owned
} else {
adapter_index
};
if adapter_index.is_empty() {
return Vec::new();
}
let gpu_rows = process_rows_from(&latest(), adapter_index);
if gpu_rows.is_empty() {
return Vec::new();
}
let gpu_pids: std::collections::HashSet<u32> = gpu_rows.iter().map(|row| row.pid).collect();
let all_processes = crate::utils::system::with_global_system(|system| {
system.refresh_processes_specifics(
sysinfo::ProcessesToUpdate::All,
true,
sysinfo::ProcessRefreshKind::everything().with_user(sysinfo::UpdateKind::Always),
);
system.refresh_memory();
crate::device::process_list::get_all_processes(system, &gpu_pids)
});
crate::device::process_list::merge_gpu_processes(all_processes, gpu_rows)
}
pub fn process_rows_from(snapshot: &Snapshot, adapter_index: &AdapterIndex) -> Vec<ProcessInfo> {
snapshot
.processes
.iter()
.filter_map(|process| {
let (device_id, uuid) = adapter_index.get(&process.luid)?;
Some(gpu_process_row(
*device_id,
uuid,
process.pid,
process.used_bytes,
))
})
.collect()
}
pub fn gpu_process_row(
device_id: usize,
device_uuid: &str,
pid: u32,
used_memory: u64,
) -> ProcessInfo {
ProcessInfo {
device_id,
device_uuid: device_uuid.to_string(),
pid,
process_name: String::new(),
used_memory,
cpu_percent: 0.0,
memory_percent: 0.0,
memory_rss: 0,
memory_vms: 0,
user: String::new(),
state: String::new(),
start_time: String::new(),
cpu_time: 0,
command: String::new(),
ppid: 0,
threads: 0,
uses_gpu: true,
priority: 0,
nice_value: 0,
gpu_utilization: 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
const MIB: u64 = 1024 * 1024;
const GIB: u64 = 1024 * MIB;
#[test]
fn small_carve_out_reports_the_shared_aperture() {
let (total, shared) = resolve_adapter_memory(128 * MIB, 16 * GIB);
assert_eq!(total, Some(16 * GIB));
assert!(shared, "a carve-out below the floor is a shared aperture");
}
#[test]
fn amd_apu_carve_out_reports_the_shared_aperture() {
for carve_out in [64 * MIB, 128 * MIB, 256 * MIB, 512 * MIB] {
let (total, shared) = resolve_adapter_memory(carve_out, 32 * GIB);
assert_eq!(total, Some(32 * GIB), "carve-out {carve_out} bytes");
assert!(shared, "carve-out {carve_out} bytes");
}
}
#[test]
fn discrete_card_keeps_its_dedicated_pool() {
for vram in [2 * GIB, 8 * GIB, 12 * GIB, 24 * GIB] {
let (total, shared) = resolve_adapter_memory(vram, 16 * GIB);
assert_eq!(total, Some(vram), "vram {vram} bytes");
assert!(!shared, "vram {vram} bytes");
}
}
#[test]
fn zero_dedicated_still_reports_the_shared_aperture() {
let (total, shared) = resolve_adapter_memory(0, 8 * GIB);
assert_eq!(total, Some(8 * GIB));
assert!(shared);
}
#[test]
fn large_configured_carve_out_is_taken_at_face_value() {
let (total, shared) = resolve_adapter_memory(2 * GIB, 32 * GIB);
assert_eq!(total, Some(2 * GIB));
assert!(!shared);
}
#[test]
fn small_pool_with_no_aperture_is_reported_as_is() {
let (total, shared) = resolve_adapter_memory(128 * MIB, 0);
assert_eq!(total, Some(128 * MIB));
assert!(!shared);
}
#[test]
fn no_memory_information_reports_nothing() {
assert_eq!(resolve_adapter_memory(0, 0), (None, false));
}
#[test]
fn the_discrete_floor_is_inclusive() {
let (_, shared_at) = resolve_adapter_memory(MIN_DISCRETE_DEDICATED_BYTES, 32 * GIB);
assert!(!shared_at, "exactly at the floor counts as dedicated");
let (_, shared_below) = resolve_adapter_memory(MIN_DISCRETE_DEDICATED_BYTES - 1, 32 * GIB);
assert!(shared_below, "one byte below the floor is a carve-out");
}
use crate::device::types::{GPU_METRIC_UNAVAILABLE, GpuInfo};
fn blank_gpu() -> GpuInfo {
let mut detail = HashMap::new();
detail.insert("Metrics Source".to_string(), "WMI".to_string());
detail.insert("Source: Utilization".to_string(), "unavailable".to_string());
detail.insert("Source: Power".to_string(), "unavailable".to_string());
detail.insert("Source: Memory".to_string(), "WMI".to_string());
GpuInfo {
uuid: "PCI\\VEN_1002&DEV_744C".to_string(),
time: String::new(),
name: "AMD Radeon RX 7900 XTX".to_string(),
device_type: "GPU".to_string(),
host_id: String::new(),
hostname: String::new(),
instance: String::new(),
utilization: GPU_METRIC_UNAVAILABLE,
ane_utilization: 0.0,
dla_utilization: None,
tensorcore_utilization: None,
temperature: 0,
used_memory: 0,
total_memory: 4_294_967_295,
frequency: 0,
power_consumption: GPU_METRIC_UNAVAILABLE,
gpu_core_count: None,
temperature_threshold_slowdown: None,
temperature_threshold_shutdown: None,
temperature_threshold_max_operating: None,
temperature_threshold_acoustic: None,
performance_state: None,
fan_speed_rpm: None,
numa_node_id: None,
gsp_firmware_mode: None,
gsp_firmware_version: None,
nvlink_remote_devices: Vec::new(),
gpm_metrics: None,
detail,
}
}
fn metrics(total: Option<u64>, used: Option<u64>, utilization: Option<f64>) -> AdapterMetrics {
AdapterMetrics {
identity: AdapterIdentity {
luid: AdapterLuid::new(0, 0xD3F5),
vendor_id: 0x1002,
device_id: 0x744C,
description: "AMD Radeon RX 7900 XTX".to_string(),
},
total_memory: total,
memory_is_shared: false,
used_memory: used,
utilization,
process_budget: None,
process_current_usage: None,
}
}
#[test]
fn dxgi_fills_the_variant_the_name_could_not_decide() {
let mut shared = blank_gpu();
let mut shared_metrics = metrics(Some(16 * GIB), None, None);
shared_metrics.memory_is_shared = true;
apply_to_gpu_info(&mut shared, &shared_metrics);
assert_eq!(shared.detail["Variant"], "Integrated");
let mut dedicated = blank_gpu();
apply_to_gpu_info(&mut dedicated, &metrics(Some(8 * GIB), None, None));
assert_eq!(dedicated.detail["Variant"], "Discrete");
}
#[test]
fn an_existing_variant_is_not_overwritten() {
let mut gpu = blank_gpu();
gpu.detail
.insert("Variant".to_string(), "Discrete".to_string());
let mut shared_metrics = metrics(Some(16 * GIB), None, None);
shared_metrics.memory_is_shared = true;
apply_to_gpu_info(&mut gpu, &shared_metrics);
assert_eq!(gpu.detail["Variant"], "Discrete");
}
#[test]
fn dxgi_total_replaces_the_truncated_wmi_value() {
let mut gpu = blank_gpu();
apply_to_gpu_info(&mut gpu, &metrics(Some(25_769_803_776), None, None));
assert_eq!(gpu.total_memory, 25_769_803_776);
assert_eq!(gpu.detail["Source: Memory"], "DXGI");
assert_eq!(gpu.detail["Metrics Source"], "WMI + DXGI");
}
#[test]
fn pdh_fields_are_recorded_with_their_source() {
let mut gpu = blank_gpu();
apply_to_gpu_info(
&mut gpu,
&metrics(Some(8_589_934_592), Some(2_147_483_648), Some(42.5)),
);
assert_eq!(gpu.utilization, 42.5);
assert_eq!(gpu.used_memory, 2_147_483_648);
assert_eq!(gpu.detail["Source: Utilization"], "PDH");
assert_eq!(gpu.detail["Source: Memory Used"], "PDH");
assert_eq!(gpu.detail["Metrics Source"], "WMI + DXGI + PDH");
}
#[test]
fn absent_fields_leave_the_baseline_untouched() {
let mut gpu = blank_gpu();
apply_to_gpu_info(&mut gpu, &metrics(Some(1_073_741_824), None, None));
assert_eq!(gpu.total_memory, 1_073_741_824);
assert_eq!(gpu.utilization_reading(), None);
assert_eq!(gpu.power_consumption_reading(), None);
assert_eq!(gpu.detail["Source: Utilization"], "unavailable");
assert_eq!(gpu.detail["Source: Power"], "unavailable");
assert_eq!(gpu.detail["Metrics Source"], "WMI + DXGI");
}
#[test]
fn applying_twice_does_not_grow_the_source_string() {
let mut gpu = blank_gpu();
let m = metrics(Some(1), Some(2), Some(3.0));
apply_to_gpu_info(&mut gpu, &m);
apply_to_gpu_info(&mut gpu, &m);
apply_to_gpu_info(&mut gpu, &m);
assert_eq!(gpu.detail["Metrics Source"], "WMI + DXGI + PDH");
}
#[test]
fn metrics_source_starts_clean_when_absent() {
let mut detail = HashMap::new();
note_metrics_source(&mut detail, "DXGI");
assert_eq!(detail["Metrics Source"], "DXGI");
note_metrics_source(&mut detail, "PDH");
assert_eq!(detail["Metrics Source"], "DXGI + PDH");
}
fn snapshot_with(adapters: Vec<AdapterMetrics>, processes: Vec<ProcessGpuMemory>) -> Snapshot {
Snapshot {
adapters,
processes,
}
}
#[test]
fn pairs_gpus_to_adapters_and_returns_the_luid_index() {
let mut gpus = vec![blank_gpu()];
let snapshot = snapshot_with(
vec![metrics(Some(25_769_803_776), Some(1024), Some(77.0))],
vec![],
);
let index = pair_and_apply(&mut gpus, &snapshot);
assert_eq!(gpus[0].total_memory, 25_769_803_776);
assert_eq!(gpus[0].utilization, 77.0);
assert_eq!(gpus[0].used_memory, 1024);
assert_eq!(
index.get(&AdapterLuid::new(0, 0xD3F5)),
Some(&(0usize, "PCI\\VEN_1002&DEV_744C".to_string()))
);
}
#[test]
fn pairing_an_empty_snapshot_changes_nothing() {
let mut gpus = vec![blank_gpu()];
let before = gpus[0].total_memory;
let index = pair_and_apply(&mut gpus, &Snapshot::default());
assert!(index.is_empty());
assert_eq!(gpus[0].total_memory, before);
assert_eq!(gpus[0].detail["Metrics Source"], "WMI");
}
#[test]
fn unmatched_gpus_keep_the_wmi_baseline() {
let mut gpus = vec![blank_gpu(), blank_gpu()];
gpus[1].uuid = "PCI\\VEN_10DE&DEV_2684".to_string();
gpus[1].name = "NVIDIA GeForce RTX 4090".to_string();
let snapshot = snapshot_with(vec![metrics(Some(8_589_934_592), None, Some(10.0))], vec![]);
let index = pair_and_apply(&mut gpus, &snapshot);
assert_eq!(gpus[0].total_memory, 8_589_934_592);
assert_eq!(gpus[1].total_memory, 4_294_967_295);
assert_eq!(gpus[1].detail["Metrics Source"], "WMI");
assert_eq!(index.len(), 1);
}
#[test]
fn process_rows_carry_the_gpu_identity_and_leave_the_rest_to_the_merge() {
let row = gpu_process_row(2, "PCI\\VEN_1002&DEV_744C", 4242, 536_870_912);
assert_eq!(row.pid, 4242);
assert_eq!(row.device_id, 2);
assert_eq!(row.device_uuid, "PCI\\VEN_1002&DEV_744C");
assert_eq!(row.used_memory, 536_870_912);
assert!(row.uses_gpu);
assert!(row.process_name.is_empty());
assert!(row.user.is_empty());
}
#[test]
fn process_rows_are_attributed_to_the_matching_adapter() {
let known = AdapterLuid::new(0, 0xD3F5);
let mut adapter_index = AdapterIndex::new();
adapter_index.insert(known, (0, "PCI\\VEN_1002&DEV_744C".to_string()));
let snapshot = snapshot_with(
vec![],
vec![
ProcessGpuMemory {
pid: 4242,
luid: known,
used_bytes: 536_870_912,
},
ProcessGpuMemory {
pid: 99,
luid: AdapterLuid::new(0, 0xFFFF),
used_bytes: 1,
},
],
);
let rows = process_rows_from(&snapshot, &adapter_index);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].pid, 4242);
assert_eq!(rows[0].used_memory, 536_870_912);
assert_eq!(rows[0].device_uuid, "PCI\\VEN_1002&DEV_744C");
assert_eq!(rows[0].device_id, 0);
}
#[test]
fn process_scoped_dxgi_figures_are_labelled_and_kept_out_of_used_memory() {
let mut gpu = blank_gpu();
let mut m = metrics(Some(8_589_934_592), None, None);
m.process_budget = Some(7_000_000_000);
m.process_current_usage = Some(123_456);
apply_to_gpu_info(&mut gpu, &m);
assert_eq!(gpu.used_memory, 0);
assert_eq!(gpu.detail["VRAM Budget (this process)"], "7000000000 bytes");
assert_eq!(gpu.detail["VRAM Usage (this process)"], "123456 bytes");
assert!(!gpu.detail.contains_key("Source: Memory Used"));
}
#[test]
fn the_platform_entry_points_never_panic() {
let mut gpus = vec![blank_gpu()];
let index = augment_gpus(&mut gpus);
let _ = process_rows_with(&index, AdapterIndex::new);
let _ = pdh_query_available();
let _ = latest();
#[cfg(not(target_os = "windows"))]
{
assert!(snapshot().is_empty());
assert!(latest().is_empty());
assert!(!pdh_query_available());
assert!(index.is_empty());
assert!(process_rows_with(&index, AdapterIndex::new).is_empty());
assert_eq!(gpus[0].total_memory, 4_294_967_295);
assert_eq!(gpus[0].detail["Metrics Source"], "WMI");
}
}
#[test]
fn an_empty_index_consults_the_refresh_closure_exactly_once() {
use std::cell::Cell;
let calls = Cell::new(0);
let rows = process_rows_with(&AdapterIndex::new(), || {
calls.set(calls.get() + 1);
AdapterIndex::new()
});
assert_eq!(calls.get(), 1);
assert!(rows.is_empty());
let calls = Cell::new(0);
let mut populated = AdapterIndex::new();
populated.insert(AdapterLuid::new(0, 1), (0, "uuid".to_string()));
let _ = process_rows_with(&populated, || {
calls.set(calls.get() + 1);
AdapterIndex::new()
});
assert_eq!(calls.get(), 0);
}
#[test]
fn snapshot_helpers_behave_on_an_empty_snapshot() {
let snapshot = Snapshot::default();
assert!(snapshot.is_empty());
assert!(snapshot.identities().is_empty());
assert!(snapshot.adapter(AdapterLuid::new(0, 1)).is_none());
}
#[test]
fn an_integrated_adapter_is_measured_against_its_aperture() {
assert_eq!(
select_adapter_usage(true, Some(0), Some(3_221_225_472)),
Some(3_221_225_472)
);
}
#[test]
fn a_discrete_adapter_is_measured_against_its_dedicated_pool() {
assert_eq!(
select_adapter_usage(false, Some(8_589_934_592), Some(1_048_576)),
Some(8_589_934_592)
);
}
#[test]
fn a_missing_pool_reports_nothing_rather_than_the_other_one() {
assert_eq!(select_adapter_usage(true, Some(134_217_728), None), None);
assert_eq!(select_adapter_usage(false, None, Some(512)), None);
assert_eq!(select_adapter_usage(true, None, None), None);
}
fn process_instance(pid: u32, luid: AdapterLuid) -> ids::GpuProcessMemoryInstance {
ids::GpuProcessMemoryInstance { pid, luid, phys: 0 }
}
#[test]
fn process_rows_follow_their_adapter() {
let igpu = AdapterLuid::new(0, 1);
let discrete = AdapterLuid::new(0, 2);
let shared_luids = std::collections::HashSet::from([igpu]);
let rows = merge_process_rows(
vec![
(process_instance(100, igpu), 0),
(process_instance(200, discrete), 4_294_967_296),
],
vec![
(process_instance(100, igpu), 1_073_741_824),
(process_instance(200, discrete), 65_536),
],
&shared_luids,
);
let mut seen: Vec<(u32, u64)> = rows.iter().map(|r| (r.pid, r.used_bytes)).collect();
seen.sort_unstable();
assert_eq!(seen, vec![(100, 1_073_741_824), (200, 4_294_967_296)]);
}
#[test]
fn zero_byte_process_rows_are_dropped() {
let luid = AdapterLuid::new(0, 1);
let rows = merge_process_rows(
vec![
(process_instance(1, luid), 0),
(process_instance(2, luid), 8),
],
Vec::new(),
&std::collections::HashSet::new(),
);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].pid, 2);
}
#[test]
fn a_discrete_only_host_is_unaffected() {
let luid = AdapterLuid::new(0, 7);
let rows = merge_process_rows(
vec![(process_instance(42, luid), 1024)],
Vec::new(),
&std::collections::HashSet::new(),
);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].used_bytes, 1024);
}
}