use core_foundation::base::{CFRelease, CFRetain, CFType, CFTypeRef, TCFType};
use core_foundation::data::CFData;
use core_foundation::dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionaryRef};
use core_foundation::string::{CFString, CFStringRef};
use std::ffi::c_void;
use std::marker::{PhantomData, PhantomPinned};
use std::ptr;
use std::sync::OnceLock;
use std::time::Instant;
struct CFStringRefs {
energy_model: CFStringRef,
cpu_stats: CFStringRef,
cpu_perf_states: CFStringRef,
gpu_stats: CFStringRef,
gpu_perf_states: CFStringRef,
}
unsafe impl Send for CFStringRefs {}
unsafe impl Sync for CFStringRefs {}
impl CFStringRefs {
fn new() -> Self {
unsafe {
let energy_model = {
let s = CFString::new(ENERGY_MODEL);
let ptr = s.as_concrete_TypeRef();
CFRetain(ptr as *const c_void);
ptr
};
let cpu_stats = {
let s = CFString::new(CPU_STATS);
let ptr = s.as_concrete_TypeRef();
CFRetain(ptr as *const c_void);
ptr
};
let cpu_perf_states = {
let s = CFString::new(CPU_PERF_STATES);
let ptr = s.as_concrete_TypeRef();
CFRetain(ptr as *const c_void);
ptr
};
let gpu_stats = {
let s = CFString::new(GPU_STATS);
let ptr = s.as_concrete_TypeRef();
CFRetain(ptr as *const c_void);
ptr
};
let gpu_perf_states = {
let s = CFString::new(GPU_PERF_STATES);
let ptr = s.as_concrete_TypeRef();
CFRetain(ptr as *const c_void);
ptr
};
Self {
energy_model,
cpu_stats,
cpu_perf_states,
gpu_stats,
gpu_perf_states,
}
}
}
}
static CFSTRING_REFS: OnceLock<CFStringRefs> = OnceLock::new();
fn get_cfstring_refs() -> &'static CFStringRefs {
CFSTRING_REFS.get_or_init(CFStringRefs::new)
}
#[derive(Clone, Copy)]
struct ChannelGroupQuery {
group: CFStringRef,
subgroup: CFStringRef,
}
unsafe impl Send for ChannelGroupQuery {}
impl ChannelGroupQuery {
fn copy_channels(self) -> CFDictionaryRef {
unsafe { IOReportCopyChannelsInGroup(self.group, self.subgroup, 0, 0, 0) }
}
}
struct MergedChannels(CFDictionaryRef);
unsafe impl Send for MergedChannels {}
unsafe impl Sync for MergedChannels {}
static MERGED_CHANNELS: OnceLock<MergedChannels> = OnceLock::new();
fn build_merged_channels() -> Result<CFDictionaryRef, &'static str> {
let refs = get_cfstring_refs();
let energy = spawn_channel_query(refs.energy_model, ptr::null());
let cpu = spawn_channel_query(refs.cpu_stats, refs.cpu_perf_states);
let gpu = spawn_channel_query(refs.gpu_stats, refs.gpu_perf_states);
let energy_channels = energy.join();
let cpu_channels = cpu.join().unwrap_or(ptr::null());
let gpu_channels = gpu.join().unwrap_or(ptr::null());
let energy_channels = match energy_channels {
Ok(dict) if !dict.is_null() => dict,
outcome => {
for dict in [cpu_channels, gpu_channels] {
if !dict.is_null() {
unsafe { CFRelease(dict as *const c_void) };
}
}
return Err(match outcome {
Err(_) => "IOReport Energy Model channel enumeration panicked",
Ok(_) => "Failed to get Energy Model channels",
});
}
};
unsafe {
if !cpu_channels.is_null() {
IOReportMergeChannels(energy_channels, cpu_channels, ptr::null());
CFRelease(cpu_channels as *const c_void);
}
if !gpu_channels.is_null() {
IOReportMergeChannels(energy_channels, gpu_channels, ptr::null());
CFRelease(gpu_channels as *const c_void);
}
}
Ok(energy_channels)
}
struct OwnedDict(CFDictionaryRef);
unsafe impl Send for OwnedDict {}
enum ChannelQuery {
Spawned(std::thread::JoinHandle<OwnedDict>),
Inline(ChannelGroupQuery),
}
impl ChannelQuery {
fn join(self) -> Result<CFDictionaryRef, ()> {
match self {
Self::Spawned(handle) => handle.join().map(|OwnedDict(dict)| dict).map_err(|_| ()),
Self::Inline(query) => Ok(query.copy_channels()),
}
}
}
fn spawn_channel_query(group: CFStringRef, subgroup: CFStringRef) -> ChannelQuery {
let query = ChannelGroupQuery { group, subgroup };
match std::thread::Builder::new()
.name("all-smi-ioreport".to_string())
.spawn(move || OwnedDict(query.copy_channels()))
{
Ok(handle) => ChannelQuery::Spawned(handle),
Err(_) => ChannelQuery::Inline(query),
}
}
fn merged_channels() -> Result<CFDictionaryRef, &'static str> {
if let Some(cached) = MERGED_CHANNELS.get() {
return Ok(cached.0);
}
let built = build_merged_channels()?;
if let Err(MergedChannels(duplicate)) = MERGED_CHANNELS.set(MergedChannels(built)) {
unsafe { CFRelease(duplicate as *const c_void) };
}
MERGED_CHANNELS
.get()
.map(|cached| cached.0)
.ok_or("IOReport channel cache was not published")
}
#[repr(C)]
struct IOReportSubscription {
_data: [u8; 0],
_phantom: PhantomData<(*mut u8, PhantomPinned)>,
}
type IOReportSubscriptionRef = *const IOReportSubscription;
#[link(name = "IOReport", kind = "dylib")]
unsafe extern "C" {
fn IOReportCopyChannelsInGroup(
group: CFStringRef,
subgroup: CFStringRef,
a: u64,
b: u64,
c: u64,
) -> CFDictionaryRef;
fn IOReportMergeChannels(
a: CFDictionaryRef,
b: CFDictionaryRef,
nil: CFTypeRef,
) -> CFDictionaryRef;
fn IOReportCreateSubscription(
a: *const c_void,
desired_channels: CFMutableDictionaryRef,
subscribed_channels: *mut CFMutableDictionaryRef,
channel_id: u64,
b: CFTypeRef,
) -> IOReportSubscriptionRef;
fn IOReportCreateSamples(
subscription: IOReportSubscriptionRef,
channels: CFMutableDictionaryRef,
a: CFTypeRef,
) -> CFDictionaryRef;
fn IOReportCreateSamplesDelta(
prev: CFDictionaryRef,
curr: CFDictionaryRef,
a: CFTypeRef,
) -> CFDictionaryRef;
fn IOReportChannelGetGroup(channel: CFDictionaryRef) -> CFStringRef;
fn IOReportChannelGetSubGroup(channel: CFDictionaryRef) -> CFStringRef;
fn IOReportChannelGetChannelName(channel: CFDictionaryRef) -> CFStringRef;
fn IOReportChannelGetUnitLabel(channel: CFDictionaryRef) -> CFStringRef;
fn IOReportSimpleGetIntegerValue(channel: CFDictionaryRef, a: i32) -> i64;
fn IOReportStateGetCount(channel: CFDictionaryRef) -> i32;
fn IOReportStateGetNameForIndex(channel: CFDictionaryRef, index: i32) -> CFStringRef;
fn IOReportStateGetResidency(channel: CFDictionaryRef, index: i32) -> i64;
}
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn IOServiceMatching(name: *const i8) -> *mut c_void;
fn IOServiceGetMatchingServices(
master_port: u32,
matching: *mut c_void,
existing: *mut u32,
) -> i32;
fn IOIteratorNext(iterator: u32) -> u32;
fn IORegistryEntryGetName(entry: u32, name: *mut i8) -> i32;
fn IORegistryEntryCreateCFProperties(
entry: u32,
properties: *mut CFMutableDictionaryRef,
allocator: *const c_void,
options: u32,
) -> i32;
fn IOObjectRelease(object: u32) -> i32;
}
static PMGR_VOLTAGE_STATES: OnceLock<Vec<(String, Vec<u32>)>> = OnceLock::new();
static GPU_FREQUENCIES: OnceLock<Vec<u32>> = OnceLock::new();
const GPU_TABLE_KEYS: [&str; 2] = ["voltage-states9-sram", "voltage-states9"];
const E_CLUSTER_TABLE_KEYS: [&str; 2] = ["voltage-states1-sram", "voltage-states1"];
const P_CLUSTER_TABLE_KEYS: [&str; 2] = ["voltage-states5-sram", "voltage-states5"];
fn load_pmgr_voltage_states() -> Vec<(String, Vec<u32>)> {
unsafe {
let matching = IOServiceMatching(c"AppleARMIODevice".as_ptr());
if matching.is_null() {
return vec![];
}
let mut iterator: u32 = 0;
if IOServiceGetMatchingServices(0, matching, &mut iterator) != 0 {
return vec![];
}
let mut tables: Vec<(String, Vec<u32>)> = vec![];
let mut entry = IOIteratorNext(iterator);
while entry != 0 {
let mut name_buf = [0i8; 128];
IORegistryEntryGetName(entry, name_buf.as_mut_ptr());
let name = std::ffi::CStr::from_ptr(name_buf.as_ptr())
.to_str()
.unwrap_or("");
if name == "pmgr" || name == "clpc" {
let mut properties: CFMutableDictionaryRef = ptr::null_mut();
if IORegistryEntryCreateCFProperties(entry, &mut properties, ptr::null(), 0) == 0
&& !properties.is_null()
{
tables = extract_voltage_state_tables(properties);
CFRelease(properties as *const c_void);
}
}
IOObjectRelease(entry);
if !tables.is_empty() {
break; }
entry = IOIteratorNext(iterator);
}
IOObjectRelease(iterator);
tables
}
}
fn extract_voltage_state_tables(properties: CFMutableDictionaryRef) -> Vec<(String, Vec<u32>)> {
unsafe {
let count = core_foundation::dictionary::CFDictionaryGetCount(properties) as usize;
if count == 0 {
return vec![];
}
let mut keys: Vec<*const c_void> = vec![ptr::null(); count];
let mut values: Vec<*const c_void> = vec![ptr::null(); count];
core_foundation::dictionary::CFDictionaryGetKeysAndValues(
properties,
keys.as_mut_ptr(),
values.as_mut_ptr(),
);
let mut tables: Vec<(String, Vec<u32>)> = Vec::new();
for i in 0..count {
let key_ref = keys[i] as CFStringRef;
if key_ref.is_null() {
continue;
}
let key_str = cfstr_to_string(key_ref).unwrap_or_default();
if !key_str.starts_with("voltage-states") {
continue;
}
let data_ref = values[i] as core_foundation::data::CFDataRef;
if data_ref.is_null() {
continue;
}
let frequencies = parse_voltage_states_data(data_ref);
if frequencies.is_empty() {
continue;
}
tables.push((key_str, frequencies));
}
tables.sort_by(|a, b| a.0.cmp(&b.0));
tables
}
}
const MIN_FREQ_HZ: u64 = 100_000_000;
const MAX_FREQ_HZ: u64 = 6_000_000_000;
const U32_SPAN_HZ: u64 = 1 << 32;
const WRAP_GUARD_HZ: u64 = 4_000_000_000;
const MAX_FREQ_ENTRIES: usize = 64;
fn parse_voltage_states_data(data_ref: core_foundation::data::CFDataRef) -> Vec<u32> {
unsafe {
let data = CFData::wrap_under_get_rule(data_ref);
parse_voltage_states_bytes(data.bytes())
}
}
fn parse_voltage_states_bytes(bytes: &[u8]) -> Vec<u32> {
let len = bytes.len();
let total_entries = (len / 8).min(MAX_FREQ_ENTRIES);
let mut frequencies: Vec<u32> = Vec::with_capacity(total_entries);
let mut prev_hz: u64 = 0;
for i in 0..total_entries {
let offset = i * 8;
if offset + 4 > len {
break;
}
let raw_hz = u32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
]) as u64;
let freq_hz = if prev_hz >= WRAP_GUARD_HZ && raw_hz < prev_hz {
raw_hz + U32_SPAN_HZ
} else {
raw_hz
};
if !(MIN_FREQ_HZ..=MAX_FREQ_HZ).contains(&freq_hz) {
continue;
}
prev_hz = freq_hz;
frequencies.push((freq_hz / 1_000_000) as u32);
}
frequencies
}
fn get_pmgr_voltage_states() -> &'static [(String, Vec<u32>)] {
PMGR_VOLTAGE_STATES.get_or_init(load_pmgr_voltage_states)
}
pub fn get_gpu_frequencies() -> &'static [u32] {
GPU_FREQUENCIES.get_or_init(|| select_gpu_frequency_table(get_pmgr_voltage_states()))
}
fn select_gpu_frequency_table(tables: &[(String, Vec<u32>)]) -> Vec<u32> {
for key in GPU_TABLE_KEYS {
if let Some((_, frequencies)) = tables.iter().find(|(k, _)| k == key) {
return frequencies.clone();
}
}
tables
.iter()
.min_by_key(|(_, frequencies)| frequencies.iter().max().copied().unwrap_or(u32::MAX))
.map(|(_, frequencies)| frequencies.clone())
.unwrap_or_default()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CpuCluster {
Super,
Efficiency,
Performance,
}
fn strip_die_prefix(channel: &str) -> &str {
let Some(rest) = channel.strip_prefix("DIE_") else {
return channel;
};
let digits_end = rest
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(rest.len());
if digits_end == 0 {
return channel;
}
rest[digits_end..].strip_prefix('_').unwrap_or(channel)
}
fn classify_cpu_channel(channel: &str) -> Option<CpuCluster> {
let name = strip_die_prefix(channel);
if name.starts_with("MCPU0") {
Some(CpuCluster::Super)
} else if name.starts_with("MCPU1") {
Some(CpuCluster::Performance)
} else if name.contains("ECPU") || name.starts_with('E') {
Some(CpuCluster::Efficiency)
} else if name.contains("PCPU") || name.starts_with('P') {
Some(CpuCluster::Performance)
} else {
None
}
}
fn select_cpu_frequency_table(
tables: &[(String, Vec<u32>)],
cluster: CpuCluster,
active_states: usize,
) -> Option<&[u32]> {
let preferred: &[&str] = match cluster {
CpuCluster::Efficiency => &E_CLUSTER_TABLE_KEYS,
CpuCluster::Performance => &P_CLUSTER_TABLE_KEYS,
CpuCluster::Super => &[],
};
for key in preferred {
if let Some((_, frequencies)) = tables.iter().find(|(k, _)| k == key)
&& frequencies.len() == active_states
{
return Some(frequencies);
}
}
if active_states > 0 {
let matched = tables
.iter()
.find(|(k, f)| f.len() == active_states && k.ends_with("-sram"))
.or_else(|| tables.iter().find(|(_, f)| f.len() == active_states));
if let Some((_, frequencies)) = matched {
return Some(frequencies);
}
}
preferred
.iter()
.find_map(|key| tables.iter().find(|(k, _)| k == key))
.map(|(_, frequencies)| frequencies.as_slice())
}
fn is_idle_state(name: &str) -> bool {
name.contains("IDLE") || name.contains("OFF") || name.contains("DOWN")
}
fn cfstr_to_string(cfstr: CFStringRef) -> Option<String> {
if cfstr.is_null() {
return None;
}
unsafe {
let cf_string = CFString::wrap_under_get_rule(cfstr);
Some(cf_string.to_string())
}
}
fn get_io_channels(dict: CFDictionaryRef) -> Vec<CFDictionaryRef> {
if dict.is_null() {
return vec![];
}
unsafe {
let cf_dict = CFDictionary::<CFType, CFType>::wrap_under_get_rule(dict);
let key = CFString::new("IOReportChannels");
if let Some(channels) = cf_dict.find(key.as_CFType().as_CFTypeRef()) {
let arr_ref = channels.as_CFTypeRef() as core_foundation::array::CFArrayRef;
if arr_ref.is_null() {
return vec![];
}
let arr = core_foundation::array::CFArray::<CFType>::wrap_under_get_rule(arr_ref);
let count = arr.len();
(0..count)
.filter_map(|i| arr.get(i).map(|v| v.as_CFTypeRef() as CFDictionaryRef))
.filter(|d| !d.is_null())
.collect()
} else {
vec![]
}
}
}
#[derive(Debug, Clone)]
pub struct IOReportChannelItem {
pub group: String,
pub subgroup: String,
pub channel: String,
pub unit: String,
pub item: CFDictionaryRef,
}
impl IOReportChannelItem {
pub fn get_integer_value(&self) -> i64 {
if self.item.is_null() {
return 0;
}
unsafe { IOReportSimpleGetIntegerValue(self.item, 0) }
}
pub fn get_residencies(&self) -> Vec<(String, i64)> {
if self.item.is_null() {
return vec![];
}
unsafe {
let count = IOReportStateGetCount(self.item);
(0..count)
.filter_map(|i| {
let name_ref = IOReportStateGetNameForIndex(self.item, i);
let name = cfstr_to_string(name_ref)?;
let residency = IOReportStateGetResidency(self.item, i);
Some((name, residency))
})
.collect()
}
}
pub fn calculate_watts(&self, duration_ns: u64) -> f64 {
let value = self.get_integer_value();
if value <= 0 || duration_ns == 0 {
return 0.0;
}
let unit_factor = match self.unit.as_str() {
"mJ" => 1e-3, "uJ" => 1e-6, "nJ" => 1e-9, _ => 1e-9, };
let energy_joules = value as f64 * unit_factor;
let duration_secs = duration_ns as f64 / 1e9;
energy_joules / duration_secs
}
}
pub struct IOReportIterator {
sample: CFDictionaryRef,
channels: Vec<CFDictionaryRef>,
index: usize,
}
impl IOReportIterator {
fn new(sample: CFDictionaryRef) -> Self {
let channels = get_io_channels(sample);
Self {
sample,
channels,
index: 0,
}
}
}
impl Drop for IOReportIterator {
fn drop(&mut self) {
if !self.sample.is_null() {
unsafe {
CFRelease(self.sample as *const c_void);
}
}
}
}
impl Iterator for IOReportIterator {
type Item = IOReportChannelItem;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.channels.len() {
return None;
}
let item = self.channels[self.index];
self.index += 1;
if item.is_null() {
return self.next();
}
unsafe {
let group = cfstr_to_string(IOReportChannelGetGroup(item)).unwrap_or_default();
let subgroup = cfstr_to_string(IOReportChannelGetSubGroup(item)).unwrap_or_default();
let channel = cfstr_to_string(IOReportChannelGetChannelName(item)).unwrap_or_default();
let unit = cfstr_to_string(IOReportChannelGetUnitLabel(item)).unwrap_or_default();
Some(IOReportChannelItem {
group,
subgroup,
channel,
unit,
item,
})
}
}
}
const ENERGY_MODEL: &str = "Energy Model";
const CPU_STATS: &str = "CPU Stats";
const CPU_PERF_STATES: &str = "CPU Core Performance States";
const GPU_STATS: &str = "GPU Stats";
const GPU_PERF_STATES: &str = "GPU Performance States";
const MIN_DELTA_WINDOW: std::time::Duration = std::time::Duration::from_millis(50);
pub struct IOReport {
subscription: IOReportSubscriptionRef,
channels: CFMutableDictionaryRef,
prev_sample: Option<(CFDictionaryRef, Instant)>,
}
impl IOReport {
pub fn new() -> Result<Self, &'static str> {
let merged = merged_channels()?;
unsafe {
let count = core_foundation::dictionary::CFDictionaryGetCount(merged) as isize;
let channels = core_foundation::dictionary::CFDictionaryCreateMutableCopy(
core_foundation::base::kCFAllocatorDefault,
count,
merged,
);
if channels.is_null() {
return Err("Failed to create mutable channel dictionary");
}
let mut subscribed_channels: CFMutableDictionaryRef = ptr::null_mut();
let subscription = IOReportCreateSubscription(
ptr::null(),
channels,
&mut subscribed_channels,
0,
ptr::null(),
);
if subscription.is_null() {
CFRelease(channels as *const c_void);
return Err("Failed to create IOReport subscription");
}
Ok(Self {
subscription,
channels,
prev_sample: None,
})
}
}
pub fn get_sample(
&mut self,
duration_ms: u64,
) -> Result<(IOReportIterator, u64), &'static str> {
let sample1 = self.take_sample()?;
let start = Instant::now();
std::thread::sleep(std::time::Duration::from_millis(duration_ms));
let sample2 = self.take_sample()?;
let duration_ns = start.elapsed().as_nanos() as u64;
let delta = unsafe {
let d = IOReportCreateSamplesDelta(sample1, sample2, ptr::null());
CFRelease(sample1 as *const c_void);
CFRelease(sample2 as *const c_void);
d
};
if delta.is_null() {
return Err("Failed to create sample delta");
}
Ok((IOReportIterator::new(delta), duration_ns))
}
pub fn get_sample_since_last(
&mut self,
) -> Result<Option<(IOReportIterator, u64)>, &'static str> {
let sample = self.take_sample()?;
let now = Instant::now();
if let Some((_, prev_at)) = self.prev_sample.as_ref()
&& now.duration_since(*prev_at) < MIN_DELTA_WINDOW
{
unsafe { CFRelease(sample as *const c_void) };
return Ok(None);
}
let Some((prev, prev_at)) = self.prev_sample.replace((sample, now)) else {
return Ok(None);
};
let duration_ns = now.duration_since(prev_at).as_nanos() as u64;
let delta = unsafe {
let d = IOReportCreateSamplesDelta(prev, sample, ptr::null());
CFRelease(prev as *const c_void);
d
};
if delta.is_null() {
return Err("Failed to create sample delta");
}
Ok(Some((IOReportIterator::new(delta), duration_ns)))
}
fn take_sample(&self) -> Result<CFDictionaryRef, &'static str> {
unsafe {
let sample = IOReportCreateSamples(self.subscription, self.channels, ptr::null());
if sample.is_null() {
return Err("Failed to create IOReport sample");
}
Ok(sample)
}
}
}
impl Drop for IOReport {
fn drop(&mut self) {
unsafe {
if let Some((prev, _)) = self.prev_sample.take()
&& !prev.is_null()
{
CFRelease(prev as *const c_void);
}
if !self.channels.is_null() {
CFRelease(self.channels as *const c_void);
}
}
}
}
unsafe impl Send for IOReport {}
unsafe impl Sync for IOReport {}
#[derive(Debug, Default, Clone)]
pub struct IOReportMetrics {
pub cpu_power: f64,
pub gpu_power: f64,
pub ane_power: f64,
pub dram_power: f64,
pub package_power: f64,
pub s_cluster_freq: u32,
pub e_cluster_freq: u32,
pub p_cluster_freq: u32,
pub s_cluster_residency: f64,
pub e_cluster_residency: f64,
pub p_cluster_residency: f64,
pub gpu_freq: u32,
pub gpu_residency: f64,
pub s_cluster_data: Vec<(u32, f64)>, pub e_cluster_data: Vec<(u32, f64)>, pub p_cluster_data: Vec<(u32, f64)>,
}
impl IOReportMetrics {
pub fn from_sample(iterator: IOReportIterator, duration_ns: u64) -> Self {
let mut metrics = Self::default();
let mut s_cluster_freqs: Vec<(u32, f64)> = vec![];
let mut e_cluster_freqs: Vec<(u32, f64)> = vec![];
let mut p_cluster_freqs: Vec<(u32, f64)> = vec![];
let mut gpu_freqs: Vec<(u32, f64)> = vec![];
for item in iterator {
match (item.group.as_str(), item.subgroup.as_str()) {
("Energy Model", _) => {
Self::process_energy_channel(&item, duration_ns, &mut metrics);
}
("CPU Stats", "CPU Core Performance States") => {
Self::process_cpu_channel(
&item,
&mut s_cluster_freqs,
&mut e_cluster_freqs,
&mut p_cluster_freqs,
);
}
("GPU Stats", "GPU Performance States") if item.channel == "GPUPH" => {
Self::process_gpu_channel(&item, &mut gpu_freqs);
}
_ => {}
}
}
metrics.s_cluster_data = s_cluster_freqs.clone();
metrics.e_cluster_data = e_cluster_freqs.clone();
metrics.p_cluster_data = p_cluster_freqs.clone();
if let Some((freq, residency)) = Self::calculate_cluster_average(&s_cluster_freqs) {
metrics.s_cluster_freq = freq;
metrics.s_cluster_residency = residency;
}
if let Some((freq, residency)) = Self::calculate_cluster_average(&e_cluster_freqs) {
metrics.e_cluster_freq = freq;
metrics.e_cluster_residency = residency;
}
if let Some((freq, residency)) = Self::calculate_cluster_average(&p_cluster_freqs) {
metrics.p_cluster_freq = freq;
metrics.p_cluster_residency = residency;
}
if let Some((freq, residency)) = Self::calculate_cluster_average(&gpu_freqs) {
metrics.gpu_freq = freq;
metrics.gpu_residency = residency;
}
metrics
}
fn process_energy_channel(item: &IOReportChannelItem, duration_ns: u64, metrics: &mut Self) {
let watts = item.calculate_watts(duration_ns);
let channel = item.channel.as_str();
if channel.contains("CPU") && !channel.contains("GPU") {
metrics.cpu_power += watts;
} else if channel.contains("GPU") && !channel.contains("CPU") {
metrics.gpu_power += watts;
} else if channel.contains("ANE") {
metrics.ane_power += watts;
} else if channel.contains("DRAM") {
metrics.dram_power += watts;
}
if channel == "CPU Energy" || channel.starts_with("CPU") {
metrics.package_power = metrics.cpu_power + metrics.gpu_power + metrics.ane_power;
}
}
fn process_cpu_channel(
item: &IOReportChannelItem,
s_cluster_freqs: &mut Vec<(u32, f64)>,
e_cluster_freqs: &mut Vec<(u32, f64)>,
p_cluster_freqs: &mut Vec<(u32, f64)>,
) {
let residencies = item.get_residencies();
if residencies.is_empty() {
return;
}
let Some(cluster) = classify_cpu_channel(&item.channel) else {
return;
};
let active_states = residencies
.iter()
.filter(|(name, _)| !is_idle_state(name))
.count();
let table = select_cpu_frequency_table(get_pmgr_voltage_states(), cluster, active_states);
let (freq, residency) = match table {
Some(table) if !table.is_empty() => Self::calc_freq_with_table(&residencies, table),
_ => Self::calc_freq_from_residencies(&residencies),
};
match cluster {
CpuCluster::Super => s_cluster_freqs.push((freq, residency)),
CpuCluster::Efficiency => e_cluster_freqs.push((freq, residency)),
CpuCluster::Performance => p_cluster_freqs.push((freq, residency)),
}
}
fn process_gpu_channel(item: &IOReportChannelItem, gpu_freqs: &mut Vec<(u32, f64)>) {
let residencies = item.get_residencies();
if residencies.is_empty() {
return;
}
let gpu_freq_table = get_gpu_frequencies();
let (freq, residency) = if !gpu_freq_table.is_empty() {
Self::calc_freq_with_table(&residencies, gpu_freq_table)
} else {
Self::calc_freq_from_residencies(&residencies)
};
gpu_freqs.push((freq, residency));
}
fn calc_freq_with_table(residencies: &[(String, i64)], freq_table: &[u32]) -> (u32, f64) {
let mut total_residency: i64 = 0;
let mut active_residency: i64 = 0;
let mut weighted_freq: f64 = 0.0;
let mut active_state_idx: usize = 0;
for (name, residency) in residencies {
total_residency += residency;
if is_idle_state(name) {
continue;
}
active_residency += residency;
if active_state_idx < freq_table.len() {
weighted_freq += freq_table[active_state_idx] as f64 * *residency as f64;
}
active_state_idx += 1;
}
if total_residency == 0 {
return (0, 0.0);
}
let avg_freq = if active_residency > 0 {
(weighted_freq / active_residency as f64) as u32
} else if !freq_table.is_empty() {
freq_table[0]
} else {
0
};
let residency_pct = (active_residency as f64 / total_residency as f64) * 100.0;
(avg_freq, residency_pct)
}
fn calc_freq_from_residencies(residencies: &[(String, i64)]) -> (u32, f64) {
let mut total_residency: i64 = 0;
let mut weighted_freq: i64 = 0;
let mut active_residency: i64 = 0;
let mut min_active_freq: Option<i64> = None;
for (name, residency) in residencies {
total_residency += residency;
if is_idle_state(name) {
continue;
}
active_residency += residency;
if let Ok(freq) = name.trim().parse::<i64>() {
weighted_freq += freq * residency;
min_active_freq = Some(min_active_freq.map_or(freq, |m| m.min(freq)));
}
}
if total_residency == 0 {
return (0, 0.0);
}
let avg_freq = if active_residency > 0 {
(weighted_freq / active_residency) as u32
} else {
min_active_freq.unwrap_or(0) as u32
};
let residency_pct = (active_residency as f64 / total_residency as f64) * 100.0;
(avg_freq, residency_pct)
}
fn calculate_cluster_average(data: &[(u32, f64)]) -> Option<(u32, f64)> {
if data.is_empty() {
return None;
}
let count = data.len() as f64;
let avg_freq = data.iter().map(|(f, _)| *f as f64).sum::<f64>() / count;
let avg_residency = data.iter().map(|(_, r)| *r).sum::<f64>() / count;
Some((avg_freq as u32, avg_residency))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(target_os = "macos")]
fn channel_enumeration_happens_once_per_process() {
let Ok(first) = merged_channels() else {
return;
};
let second = merged_channels().expect("a cached lookup cannot start failing");
assert_eq!(
first, second,
"the merged channel dictionary must be enumerated once and reused"
);
}
#[test]
#[cfg(target_os = "macos")]
fn each_subscription_copies_the_cached_channels() {
let Ok(cached) = merged_channels() else {
return;
};
let Ok(report) = IOReport::new() else {
return;
};
assert_ne!(
report.channels as CFDictionaryRef, cached,
"a subscription must not hold the cached dictionary itself"
);
}
#[test]
#[cfg(target_os = "macos")]
fn two_subscriptions_can_coexist() {
let Ok(first) = IOReport::new() else {
return;
};
let Ok(second) = IOReport::new() else {
return;
};
assert!(!first.channels.is_null());
assert!(!second.channels.is_null());
assert_ne!(
first.channels, second.channels,
"each subscription needs its own channel dictionary"
);
}
#[test]
fn test_calc_freq_from_residencies() {
let residencies = vec![
("IDLE".to_string(), 500),
("600".to_string(), 100),
("1200".to_string(), 200),
("2400".to_string(), 200),
];
let (freq, residency) = IOReportMetrics::calc_freq_from_residencies(&residencies);
assert!((residency - 50.0).abs() < 0.1);
assert_eq!(freq, 1560);
}
#[test]
fn test_calculate_cluster_average() {
let data = vec![(1000, 50.0), (2000, 60.0), (1500, 40.0)];
let result = IOReportMetrics::calculate_cluster_average(&data);
assert!(result.is_some());
let (avg_freq, avg_residency) = result.unwrap();
assert_eq!(avg_freq, 1500);
assert!((avg_residency - 50.0).abs() < 0.1);
}
#[test]
fn test_calculate_cluster_average_empty() {
let result = IOReportMetrics::calculate_cluster_average(&[]);
assert!(result.is_none());
}
#[test]
fn test_calc_gpu_freq_with_table() {
let residencies = vec![
("OFF".to_string(), 100),
("IDLE".to_string(), 400),
("state0".to_string(), 200), ("state1".to_string(), 200), ("state2".to_string(), 100), ];
let freq_table = [396, 720, 1398];
let (freq, residency) = IOReportMetrics::calc_freq_with_table(&residencies, &freq_table);
assert!((residency - 50.0).abs() < 0.1);
assert_eq!(freq, 726);
}
#[test]
fn test_calc_gpu_freq_with_empty_table() {
let residencies = vec![("OFF".to_string(), 100), ("state0".to_string(), 200)];
let freq_table: [u32; 0] = [];
let (freq, residency) = IOReportMetrics::calc_freq_with_table(&residencies, &freq_table);
assert!((residency - 66.67).abs() < 0.1);
assert_eq!(freq, 0);
}
#[test]
fn test_calc_freq_from_residencies_all_idle() {
let residencies = vec![("IDLE".to_string(), 500)];
let (freq, residency) = IOReportMetrics::calc_freq_from_residencies(&residencies);
assert_eq!(freq, 0);
assert!((residency - 0.0).abs() < 0.1);
}
#[test]
fn test_calc_freq_from_residencies_idle_with_known_states() {
let residencies = vec![
("IDLE".to_string(), 500),
("600".to_string(), 0),
("1200".to_string(), 0),
];
let (freq, residency) = IOReportMetrics::calc_freq_from_residencies(&residencies);
assert_eq!(freq, 600);
assert!((residency - 0.0).abs() < 0.1);
}
#[test]
fn test_calc_gpu_freq_with_table_idle() {
let residencies = vec![("OFF".to_string(), 200), ("IDLE".to_string(), 800)];
let freq_table = [396, 720, 1398];
let (freq, residency) = IOReportMetrics::calc_freq_with_table(&residencies, &freq_table);
assert_eq!(freq, 396);
assert!((residency - 0.0).abs() < 0.1);
}
#[test]
fn test_calc_gpu_freq_with_table_empty_table_idle() {
let residencies = vec![("OFF".to_string(), 200), ("IDLE".to_string(), 800)];
let freq_table: [u32; 0] = [];
let (freq, residency) = IOReportMetrics::calc_freq_with_table(&residencies, &freq_table);
assert_eq!(freq, 0);
assert!((residency - 0.0).abs() < 0.1);
}
const M1_ULTRA_P_TABLE: [u32; 15] = [
600, 828, 1056, 1296, 1524, 1752, 1980, 2208, 2448, 2676, 2904, 3036, 3132, 3168, 3228,
];
const M1_ULTRA_E_TABLE: [u32; 5] = [600, 972, 1332, 1704, 2064];
const M1_ULTRA_GPU_TABLE: [u32; 6] = [388, 486, 648, 777, 972, 1296];
fn encode_voltage_states(freqs_hz: &[u64]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(freqs_hz.len() * 8);
for (i, hz) in freqs_hz.iter().enumerate() {
bytes.extend_from_slice(&(*hz as u32).to_le_bytes());
bytes.extend_from_slice(&(700 + i as u32).to_le_bytes());
}
bytes
}
fn residencies(pairs: &[(&str, i64)]) -> Vec<(String, i64)> {
pairs.iter().map(|(n, r)| ((*n).to_string(), *r)).collect()
}
fn tables(entries: &[(&str, &[u32])]) -> Vec<(String, Vec<u32>)> {
let mut out: Vec<(String, Vec<u32>)> = entries
.iter()
.map(|(k, v)| ((*k).to_string(), v.to_vec()))
.collect();
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
fn m1_ultra_tables() -> Vec<(String, Vec<u32>)> {
tables(&[
("voltage-states1-sram", &M1_ULTRA_E_TABLE),
("voltage-states5-sram", &M1_ULTRA_P_TABLE),
("voltage-states9-sram", &M1_ULTRA_GPU_TABLE),
("voltage-states9", &M1_ULTRA_GPU_TABLE),
("voltage-states13-sram", &M1_ULTRA_P_TABLE),
])
}
#[test]
fn test_parse_voltage_states_bytes_reads_frequency_table() {
let hz: Vec<u64> = M1_ULTRA_P_TABLE
.iter()
.map(|m| *m as u64 * 1_000_000)
.collect();
let parsed = parse_voltage_states_bytes(&encode_voltage_states(&hz));
assert_eq!(parsed, M1_ULTRA_P_TABLE.to_vec());
}
#[test]
fn test_parse_voltage_states_bytes_rejects_period_table() {
let periods: Vec<u64> = vec![
109226, 79149, 62060, 50567, 43002, 37406, 33098, 29681, 26771, 24490, 22567, 21586,
20924, 20686, 20302,
];
let parsed = parse_voltage_states_bytes(&encode_voltage_states(&periods));
assert!(
parsed.is_empty(),
"period table must not parse as frequencies, got {parsed:?}"
);
}
#[test]
fn test_parse_voltage_states_bytes_undoes_u32_wraparound() {
let hz: Vec<u64> = vec![3_000_000_000, 4_100_000_000, 4_512_000_000, 4_608_000_000];
let parsed = parse_voltage_states_bytes(&encode_voltage_states(&hz));
assert_eq!(parsed, vec![3000, 4100, 4512, 4608]);
}
#[test]
fn test_parse_voltage_states_bytes_ignores_trailing_partial_entry() {
let mut bytes = encode_voltage_states(&[600_000_000, 972_000_000]);
bytes.extend_from_slice(&[0x11, 0x22]);
assert_eq!(parse_voltage_states_bytes(&bytes), vec![600, 972]);
}
#[test]
fn test_strip_die_prefix() {
assert_eq!(strip_die_prefix("DIE_0_ECPU_CPU0"), "ECPU_CPU0");
assert_eq!(strip_die_prefix("DIE_1_PCPU1_CPU3"), "PCPU1_CPU3");
assert_eq!(strip_die_prefix("DIE_12_PCPU_CPU0"), "PCPU_CPU0");
assert_eq!(strip_die_prefix("ECPU0"), "ECPU0");
assert_eq!(strip_die_prefix("DIE_ECPU"), "DIE_ECPU");
}
#[test]
fn test_classify_cpu_channel() {
assert_eq!(
classify_cpu_channel("DIE_0_ECPU_CPU0"),
Some(CpuCluster::Efficiency)
);
assert_eq!(
classify_cpu_channel("DIE_1_ECPU_CPU1"),
Some(CpuCluster::Efficiency)
);
assert_eq!(
classify_cpu_channel("DIE_0_PCPU_CPU0"),
Some(CpuCluster::Performance)
);
assert_eq!(
classify_cpu_channel("DIE_1_PCPU1_CPU3"),
Some(CpuCluster::Performance)
);
assert_eq!(classify_cpu_channel("ECPU"), Some(CpuCluster::Efficiency));
assert_eq!(classify_cpu_channel("ECPU0"), Some(CpuCluster::Efficiency));
assert_eq!(classify_cpu_channel("PCPU"), Some(CpuCluster::Performance));
assert_eq!(classify_cpu_channel("PCPU1"), Some(CpuCluster::Performance));
assert_eq!(classify_cpu_channel("MCPU0"), Some(CpuCluster::Super));
assert_eq!(classify_cpu_channel("MCPU05"), Some(CpuCluster::Super));
assert_eq!(classify_cpu_channel("DIE_0_MCPU0"), Some(CpuCluster::Super));
assert_eq!(classify_cpu_channel("MCPU1"), Some(CpuCluster::Performance));
assert_eq!(
classify_cpu_channel("DIE_1_MCPU15"),
Some(CpuCluster::Performance)
);
assert_eq!(classify_cpu_channel("GPUPH"), None);
assert_eq!(classify_cpu_channel(""), None);
}
#[test]
fn test_select_cpu_frequency_table_prefers_documented_key() {
let tables = m1_ultra_tables();
let e = select_cpu_frequency_table(&tables, CpuCluster::Efficiency, 5).unwrap();
assert_eq!(e, M1_ULTRA_E_TABLE);
let p = select_cpu_frequency_table(&tables, CpuCluster::Performance, 15).unwrap();
assert_eq!(p, M1_ULTRA_P_TABLE);
}
#[test]
fn test_select_cpu_frequency_table_falls_back_on_length_mismatch() {
let tables = tables(&[
("voltage-states2-sram", &M1_ULTRA_E_TABLE),
("voltage-states5-sram", &M1_ULTRA_P_TABLE),
]);
let e = select_cpu_frequency_table(&tables, CpuCluster::Efficiency, 5).unwrap();
assert_eq!(e, M1_ULTRA_E_TABLE);
}
#[test]
fn test_select_cpu_frequency_table_super_cluster_uses_length_match() {
let super_table: [u32; 4] = [800, 1600, 2800, 4000];
let tables = tables(&[
("voltage-states1-sram", &M1_ULTRA_E_TABLE),
("voltage-states7-sram", &super_table),
]);
let s = select_cpu_frequency_table(&tables, CpuCluster::Super, 4).unwrap();
assert_eq!(s, super_table);
assert!(select_cpu_frequency_table(&tables, CpuCluster::Super, 9).is_none());
}
#[test]
fn test_select_cpu_frequency_table_last_resort_uses_documented_key() {
let tables = tables(&[("voltage-states5-sram", &M1_ULTRA_P_TABLE)]);
let p = select_cpu_frequency_table(&tables, CpuCluster::Performance, 99).unwrap();
assert_eq!(p, M1_ULTRA_P_TABLE);
assert!(select_cpu_frequency_table(&[], CpuCluster::Performance, 15).is_none());
}
#[test]
fn test_select_gpu_frequency_table() {
assert_eq!(
select_gpu_frequency_table(&m1_ultra_tables()),
M1_ULTRA_GPU_TABLE.to_vec()
);
let odd = tables(&[
("voltage-states1-sram", &M1_ULTRA_E_TABLE),
("voltage-states5-sram", &M1_ULTRA_P_TABLE),
("voltage-states22-sram", &M1_ULTRA_GPU_TABLE),
]);
assert_eq!(
select_gpu_frequency_table(&odd),
M1_ULTRA_GPU_TABLE.to_vec()
);
assert!(select_gpu_frequency_table(&[]).is_empty());
}
#[test]
fn test_cpu_performance_states_are_not_numeric() {
let res = residencies(&[
("IDLE", 2076468),
("V0P14", 0),
("V1P13", 0),
("V2P12", 1874),
("V3P11", 0),
("V4P10", 0),
("V5P9", 953),
("V6P8", 0),
("V7P7", 0),
("V8P6", 0),
("V9P5", 0),
("V10P4", 0),
("V11P3", 0),
("V12P2", 0),
("V13P1", 0),
("V14P0", 5363773),
]);
let (freq, residency) = IOReportMetrics::calc_freq_from_residencies(&res);
assert_eq!(freq, 0, "state names carry no megahertz value");
assert!((residency - 72.10).abs() < 0.01);
}
#[test]
fn test_p_cluster_frequency_from_real_m1_ultra_sample() {
let res = residencies(&[
("IDLE", 2076468),
("V0P14", 0),
("V1P13", 0),
("V2P12", 1874),
("V3P11", 0),
("V4P10", 0),
("V5P9", 953),
("V6P8", 0),
("V7P7", 0),
("V8P6", 0),
("V9P5", 0),
("V10P4", 0),
("V11P3", 0),
("V12P2", 0),
("V13P1", 0),
("V14P0", 5363773),
]);
let (freq, residency) = IOReportMetrics::calc_freq_with_table(&res, &M1_ULTRA_P_TABLE);
assert_eq!(freq, 3226);
assert!((residency - 72.10).abs() < 0.01);
assert!(freq <= *M1_ULTRA_P_TABLE.last().unwrap());
}
#[test]
fn test_e_cluster_frequency_from_real_m1_ultra_sample() {
let res = residencies(&[
("IDLE", 1999280),
("V0P4", 0),
("V1P3", 511855),
("V2P2", 576745),
("V3P1", 568251),
("V4P0", 3786908),
]);
let (freq, residency) = IOReportMetrics::calc_freq_with_table(&res, &M1_ULTRA_E_TABLE);
assert_eq!(freq, 1846);
assert!((residency - 73.14).abs() < 0.01);
assert!(freq >= M1_ULTRA_E_TABLE[0] && freq <= *M1_ULTRA_E_TABLE.last().unwrap());
}
#[test]
fn test_idle_cpu_cluster_reports_parked_clock_not_zero() {
let mut pairs = vec![("IDLE", 7478905)];
for name in [
"V0P14", "V1P13", "V2P12", "V3P11", "V4P10", "V5P9", "V6P8", "V7P7", "V8P6", "V9P5",
"V10P4", "V11P3", "V12P2", "V13P1", "V14P0",
] {
pairs.push((name, 0));
}
let (freq, residency) =
IOReportMetrics::calc_freq_with_table(&residencies(&pairs), &M1_ULTRA_P_TABLE);
assert_eq!(freq, 600);
assert!((residency - 0.0).abs() < 0.01);
}
#[test]
fn test_is_idle_state() {
assert!(is_idle_state("IDLE"));
assert!(is_idle_state("OFF"));
assert!(is_idle_state("DOWN"));
assert!(!is_idle_state("V0P14"));
assert!(!is_idle_state("P1"));
}
}