#![allow(non_upper_case_globals)]
#![allow(dead_code)]
use std::{
collections::HashMap,
ffi::CString,
marker::{PhantomData, PhantomPinned},
mem::{MaybeUninit, size_of},
os::raw::c_void,
ptr::{null, null_mut},
sync::OnceLock,
time::Duration,
};
use core_foundation::{
array::{
CFArrayAppendValue, CFArrayCreateMutable, CFArrayGetCount, CFArrayGetValueAtIndex, CFArrayRef,
CFMutableArrayRef, kCFTypeArrayCallBacks,
},
base::{CFAllocatorRef, CFRange, CFRelease, CFTypeRef, kCFAllocatorDefault, kCFAllocatorNull},
data::{CFDataGetBytes, CFDataGetLength, CFDataRef},
dictionary::{
CFDictionaryCreate, CFDictionaryCreateMutableCopy, CFDictionaryGetCount,
CFDictionaryGetKeysAndValues, CFDictionaryGetValue, CFDictionaryRef, CFDictionarySetValue,
CFMutableDictionaryRef, kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks,
},
number::{
CFNumberCreate, CFNumberGetValue, CFNumberRef, kCFNumberSInt32Type, kCFNumberSInt64Type,
},
string::{CFStringCreateWithBytesNoCopy, CFStringGetCString, CFStringRef, kCFStringEncodingUTF8},
};
use serde::Serialize;
pub type WithError<T> = Result<T, Box<dyn std::error::Error>>;
pub type CVoidRef = *const std::ffi::c_void;
static SOC_INFO_CACHE: OnceLock<SocInfo> = OnceLock::new();
pub fn cfnum(val: i32) -> CFNumberRef {
unsafe { CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &val as *const i32 as _) }
}
pub fn cfstr(val: &str) -> CFStringRef {
unsafe {
CFStringCreateWithBytesNoCopy(
kCFAllocatorDefault,
val.as_ptr(),
val.len() as isize,
kCFStringEncodingUTF8,
0,
kCFAllocatorNull,
)
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn from_cfstr(val: CFStringRef) -> String {
unsafe {
let mut buf = Vec::with_capacity(128);
if CFStringGetCString(val, buf.as_mut_ptr(), 128, kCFStringEncodingUTF8) == 0 {
panic!("Failed to convert CFString to CString");
}
std::ffi::CStr::from_ptr(buf.as_ptr()).to_string_lossy().to_string()
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn cfdict_keys(dict: CFDictionaryRef) -> Vec<String> {
unsafe {
let count = CFDictionaryGetCount(dict) as usize;
let mut keys: Vec<CFStringRef> = Vec::with_capacity(count);
let mut vals: Vec<CFTypeRef> = Vec::with_capacity(count);
CFDictionaryGetKeysAndValues(dict, keys.as_mut_ptr() as _, vals.as_mut_ptr());
keys.set_len(count);
vals.set_len(count);
keys.iter().map(|k| from_cfstr(*k as _)).collect()
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn cfdict_get_val(dict: CFDictionaryRef, key: &str) -> Option<CFTypeRef> {
unsafe {
let key = cfstr(key);
let val = CFDictionaryGetValue(dict, key as _);
CFRelease(key as _);
match val {
_ if val.is_null() => None,
_ => Some(val),
}
}
}
#[link(name = "IOKit", kind = "framework")]
#[rustfmt::skip]
unsafe extern "C" {
fn IOServiceMatching(name: *const i8) -> CFMutableDictionaryRef;
fn IOServiceGetMatchingServices(mainPort: u32, matching: CFDictionaryRef, 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: CFAllocatorRef, options: u32) -> i32;
fn IOObjectRelease(obj: u32) -> u32;
}
#[repr(C)]
struct IOReportSubscription {
_data: [u8; 0],
_phantom: PhantomData<(*mut u8, PhantomPinned)>,
}
type IOReportSubscriptionRef = *const IOReportSubscription;
type ChannelFilter = fn(&str, &str, &str, &str) -> bool;
type ChannelFilterRef<'a> = &'a dyn Fn(&str, &str, &str, &str) -> bool;
#[link(name = "IOReport", kind = "dylib")]
#[rustfmt::skip]
unsafe extern "C" {
fn IOReportCopyAllChannels(a: u64, b: u64) -> CFDictionaryRef;
fn IOReportCreateSubscription(a: CVoidRef, b: CFMutableDictionaryRef, c: *mut CFMutableDictionaryRef, d: u64, b: CFTypeRef) -> IOReportSubscriptionRef;
fn IOReportCreateSamples(a: IOReportSubscriptionRef, b: CFMutableDictionaryRef, c: CFTypeRef) -> CFDictionaryRef;
fn IOReportCreateSamplesDelta(a: CFDictionaryRef, b: CFDictionaryRef, c: CFTypeRef) -> CFDictionaryRef;
fn IOReportChannelGetGroup(a: CFDictionaryRef) -> CFStringRef;
fn IOReportChannelGetSubGroup(a: CFDictionaryRef) -> CFStringRef;
fn IOReportChannelGetChannelName(a: CFDictionaryRef) -> CFStringRef;
fn IOReportSimpleGetIntegerValue(a: CFDictionaryRef, b: i32) -> i64;
fn IOReportChannelGetUnitLabel(a: CFDictionaryRef) -> CFStringRef;
fn IOReportStateGetCount(a: CFDictionaryRef) -> i32;
fn IOReportStateGetNameForIndex(a: CFDictionaryRef, b: i32) -> CFStringRef;
fn IOReportStateGetResidency(a: CFDictionaryRef, b: i32) -> i64;
}
fn cfio_get_group(item: CFDictionaryRef) -> String {
match unsafe { IOReportChannelGetGroup(item) } {
x if x.is_null() => String::new(),
x => from_cfstr(x),
}
}
fn cfio_get_subgroup(item: CFDictionaryRef) -> String {
match unsafe { IOReportChannelGetSubGroup(item) } {
x if x.is_null() => String::new(),
x => from_cfstr(x),
}
}
fn cfio_get_channel(item: CFDictionaryRef) -> String {
match unsafe { IOReportChannelGetChannelName(item) } {
x if x.is_null() => String::new(),
x => from_cfstr(x),
}
}
fn cfio_channel_matches(items: &[(&str, Option<&str>)], group: &str, subgroup: &str) -> bool {
items.is_empty()
|| items.iter().any(|(item_group, item_subgroup)| {
*item_group == group && item_subgroup.is_none_or(|value| value == subgroup)
})
}
pub fn cfio_get_props(entry: u32, name: String) -> WithError<CFDictionaryRef> {
unsafe {
let mut props: MaybeUninit<CFMutableDictionaryRef> = MaybeUninit::uninit();
if IORegistryEntryCreateCFProperties(entry, props.as_mut_ptr(), kCFAllocatorDefault, 0) != 0 {
return Err(format!("Failed to get properties for {}", name).into());
}
Ok(props.assume_init())
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn cfio_get_residencies(item: CFDictionaryRef) -> Vec<(String, i64)> {
let count = unsafe { IOReportStateGetCount(item) };
let mut res = vec![];
for i in 0..count {
let name = unsafe { IOReportStateGetNameForIndex(item, i) };
let val = unsafe { IOReportStateGetResidency(item, i) };
let name = match name {
x if x.is_null() => format!("S{i}"),
x => from_cfstr(x),
};
res.push((name, val));
}
res
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn cfio_watts(item: CFDictionaryRef, unit: &str, duration: Duration) -> WithError<f32> {
let val = unsafe { IOReportSimpleGetIntegerValue(item, 0) } as f64;
watts_from_energy(val, unit, duration)
}
fn watts_from_energy(val: f64, unit: &str, duration: Duration) -> WithError<f32> {
let val = val / duration.as_secs_f64();
match unit {
"mJ" => Ok((val / 1e3) as f32),
"uJ" => Ok((val / 1e6) as f32),
"nJ" => Ok((val / 1e9) as f32),
_ => Err(format!("Invalid energy unit: {}", unit).into()),
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn cfio_integer_value(item: CFDictionaryRef) -> i64 {
unsafe { IOReportSimpleGetIntegerValue(item, 0) }
}
pub struct IOServiceIterator {
existing: u32,
}
impl IOServiceIterator {
pub fn new(service_name: &str) -> WithError<Self> {
let service_name = std::ffi::CString::new(service_name).unwrap();
let existing = unsafe {
let service = IOServiceMatching(service_name.as_ptr() as _);
let mut existing = 0;
if IOServiceGetMatchingServices(0, service, &mut existing) != 0 {
return Err(format!("{} not found", service_name.to_string_lossy()).into());
}
existing
};
Ok(Self { existing })
}
}
impl Drop for IOServiceIterator {
fn drop(&mut self) {
unsafe {
IOObjectRelease(self.existing);
}
}
}
impl Iterator for IOServiceIterator {
type Item = (u32, String);
fn next(&mut self) -> Option<Self::Item> {
let next = unsafe { IOIteratorNext(self.existing) };
if next == 0 {
return None;
}
let mut name = [0; 128]; if unsafe { IORegistryEntryGetName(next, name.as_mut_ptr()) } != 0 {
return None;
}
let name = unsafe { std::ffi::CStr::from_ptr(name.as_ptr()) };
let name = name.to_string_lossy().to_string();
Some((next, name))
}
}
pub struct IOReportIterator {
sample: CFDictionaryRef,
index: isize,
items: CFArrayRef,
items_size: isize,
metadata: Vec<(String, String, String, String)>,
}
impl IOReportIterator {
pub fn new(data: CFDictionaryRef, metadata: Vec<(String, String, String, String)>) -> Self {
let items = cfdict_get_val(data, "IOReportChannels").unwrap() as CFArrayRef;
let items_size = unsafe { CFArrayGetCount(items) } as isize;
debug_assert_eq!(metadata.len(), items_size as usize);
Self { sample: data, items, items_size, index: 0, metadata }
}
}
impl Drop for IOReportIterator {
fn drop(&mut self) {
unsafe { CFRelease(self.sample as _) };
}
}
#[derive(Debug)]
pub struct IOReportIteratorItem {
pub group: String,
pub subgroup: String,
pub channel: String,
pub unit: String,
pub item: CFDictionaryRef,
}
impl Iterator for IOReportIterator {
type Item = IOReportIteratorItem;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.items_size {
return None;
}
let item = unsafe { CFArrayGetValueAtIndex(self.items, self.index) } as CFDictionaryRef;
let (group, subgroup, channel, unit) =
self.metadata.get(self.index as usize).cloned().unwrap_or_default();
self.index += 1;
Some(IOReportIteratorItem { group, subgroup, channel, unit, item })
}
}
pub fn libc_ram() -> WithError<(u64, u64)> {
let (mut usage, mut total) = (0u64, 0u64);
unsafe {
let mut name = [libc::CTL_HW, libc::HW_MEMSIZE];
let mut size = std::mem::size_of::<u64>();
let ret_code = libc::sysctl(
name.as_mut_ptr(),
name.len() as _,
&mut total as *mut _ as *mut _,
&mut size,
std::ptr::null_mut(),
0,
);
if ret_code != 0 {
return Err("Failed to get total memory".into());
}
}
unsafe {
let mut count: u32 = libc::HOST_VM_INFO64_COUNT as _;
let mut stats = std::mem::zeroed::<libc::vm_statistics64>();
#[allow(deprecated)]
let ret_code = libc::host_statistics64(
libc::mach_host_self(),
libc::HOST_VM_INFO64,
&mut stats as *mut _ as *mut _,
&mut count,
);
if ret_code != 0 {
return Err("Failed to get memory stats".into());
}
let page_size_kb = libc::sysconf(libc::_SC_PAGESIZE) as u64;
usage = (stats.active_count as u64
+ stats.inactive_count as u64
+ stats.wire_count as u64
+ stats.speculative_count as u64
+ stats.compressor_page_count as u64
- stats.purgeable_count as u64
- stats.external_page_count as u64)
* page_size_kb;
}
Ok((usage, total))
}
pub fn libc_swap() -> WithError<(u64, u64)> {
let (mut usage, mut total) = (0u64, 0u64);
unsafe {
let mut name = [libc::CTL_VM, libc::VM_SWAPUSAGE];
let mut size = std::mem::size_of::<libc::xsw_usage>();
let mut xsw: libc::xsw_usage = std::mem::zeroed::<libc::xsw_usage>();
let ret_code = libc::sysctl(
name.as_mut_ptr(),
name.len() as _,
&mut xsw as *mut _ as *mut _,
&mut size,
std::ptr::null_mut(),
0,
);
if ret_code != 0 {
return Err("Failed to get swap usage".into());
}
usage = xsw.xsu_used;
total = xsw.xsu_total;
}
Ok((usage, total))
}
#[derive(Debug, Default, Clone, Serialize)]
pub struct SocInfo {
pub mac_model: String,
pub chip_name: String,
pub memory_gb: u16,
pub ecpu_cores: u8,
pub pcpu_cores: u8,
pub ecpu_label: String,
pub pcpu_label: String,
pub ecpu_freqs: Vec<u32>,
pub pcpu_freqs: Vec<u32>,
pub gpu_cores: u8,
pub gpu_freqs: Vec<u32>,
}
impl SocInfo {
pub fn new() -> WithError<Self> {
get_soc_info()
}
}
pub fn get_dvfs_mhz(dict: CFDictionaryRef, key: &str) -> Option<(Vec<u32>, Vec<u32>)> {
unsafe {
let obj = cfdict_get_val(dict, key)? as CFDataRef;
let obj_len = CFDataGetLength(obj);
let obj_val = vec![0u8; obj_len as usize];
CFDataGetBytes(obj, CFRange::init(0, obj_len), obj_val.as_ptr() as *mut u8);
let items_count = (obj_len / 8) as usize;
let [mut freqs, mut volts] = [vec![0u32; items_count], vec![0u32; items_count]];
for (i, x) in obj_val.chunks_exact(8).enumerate() {
volts[i] = u32::from_le_bytes([x[4], x[5], x[6], x[7]]);
freqs[i] = u32::from_le_bytes([x[0], x[1], x[2], x[3]]);
}
Some((volts, freqs))
}
}
fn parse_acc_clusters(data: &[u8]) -> Option<(String, String)> {
let mut clusters: Vec<(u8, String)> = Vec::new();
for chunk in data.chunks_exact(8) {
clusters.push((chunk[1], format!("voltage-states{}-sram", chunk[0])));
}
clusters.sort_by_key(|c| c.0);
if clusters.len() < 2 {
return None;
}
let ecpu_key = clusters[clusters.len() - 2].1.clone();
let pcpu_key = clusters.last()?.1.clone();
Some((ecpu_key, pcpu_key))
}
fn parse_acc_clusters_from(dict: CFDictionaryRef) -> Option<(String, String)> {
let obj = cfdict_get_val(dict, "acc-clusters")? as CFDataRef;
let len = unsafe { CFDataGetLength(obj) } as usize;
if len < 8 {
return None;
}
let mut data = vec![0u8; len];
unsafe { CFDataGetBytes(obj, CFRange::init(0, len as _), data.as_mut_ptr()) };
parse_acc_clusters(&data)
}
fn to_mhz(vals: Vec<u32>, scale: u32) -> Vec<u32> {
vals.iter().map(|x| *x / scale).collect()
}
fn cpu_freq_scale(chip_name: &str) -> u32 {
let hz_freqs = chip_name.contains("M1")
|| chip_name.contains("M2")
|| chip_name.contains("M3")
|| chip_name.contains("A1"); if hz_freqs { 1_000_000 } else { 1_000 }
}
fn cpu_freqs(item: CFDictionaryRef, key: &str, is_ecpu: bool, scale: u32) -> Option<Vec<u32>> {
if let Some((_, freqs)) = get_dvfs_mhz(item, key) {
return Some(to_mhz(freqs, scale));
}
let (ecpu_key, pcpu_key) = parse_acc_clusters_from(item)?;
let key = if is_ecpu { ecpu_key } else { pcpu_key };
let (_, freqs) = get_dvfs_mhz(item, &key)?;
Some(to_mhz(freqs, scale))
}
fn parse_cpu_cores(s: &str) -> (u64, u64, bool) {
let procs = s.strip_prefix("proc ").unwrap_or("");
let parts: Vec<u64> = procs.split(':').map(|x| x.parse().unwrap_or(0)).collect();
match parts.len() {
4 => {
let (e, m) = (parts[2], parts[3]);
if m > 0 { (m, parts[1], true) } else { (e, parts[1], false) }
}
3 => (parts[2], parts[1], false), _ => (0, 0, false),
}
}
#[derive(Debug)]
pub(crate) struct HwInfo {
pub(crate) chip_name: String,
pub(crate) mac_model: String,
pub(crate) memory_gb: u16,
pub(crate) ecpu_cores: u8,
pub(crate) pcpu_cores: u8,
pub(crate) ecpu_label: String,
pub(crate) pcpu_label: String,
pub(crate) gpu_cores: u8,
}
pub(crate) fn sysctl_str(name: &str) -> Option<String> {
let cname = CString::new(name).ok()?;
unsafe {
let mut size: usize = 0;
let ret = libc::sysctlbyname(cname.as_ptr(), null_mut(), &mut size, null_mut(), 0);
if ret != 0 || size == 0 {
return None;
}
let mut buf = vec![0u8; size];
let ret =
libc::sysctlbyname(cname.as_ptr(), buf.as_mut_ptr() as *mut c_void, &mut size, null_mut(), 0);
if ret != 0 {
return None;
}
buf.truncate(size.saturating_sub(1)); String::from_utf8(buf).ok()
}
}
fn sysctl_buf<const N: usize>(name: &str) -> Option<[u8; N]> {
let cname = CString::new(name).ok()?;
let mut buf = [0; N];
let mut size = N;
let ret = unsafe {
libc::sysctlbyname(cname.as_ptr(), buf.as_mut_ptr().cast(), &mut size, null_mut(), 0)
};
(ret == 0).then_some(buf)
}
fn sysctl_u32(name: &str) -> Option<u32> {
sysctl_buf(name).map(u32::from_ne_bytes)
}
fn sysctl_u64(name: &str) -> Option<u64> {
sysctl_buf(name).map(u64::from_ne_bytes)
}
fn cfnum_get_i64(dict: CFDictionaryRef, key: &str) -> Option<i64> {
let obj = cfdict_get_val(dict, key)? as CFNumberRef;
let mut val: i64 = 0;
let ok = unsafe { CFNumberGetValue(obj, kCFNumberSInt64Type, &mut val as *mut _ as *mut c_void) };
ok.then_some(val)
}
fn cpu_tier_counts(chip_name: &str) -> Option<(u8, u8, &'static str, &'static str)> {
let nperflevels = sysctl_u32("hw.nperflevels")?;
if nperflevels < 2 {
return None;
}
let hi = sysctl_u32("hw.perflevel0.physicalcpu")?;
let lo = sysctl_u32(&format!("hw.perflevel{}.physicalcpu", nperflevels - 1))?;
let is_legacy = ["M1", "M2", "M3", "M4", "A1"].iter().any(|x| chip_name.contains(x));
let (ecpu_label, pcpu_label) = if is_legacy { ("E", "P") } else { ("P", "S") };
Some((lo as u8, hi as u8, ecpu_label, pcpu_label))
}
pub(crate) fn hw_native() -> WithError<HwInfo> {
let chip_name = sysctl_str("machdep.cpu.brand_string").ok_or("Failed to read chip name")?;
let mac_model = sysctl_str("hw.model").ok_or("Failed to read mac model")?;
let memory_gb =
sysctl_u64("hw.memsize").ok_or("Failed to read memory size")? / (1024 * 1024 * 1024);
let (ecpu_cores, pcpu_cores, ecpu_label, pcpu_label) =
cpu_tier_counts(&chip_name).ok_or("Failed to read CPU core topology")?;
let mut gpu_cores = 0u8;
for (entry, name) in IOServiceIterator::new("AGXAccelerator")? {
if let Ok(item) = cfio_get_props(entry, name) {
if let Some(cores) = cfnum_get_i64(item, "gpu-core-count") {
gpu_cores = cores as u8;
}
unsafe { CFRelease(item as _) }
}
}
Ok(HwInfo {
chip_name,
mac_model,
memory_gb: memory_gb as u16,
ecpu_cores,
pcpu_cores,
ecpu_label: ecpu_label.into(),
pcpu_label: pcpu_label.into(),
gpu_cores,
})
}
pub(crate) fn hw_from_profiler() -> WithError<HwInfo> {
let out = std::process::Command::new("system_profiler")
.args(["SPHardwareDataType", "SPDisplaysDataType", "-json"])
.output()?;
let out = std::str::from_utf8(&out.stdout)?;
let out = serde_json::from_str::<serde_json::Value>(out)?;
let chip_name = out["SPHardwareDataType"][0]["chip_type"].as_str();
let chip_name = chip_name.unwrap_or("Unknown chip").to_string();
let mac_model = out["SPHardwareDataType"][0]["machine_model"].as_str();
let mac_model = mac_model.unwrap_or("Unknown model").to_string();
let mem_gb = out["SPHardwareDataType"][0]["physical_memory"].as_str();
let mem_gb = mem_gb.and_then(|x| x.strip_suffix(" GB")).and_then(|x| x.parse::<u64>().ok());
let mem_gb = mem_gb.unwrap_or(0);
let number_processors = out["SPHardwareDataType"][0]["number_processors"].as_str().unwrap_or("");
let (ecpu_cores, pcpu_cores, has_mcpu) = parse_cpu_cores(number_processors);
let gpu_cores = out["SPDisplaysDataType"][0]["sppci_cores"].as_str();
let gpu_cores = gpu_cores.unwrap_or("0").parse::<u64>().unwrap_or(0);
Ok(HwInfo {
chip_name,
mac_model,
memory_gb: mem_gb as u16,
ecpu_cores: ecpu_cores as u8,
pcpu_cores: pcpu_cores as u8,
ecpu_label: if has_mcpu { "P".into() } else { "E".into() },
pcpu_label: if has_mcpu { "S".into() } else { "P".into() },
gpu_cores: gpu_cores as u8,
})
}
fn load_soc_info() -> WithError<SocInfo> {
let hw = match hw_native() {
Ok(hw) => hw,
Err(_) => hw_from_profiler()?,
};
let mut info = SocInfo {
chip_name: hw.chip_name,
mac_model: hw.mac_model,
memory_gb: hw.memory_gb,
ecpu_cores: hw.ecpu_cores,
pcpu_cores: hw.pcpu_cores,
ecpu_label: hw.ecpu_label,
pcpu_label: hw.pcpu_label,
gpu_cores: hw.gpu_cores,
..Default::default()
};
let cpu_scale = cpu_freq_scale(&info.chip_name);
let gpu_scale: u32 = 1000 * 1000;
for (entry, name) in IOServiceIterator::new("AppleARMIODevice")? {
if name == "pmgr" {
let item = cfio_get_props(entry, name)?;
if let Some(f) = cpu_freqs(item, "voltage-states1-sram", true, cpu_scale) {
info.ecpu_freqs = f;
}
if let Some(f) = cpu_freqs(item, "voltage-states5-sram", false, cpu_scale) {
info.pcpu_freqs = f;
}
if let Some((_, freqs)) = get_dvfs_mhz(item, "voltage-states9") {
info.gpu_freqs = to_mhz(freqs, gpu_scale);
}
unsafe { CFRelease(item as _) }
}
}
if info.ecpu_freqs.is_empty() || info.pcpu_freqs.is_empty() {
return Err("No CPU frequencies found".into());
}
Ok(info)
}
pub fn get_soc_info() -> WithError<SocInfo> {
if let Some(info) = SOC_INFO_CACHE.get() {
return Ok(info.clone());
}
let info = load_soc_info()?;
let _ = SOC_INFO_CACHE.set(info.clone());
Ok(info)
}
struct IOReportChannels {
chan: CFMutableDictionaryRef,
source: Option<CFDictionaryRef>,
selected: Option<CFMutableArrayRef>,
}
fn cfio_get_chan(filter: Option<ChannelFilterRef<'_>>) -> WithError<IOReportChannels> {
let all_channels = unsafe { IOReportCopyAllChannels(0, 0) };
let Some(channel_array) = cfdict_get_val(all_channels, "IOReportChannels") else {
unsafe { CFRelease(all_channels as _) };
return Err("Failed to get channels".into());
};
let channel_array = channel_array as CFArrayRef;
let size = unsafe { CFDictionaryGetCount(all_channels) };
let chan = unsafe { CFDictionaryCreateMutableCopy(kCFAllocatorDefault, size, all_channels) };
let mut selected_channels = None;
if let Some(filter) = filter {
let count = unsafe { CFArrayGetCount(channel_array) };
let selected =
unsafe { CFArrayCreateMutable(kCFAllocatorDefault, count, &kCFTypeArrayCallBacks) };
for i in 0..count {
let item = unsafe { CFArrayGetValueAtIndex(channel_array, i) } as CFDictionaryRef;
let group = cfio_get_group(item);
let subgroup = cfio_get_subgroup(item);
let channel = cfio_get_channel(item);
let unit = from_cfstr(unsafe { IOReportChannelGetUnitLabel(item) }).trim().to_string();
if filter(&group, &subgroup, &channel, &unit) {
unsafe { CFArrayAppendValue(selected, item as _) };
}
}
let key = cfstr("IOReportChannels");
unsafe {
CFDictionarySetValue(chan, key as _, selected as _);
CFRelease(key as _);
}
selected_channels = Some(selected);
}
Ok(IOReportChannels { chan, source: Some(all_channels), selected: selected_channels })
}
fn cfio_channel_metadata(channels: CFDictionaryRef) -> Vec<(String, String, String, String)> {
let Some(channel_array) = cfdict_get_val(channels, "IOReportChannels") else {
return Vec::new();
};
let channel_array = channel_array as CFArrayRef;
let count = unsafe { CFArrayGetCount(channel_array) };
let mut metadata = Vec::with_capacity(count as usize);
for i in 0..count {
let item = unsafe { CFArrayGetValueAtIndex(channel_array, i) } as CFDictionaryRef;
metadata.push((
cfio_get_group(item),
cfio_get_subgroup(item),
cfio_get_channel(item),
from_cfstr(unsafe { IOReportChannelGetUnitLabel(item) }).trim().to_string(),
));
}
metadata
}
fn cfio_get_subs(chan: CFMutableDictionaryRef) -> WithError<IOReportSubscriptionRef> {
let mut s: MaybeUninit<CFMutableDictionaryRef> = MaybeUninit::uninit();
let rs = unsafe { IOReportCreateSubscription(null(), chan, s.as_mut_ptr(), 0, null()) };
if rs.is_null() {
return Err("Failed to create subscription".into());
}
unsafe { s.assume_init() };
Ok(rs)
}
pub struct IOReport {
subs: IOReportSubscriptionRef,
chan: CFMutableDictionaryRef,
source: Option<CFDictionaryRef>,
selected: Option<CFMutableArrayRef>,
metadata: Vec<(String, String, String, String)>,
prev: Option<(CFDictionaryRef, std::time::Instant)>,
}
impl IOReport {
fn from_filter(filter: Option<ChannelFilterRef<'_>>) -> WithError<Self> {
let channels = cfio_get_chan(filter)?;
let metadata = cfio_channel_metadata(channels.chan);
let subs = cfio_get_subs(channels.chan)?;
Ok(Self {
subs,
chan: channels.chan,
source: channels.source,
selected: channels.selected,
metadata,
prev: None,
})
}
pub fn new(channels: Vec<(&str, Option<&str>)>) -> WithError<Self> {
let filter = |group: &str, subgroup: &str, _channel: &str, _unit: &str| {
cfio_channel_matches(&channels, group, subgroup)
};
Self::from_filter(Some(&filter))
}
pub(crate) fn with_filter(filter: Option<ChannelFilter>) -> WithError<Self> {
match filter {
Some(filter) => Self::from_filter(Some(&filter)),
None => Self::from_filter(None),
}
}
pub fn get_sample(&self, duration: u64) -> IOReportIterator {
unsafe {
let sample1 = IOReportCreateSamples(self.subs, self.chan, null());
std::thread::sleep(std::time::Duration::from_millis(duration));
let sample2 = IOReportCreateSamples(self.subs, self.chan, null());
let sample3 = IOReportCreateSamplesDelta(sample1, sample2, null());
CFRelease(sample1 as _);
CFRelease(sample2 as _);
IOReportIterator::new(sample3, self.metadata.clone())
}
}
fn raw_sample(&self) -> (CFDictionaryRef, std::time::Instant) {
(unsafe { IOReportCreateSamples(self.subs, self.chan, null()) }, std::time::Instant::now())
}
pub(crate) fn get_sample_interval(&mut self, duration: Duration) -> (IOReportIterator, Duration) {
let prev = match self.prev {
Some(x) => x,
None => self.raw_sample(),
};
let target_at = prev.1 + duration;
let now = std::time::Instant::now();
if target_at > now {
std::thread::sleep(target_at.duration_since(now));
}
let next = self.raw_sample();
let diff = unsafe { IOReportCreateSamplesDelta(prev.0, next.0, null()) };
unsafe { CFRelease(prev.0 as _) };
let elapsed = next.1.duration_since(prev.1).max(Duration::from_nanos(1));
self.prev = Some(next);
(IOReportIterator::new(diff, self.metadata.clone()), elapsed)
}
pub fn get_samples(&mut self, duration: u64, count: usize) -> Vec<(IOReportIterator, u64)> {
let count = count.clamp(1, 32);
let mut samples: Vec<(IOReportIterator, u64)> = Vec::with_capacity(count);
let mut prev = match self.prev {
Some(x) => x,
None => self.raw_sample(),
};
let started_at = prev.1;
for i in 1..=count {
let target_msec = duration.saturating_mul(i as u64) / count as u64;
let target_at = started_at + std::time::Duration::from_millis(target_msec);
let now = std::time::Instant::now();
if target_at > now {
std::thread::sleep(target_at.duration_since(now));
}
let next = self.raw_sample();
let diff = unsafe { IOReportCreateSamplesDelta(prev.0, next.0, null()) };
unsafe { CFRelease(prev.0 as _) };
let elapsed = next.1.duration_since(prev.1).as_millis() as u64;
prev = next;
samples.push((IOReportIterator::new(diff, self.metadata.clone()), elapsed.max(1)));
}
self.prev = Some(prev);
samples
}
}
impl Drop for IOReport {
fn drop(&mut self) {
unsafe {
CFRelease(self.chan as _);
CFRelease(self.subs as _);
if let Some(selected) = self.selected {
CFRelease(selected as _);
}
if let Some(source) = self.source {
CFRelease(source as _);
}
if let Some(prev) = self.prev {
CFRelease(prev.0 as _);
}
}
}
}
#[repr(C)]
struct IOHIDServiceClient(libc::c_void);
#[repr(C)]
struct IOHIDEventSystemClient(libc::c_void);
#[repr(C)]
struct IOHIDEvent(libc::c_void);
type IOHIDServiceClientRef = *const IOHIDServiceClient;
type IOHIDEventSystemClientRef = *const IOHIDEventSystemClient;
type IOHIDEventRef = *const IOHIDEvent;
const kHIDPage_AppleVendor: i32 = 0xff00;
const kHIDUsage_AppleVendor_TemperatureSensor: i32 = 0x0005;
const kIOHIDEventTypeTemperature: i64 = 15;
const kIOHIDEventTypePower: i64 = 25;
#[link(name = "IOKit", kind = "framework")]
#[rustfmt::skip]
unsafe extern "C" {
fn IOHIDEventSystemClientCreate(allocator: CFAllocatorRef) -> IOHIDEventSystemClientRef;
fn IOHIDEventSystemClientSetMatching(a: IOHIDEventSystemClientRef, b: CFDictionaryRef) -> i32;
fn IOHIDEventSystemClientCopyServices(a: IOHIDEventSystemClientRef) -> CFArrayRef;
fn IOHIDServiceClientCopyProperty(a: IOHIDServiceClientRef, b: CFStringRef) -> CFStringRef;
fn IOHIDServiceClientCopyEvent(a: IOHIDServiceClientRef, v0: i64, v1: i32, v2: i64) -> IOHIDEventRef;
fn IOHIDEventGetFloatValue(event: IOHIDEventRef, field: i64) -> f64;
}
pub struct IOHIDSensors {
sensors: CFDictionaryRef,
}
impl IOHIDSensors {
pub fn new() -> WithError<Self> {
let keys = [cfstr("PrimaryUsagePage"), cfstr("PrimaryUsage")];
let nums = [cfnum(kHIDPage_AppleVendor), cfnum(kHIDUsage_AppleVendor_TemperatureSensor)];
let sensors = unsafe {
CFDictionaryCreate(
kCFAllocatorDefault,
keys.as_ptr() as _,
nums.as_ptr() as _,
2,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks,
)
};
Ok(Self { sensors })
}
pub fn get_metrics(&self) -> Vec<(String, f32)> {
unsafe {
let system = match IOHIDEventSystemClientCreate(kCFAllocatorDefault) {
x if x.is_null() => return vec![],
x => x,
};
IOHIDEventSystemClientSetMatching(system, self.sensors);
let services = match IOHIDEventSystemClientCopyServices(system) {
x if x.is_null() => return vec![],
x => x,
};
let mut items = vec![] as Vec<(String, f32)>;
for i in 0..CFArrayGetCount(services) {
let sc = match CFArrayGetValueAtIndex(services, i) as IOHIDServiceClientRef {
x if x.is_null() => continue,
x => x,
};
let name = match IOHIDServiceClientCopyProperty(sc, cfstr("Product")) {
x if x.is_null() => continue,
x => from_cfstr(x),
};
let event = match IOHIDServiceClientCopyEvent(sc, kIOHIDEventTypeTemperature, 0, 0) {
x if x.is_null() => continue,
x => x,
};
let temp = IOHIDEventGetFloatValue(event, kIOHIDEventTypeTemperature << 16);
CFRelease(event as _);
if temp <= 0.0 || temp > 150.0 {
continue;
}
items.push((name, temp as f32));
}
CFRelease(services as _);
CFRelease(system as _);
items.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
items
}
}
}
impl Drop for IOHIDSensors {
fn drop(&mut self) {
unsafe { CFRelease(self.sensors as _) };
}
}
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn mach_task_self() -> u32;
fn IOServiceOpen(device: u32, a: u32, b: u32, c: *mut u32) -> i32;
fn IOServiceClose(conn: u32) -> i32;
fn IOConnectCallStructMethod(
conn: u32,
selector: u32,
ival: *const c_void,
isize: usize,
oval: *mut c_void,
osize: *mut usize,
) -> i32;
}
#[repr(C)]
#[derive(Debug, Default)]
pub struct KeyDataVer {
pub major: u8,
pub minor: u8,
pub build: u8,
pub reserved: u8,
pub release: u16,
}
#[repr(C)]
#[derive(Debug, Default)]
pub struct PLimitData {
pub version: u16,
pub length: u16,
pub cpu_p_limit: u32,
pub gpu_p_limit: u32,
pub mem_p_limit: u32,
}
#[repr(C)]
#[derive(Debug, Default, Clone, Copy)]
pub struct KeyInfo {
pub data_size: u32,
pub data_type: u32,
pub data_attributes: u8,
}
#[repr(C)]
#[derive(Debug, Default)]
pub struct KeyData {
pub key: u32,
pub vers: KeyDataVer,
pub p_limit_data: PLimitData,
pub key_info: KeyInfo,
pub result: u8,
pub status: u8,
pub data8: u8,
pub data32: u32,
pub bytes: [u8; 32],
}
#[derive(Debug, Clone)]
pub struct SensorVal {
pub name: String,
pub unit: String,
pub data: Vec<u8>,
}
#[allow(clippy::upper_case_acronyms)]
pub struct SMC {
conn: u32,
keys: HashMap<u32, KeyInfo>,
}
impl SMC {
pub fn new() -> WithError<Self> {
let mut conn = 0;
for (device, name) in IOServiceIterator::new("AppleSMC")? {
if name == "AppleSMCKeysEndpoint" {
let rs = unsafe { IOServiceOpen(device, mach_task_self(), 0, &mut conn) };
if rs != 0 {
return Err(format!("IOServiceOpen: {}", rs).into());
}
}
}
Ok(Self { conn, keys: HashMap::new() })
}
fn read(&self, input: &KeyData) -> WithError<KeyData> {
let ival = input as *const _ as _;
let ilen = size_of::<KeyData>();
let mut oval = KeyData::default();
let mut olen = size_of::<KeyData>();
let rs = unsafe {
IOConnectCallStructMethod(self.conn, 2, ival, ilen, &mut oval as *mut _ as _, &mut olen)
};
if rs != 0 {
return Err(format!("IOConnectCallStructMethod: {}", rs).into());
}
if oval.result == 132 {
return Err("SMC key not found".into());
}
if oval.result != 0 {
return Err(format!("SMC error: {}", oval.result).into());
}
Ok(oval)
}
fn parse_key(key: &str) -> WithError<u32> {
if key.len() != 4 {
return Err("SMC key must be 4 bytes long".into());
}
Ok(key.bytes().fold(0, |acc, x| (acc << 8) + x as u32))
}
fn read_key_info_by_id(&mut self, key: u32) -> WithError<KeyInfo> {
if let Some(key_info) = self.keys.get(&key) {
return Ok(*key_info);
}
let ival = KeyData { data8: 9, key, ..Default::default() };
let oval = self.read(&ival)?;
self.keys.insert(key, oval.key_info);
Ok(oval.key_info)
}
pub fn key_by_index(&self, index: u32) -> WithError<String> {
let ival = KeyData { data8: 8, data32: index, ..Default::default() };
let oval = self.read(&ival)?;
Ok(std::str::from_utf8(&oval.key.to_be_bytes()).unwrap().to_string())
}
pub fn read_key_info(&mut self, key: &str) -> WithError<KeyInfo> {
let key = Self::parse_key(key)?;
self.read_key_info_by_id(key)
}
pub fn read_val(&mut self, key: &str) -> WithError<SensorVal> {
let name = key.to_string();
let key = Self::parse_key(key)?;
let key_info = self.read_key_info_by_id(key)?;
let ival = KeyData { data8: 5, key, key_info, ..Default::default() };
let oval = self.read(&ival)?;
Ok(SensorVal {
name,
unit: std::str::from_utf8(&key_info.data_type.to_be_bytes()).unwrap().to_string(),
data: oval.bytes[0..key_info.data_size as usize].to_vec(),
})
}
pub fn read_float_val(&mut self, key: &str) -> WithError<f32> {
const FLOAT_TYPE: u32 = 1718383648;
let key_id = Self::parse_key(key)?;
let key_info = self.read_key_info_by_id(key_id)?;
if key_info.data_size != 4 || key_info.data_type != FLOAT_TYPE {
return Err(
format!(
"SMC key '{}' is not a 4-byte float (size={}, type={})",
key, key_info.data_size, key_info.data_type
)
.into(),
);
}
let ival = KeyData { data8: 5, key: key_id, key_info, ..Default::default() };
let oval = self.read(&ival)?;
Ok(f32::from_le_bytes(oval.bytes[0..4].try_into().unwrap()))
}
pub fn key_count(&mut self) -> WithError<u32> {
let key = Self::parse_key("#KEY")?;
let key_info = self.read_key_info_by_id(key)?;
let ival = KeyData { data8: 5, key, key_info, ..Default::default() };
let oval = self.read(&ival)?;
Ok(u32::from_be_bytes(oval.bytes[0..4].try_into().unwrap()))
}
pub fn read_all_keys(&mut self) -> WithError<Vec<String>> {
let count = self.key_count()?;
let mut keys = Vec::new();
for i in 0..count {
match self.key_by_index(i) {
Ok(key) => keys.push(key),
Err(_) => continue,
}
}
Ok(keys)
}
}
impl Drop for SMC {
fn drop(&mut self) {
unsafe {
IOServiceClose(self.conn);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_acc_clusters_values() {
#[rustfmt::skip]
let data = [
0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x05, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let (e, p) = parse_acc_clusters(&data).unwrap();
assert_eq!(e, "voltage-states23-sram");
assert_eq!(p, "voltage-states5-sram");
assert!(parse_acc_clusters(&[]).is_none());
assert!(parse_acc_clusters(&[1, 0, 0, 0, 0, 0, 0, 0]).is_none());
}
#[test]
fn parse_cpu_core_counts() {
for (value, expected) in [
("proc 18:6:0:12", (12, 6, true)),
("proc 16:12:4:0", (4, 12, false)),
("proc 8:4:4:0", (4, 4, false)),
("proc 8:4:4", (4, 4, false)),
("", (0, 0, false)),
("garbage", (0, 0, false)),
("10:8:2", (0, 0, false)),
("proc 8", (0, 0, false)),
("proc 8:4", (0, 0, false)),
("proc 24:6:0:12:6", (0, 0, false)),
] {
assert_eq!(parse_cpu_cores(value), expected, "{value}");
}
}
#[test]
fn converts_energy_using_the_exact_sample_duration() {
let watts = watts_from_energy(1_000_000.0, "uJ", Duration::from_micros(250_500)).unwrap();
assert!((watts - 3.992_016).abs() < 0.000_001);
}
#[test]
fn cfio_channel_filter_semantics() {
let group = [("Energy Model", None)];
assert!(cfio_channel_matches(&group, "Energy Model", ""));
assert!(cfio_channel_matches(&group, "Energy Model", "CPU Core Performance States"));
assert!(!cfio_channel_matches(&group, "CPU Stats", "CPU Core Performance States"));
let subgroup = [("CPU Stats", Some("CPU Core Performance States"))];
assert!(cfio_channel_matches(&subgroup, "CPU Stats", "CPU Core Performance States"));
assert!(!cfio_channel_matches(&subgroup, "CPU Stats", "CPU Performance States"));
assert!(!cfio_channel_matches(&subgroup, "GPU Stats", "CPU Core Performance States"));
assert!(cfio_channel_matches(&[], "CPU Stats", "CPU Core Performance States"));
assert!(cfio_channel_matches(&[], "Energy Model", ""));
}
}