use std::ffi::c_void;
use std::time::{Duration, Instant};
use core_foundation::array::CFArray;
use core_foundation::base::TCFType;
use core_foundation::dictionary::CFDictionary;
use core_foundation::string::CFString;
use core_foundation_sys::base::{CFRelease, CFTypeRef};
use core_foundation_sys::dictionary::{
CFDictionaryGetValue, CFDictionaryRef, CFMutableDictionaryRef,
};
use core_foundation_sys::string::CFStringRef;
use crate::gpu_engine::GpuError;
const LIB_IOREPORT: &str = "/usr/lib/libIOReport.dylib";
const GROUP_ENERGY: &str = "Energy Model";
const GROUP_GPU_STATS: &str = "GPU Stats";
const SUBGROUP_PERF_STATES: &str = "GPU Performance States";
const CHANNEL_GPU_ENERGY: &str = "GPU Energy";
const CHANNEL_GPU: &str = "GPU";
const MIN_SAMPLE_INTERVAL: Duration = Duration::from_millis(100);
type SubscriptionRef = *const c_void;
struct Symbols {
create_samples:
unsafe extern "C" fn(SubscriptionRef, CFMutableDictionaryRef, CFTypeRef) -> CFDictionaryRef,
create_delta:
unsafe extern "C" fn(CFDictionaryRef, CFDictionaryRef, CFTypeRef) -> CFDictionaryRef,
channel_group: unsafe extern "C" fn(CFDictionaryRef) -> CFStringRef,
channel_subgroup: unsafe extern "C" fn(CFDictionaryRef) -> CFStringRef,
channel_name: unsafe extern "C" fn(CFDictionaryRef) -> CFStringRef,
channel_unit: unsafe extern "C" fn(CFDictionaryRef) -> CFStringRef,
simple_value: unsafe extern "C" fn(CFDictionaryRef, i32) -> i64,
state_count: unsafe extern "C" fn(CFDictionaryRef) -> i32,
state_name: unsafe extern "C" fn(CFDictionaryRef, i32) -> CFStringRef,
state_residency: unsafe extern "C" fn(CFDictionaryRef, i32) -> i64,
}
#[derive(Debug, Clone, Default)]
pub struct IntervalReport {
pub energy: Option<(i64, String)>,
pub residencies: Vec<(String, u64)>,
pub elapsed: Duration,
}
pub struct IoReportSampler {
symbols: Symbols,
subscription: SubscriptionRef,
channels: CFMutableDictionaryRef,
previous: Option<(CFDictionaryRef, Instant)>,
#[allow(dead_code, reason = "held to keep the dlopen'd library mapped")]
library: libloading::Library,
}
unsafe impl Send for IoReportSampler {}
impl IoReportSampler {
pub fn open() -> Result<Self, GpuError> {
unsafe {
let library = libloading::Library::new(LIB_IOREPORT).map_err(|e| {
GpuError::DriverUnavailable {
vendor: "Apple",
reason: format!("{LIB_IOREPORT} could not be loaded: {e}"),
}
})?;
macro_rules! symbol {
($name:literal, $signature:ty) => {{
let resolved: libloading::Symbol<$signature> =
library
.get($name)
.map_err(|e| GpuError::DriverUnavailable {
vendor: "Apple",
reason: format!(
"{} is missing from {LIB_IOREPORT}: {e}",
String::from_utf8_lossy(&$name[..$name.len() - 1])
),
})?;
*resolved
}};
}
let copy_channels = symbol!(
b"IOReportCopyChannelsInGroup\0",
unsafe extern "C" fn(
CFStringRef,
CFStringRef,
u64,
u64,
u64,
) -> CFMutableDictionaryRef
);
let merge_channels = symbol!(
b"IOReportMergeChannels\0",
unsafe extern "C" fn(CFMutableDictionaryRef, CFMutableDictionaryRef, CFTypeRef)
);
let create_subscription = symbol!(
b"IOReportCreateSubscription\0",
unsafe extern "C" fn(
*const c_void,
CFMutableDictionaryRef,
*mut CFMutableDictionaryRef,
u64,
CFTypeRef,
) -> SubscriptionRef
);
let symbols = Symbols {
create_samples: symbol!(
b"IOReportCreateSamples\0",
unsafe extern "C" fn(
SubscriptionRef,
CFMutableDictionaryRef,
CFTypeRef,
) -> CFDictionaryRef
),
create_delta: symbol!(
b"IOReportCreateSamplesDelta\0",
unsafe extern "C" fn(
CFDictionaryRef,
CFDictionaryRef,
CFTypeRef,
) -> CFDictionaryRef
),
channel_group: symbol!(
b"IOReportChannelGetGroup\0",
unsafe extern "C" fn(CFDictionaryRef) -> CFStringRef
),
channel_subgroup: symbol!(
b"IOReportChannelGetSubGroup\0",
unsafe extern "C" fn(CFDictionaryRef) -> CFStringRef
),
channel_name: symbol!(
b"IOReportChannelGetChannelName\0",
unsafe extern "C" fn(CFDictionaryRef) -> CFStringRef
),
channel_unit: symbol!(
b"IOReportChannelGetUnitLabel\0",
unsafe extern "C" fn(CFDictionaryRef) -> CFStringRef
),
simple_value: symbol!(
b"IOReportSimpleGetIntegerValue\0",
unsafe extern "C" fn(CFDictionaryRef, i32) -> i64
),
state_count: symbol!(
b"IOReportStateGetCount\0",
unsafe extern "C" fn(CFDictionaryRef) -> i32
),
state_name: symbol!(
b"IOReportStateGetNameForIndex\0",
unsafe extern "C" fn(CFDictionaryRef, i32) -> CFStringRef
),
state_residency: symbol!(
b"IOReportStateGetResidency\0",
unsafe extern "C" fn(CFDictionaryRef, i32) -> i64
),
};
let energy_group = CFString::new(GROUP_ENERGY);
let gpu_group = CFString::new(GROUP_GPU_STATS);
let perf_subgroup = CFString::new(SUBGROUP_PERF_STATES);
let channels = copy_channels(
energy_group.as_concrete_TypeRef(),
std::ptr::null(),
0,
0,
0,
);
if channels.is_null() {
return Err(GpuError::Query(format!(
"IOReport has no '{GROUP_ENERGY}' group on this host"
)));
}
let perf_channels = copy_channels(
gpu_group.as_concrete_TypeRef(),
perf_subgroup.as_concrete_TypeRef(),
0,
0,
0,
);
if !perf_channels.is_null() {
merge_channels(channels, perf_channels, std::ptr::null());
CFRelease(perf_channels as CFTypeRef);
}
let mut subscribed: CFMutableDictionaryRef = std::ptr::null_mut();
let subscription = create_subscription(
std::ptr::null(),
channels,
&mut subscribed,
0,
std::ptr::null(),
);
if subscription.is_null() {
CFRelease(channels as CFTypeRef);
return Err(GpuError::Query(
"IOReportCreateSubscription was refused".into(),
));
}
Ok(Self {
symbols,
subscription,
channels,
previous: None,
library,
})
}
}
pub fn sample(&mut self) -> Option<IntervalReport> {
unsafe {
let current = self.take_sample();
if current.is_null() {
return None;
}
let now = Instant::now();
let Some((previous, taken)) = self.previous.take() else {
self.previous = Some((current, now));
return None;
};
if now.duration_since(taken) < MIN_SAMPLE_INTERVAL {
CFRelease(current as CFTypeRef);
self.previous = Some((previous, taken));
return None;
}
let delta = (self.symbols.create_delta)(previous, current, std::ptr::null());
CFRelease(previous as CFTypeRef);
self.previous = Some((current, now));
if delta.is_null() {
return None;
}
let mut report = self.decode(delta);
report.elapsed = now.duration_since(taken);
CFRelease(delta as CFTypeRef);
Some(report)
}
}
unsafe fn take_sample(&self) -> CFDictionaryRef {
unsafe { (self.symbols.create_samples)(self.subscription, self.channels, std::ptr::null()) }
}
unsafe fn decode(&self, delta: CFDictionaryRef) -> IntervalReport {
let mut report = IntervalReport::default();
let mut have_preferred_energy = false;
unsafe {
let key = CFString::new("IOReportChannels");
let raw = CFDictionaryGetValue(delta, key.as_CFTypeRef().cast());
if raw.is_null() {
return report;
}
let items: CFArray<CFDictionary> = CFArray::wrap_under_get_rule(raw.cast());
for item in items.iter() {
let channel = item.as_concrete_TypeRef();
let group = cf_string((self.symbols.channel_group)(channel));
if group == GROUP_ENERGY {
let name = cf_string((self.symbols.channel_name)(channel));
let preferred = name == CHANNEL_GPU_ENERGY;
if preferred || (name == CHANNEL_GPU && !have_preferred_energy) {
let value = (self.symbols.simple_value)(channel, 0);
let unit = cf_string((self.symbols.channel_unit)(channel));
report.energy = Some((value, unit));
have_preferred_energy |= preferred;
}
continue;
}
if group == GROUP_GPU_STATS
&& cf_string((self.symbols.channel_subgroup)(channel)) == SUBGROUP_PERF_STATES
{
let count = (self.symbols.state_count)(channel);
for index in 0..count {
let name = cf_string((self.symbols.state_name)(channel, index));
let ticks = (self.symbols.state_residency)(channel, index);
report.residencies.push((name, ticks.max(0) as u64));
}
}
}
}
report
}
}
impl Drop for IoReportSampler {
fn drop(&mut self) {
unsafe {
if let Some((sample, _)) = self.previous.take() {
CFRelease(sample as CFTypeRef);
}
if !self.channels.is_null() {
CFRelease(self.channels as CFTypeRef);
}
if !self.subscription.is_null() {
CFRelease(self.subscription as CFTypeRef);
}
}
}
}
unsafe fn cf_string(raw: CFStringRef) -> String {
if raw.is_null() {
return String::new();
}
unsafe { CFString::wrap_under_get_rule(raw).to_string() }
}