From 642a35a1fb1d8efcb2e0f434b2cfb9e899dec143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rik=20Levente?= Date: Thu, 1 Aug 2024 22:33:57 +0200 Subject: [PATCH] macOS hwinfo --- README.md | 1 + platforms/unix/hardwareinfo/Cargo.toml | 4 + platforms/unix/hardwareinfo/src/metrics.rs | 278 +++++++ platforms/unix/hardwareinfo/src/sources.rs | 862 +++++++++++++++++++++ 4 files changed, 1145 insertions(+) create mode 100644 platforms/unix/hardwareinfo/src/metrics.rs create mode 100644 platforms/unix/hardwareinfo/src/sources.rs diff --git a/README.md b/README.md index 5ef2aa9..d5ae8e8 100644 --- a/README.md +++ b/README.md @@ -37,3 +37,4 @@ - This software is licensed under: [GPL-3.0](https://github.com/Levminer/cores/blob/dev/LICENSE.md) - You can buy the software as an individual on the [website](https://cores.levminer.com/#pricing). If you are planning to use this software as a business please contact me at: cores@levminer.com +- Credits: [Libre Hardware Monitor](https://github.com/LibreHardwareMonitor/LibreHardwareMonitor), [Mac Monitor](https://github.com/vladkens/macmon), [Resources](https://github.com/nokyan/resources) diff --git a/platforms/unix/hardwareinfo/Cargo.toml b/platforms/unix/hardwareinfo/Cargo.toml index 644a29f..8f482a2 100644 --- a/platforms/unix/hardwareinfo/Cargo.toml +++ b/platforms/unix/hardwareinfo/Cargo.toml @@ -14,3 +14,7 @@ serde_json = "1" log = "0.4" uuid = { version = "1.6.1", features = ["v4"] } directories = "4.0.1" + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "0.9.4" +libc = "0.2.155" diff --git a/platforms/unix/hardwareinfo/src/metrics.rs b/platforms/unix/hardwareinfo/src/metrics.rs new file mode 100644 index 0000000..c84b1d1 --- /dev/null +++ b/platforms/unix/hardwareinfo/src/metrics.rs @@ -0,0 +1,278 @@ +use core_foundation::dictionary::CFDictionaryRef; + +use crate::sources::{ + cfio_get_residencies, cfio_watts, libc_ram, libc_swap, IOHIDSensors, IOReport, SocInfo, SMC, +}; + +type WithError = Result>; + +// const CPU_FREQ_DICE_SUBG: &str = "CPU Complex Performance States"; +const CPU_FREQ_CORE_SUBG: &str = "CPU Core Performance States"; +const GPU_FREQ_DICE_SUBG: &str = "GPU Performance States"; + +// MARK: Structs + +#[derive(Debug, Default)] +pub struct TempMetrics { + pub cpu_temp_avg: f32, // Celsius + pub gpu_temp_avg: f32, // Celsius +} + +#[derive(Debug, Default)] +pub struct MemMetrics { + pub ram_total: u64, // bytes + pub ram_usage: u64, // bytes + pub swap_total: u64, // bytes + pub swap_usage: u64, // bytes +} + +#[derive(Debug, Default)] +pub struct Metrics { + pub temp: TempMetrics, + pub memory: MemMetrics, + pub ecpu_usage: (u32, f32), // freq, percent_from_max + pub pcpu_usage: (u32, f32), // freq, percent_from_max + pub gpu_usage: (u32, f32), // freq, percent_from_max + pub cpu_power: f32, // Watts + pub gpu_power: f32, // Watts + pub ane_power: f32, // Watts + pub all_power: f32, // Watts + pub sys_power: f32, // Watts +} + +// MARK: Helpers + +fn zero_div + Default + PartialEq>(a: T, b: T) -> T { + let zero: T = Default::default(); + return if b == zero { zero } else { a / b }; +} + +fn calc_freq(item: CFDictionaryRef, freqs: &Vec) -> (u32, f32) { + let residencies = cfio_get_residencies(item); // (ns, freq) + let (len1, len2) = (residencies.len(), freqs.len()); + assert!(len1 > len2, "cacl_freq invalid data: {} vs {}", len1, len2); // todo? + + // first is IDLE for CPU and OFF for GPU + let usage = residencies.iter().map(|x| x.1 as f64).skip(1).sum::(); + let total = residencies.iter().map(|x| x.1 as f64).sum::(); + let count = freqs.len(); + // println!("{:?}", residencies); + + let mut freq = 0f64; + for i in 0..count { + let percent = zero_div(residencies[i + 1].1 as _, usage); + freq += percent * freqs[i] as f64; + } + + let percent = zero_div(usage, total); + let min_freq = freqs.first().unwrap().clone() as f64; + let max_freq = freqs.last().unwrap().clone() as f64; + let from_max = (freq.max(min_freq) * percent) / max_freq; + + (freq as u32, from_max as f32) +} + +fn calc_freq_final(items: &Vec<(u32, f32)>, freqs: &Vec) -> (u32, f32) { + let avg_freq = zero_div(items.iter().map(|x| x.0 as f32).sum(), items.len() as f32); + let avg_perc = zero_div(items.iter().map(|x| x.1 as f32).sum(), items.len() as f32); + let min_freq = freqs.first().unwrap().clone() as f32; + + (avg_freq.max(min_freq) as u32, avg_perc) +} + +fn init_smc() -> WithError<(SMC, Vec, Vec)> { + let mut smc = SMC::new()?; + + let mut cpu_sensors = Vec::new(); + let mut gpu_sensors = Vec::new(); + + let names = smc.read_all_keys().unwrap_or(vec![]); + for name in &names { + let key = match smc.read_key_info(&name) { + Ok(key) => key, + Err(_) => continue, + }; + + if key.data_size != 4 || key.data_type != 1718383648 { + continue; + } + + let _ = match smc.read_val(&name) { + Ok(val) => val, + Err(_) => continue, + }; + + // Unfortunately, it is not known which keys are responsible for what. + // Basically in the code that can be found publicly "Tp" is used for CPU and "Tg" for GPU. + + match name { + name if name.starts_with("Tp") => cpu_sensors.push(name.clone()), + name if name.starts_with("Tg") => gpu_sensors.push(name.clone()), + _ => (), + } + } + + // println!("{} {}", cpu_sensors.len(), gpu_sensors.len()); + Ok((smc, cpu_sensors, gpu_sensors)) +} + +// MARK: Sampler + +pub struct Sampler { + soc: SocInfo, + ior: IOReport, + hid: IOHIDSensors, + smc: SMC, + smc_cpu_keys: Vec, + smc_gpu_keys: Vec, +} + +impl Sampler { + pub fn new() -> WithError { + let channels = vec![ + ("Energy Model", None), // cpu/gpu/ane power + // ("CPU Stats", Some(CPU_FREQ_DICE_SUBG)), // cpu freq by cluster + ("CPU Stats", Some(CPU_FREQ_CORE_SUBG)), // cpu freq per core + ("GPU Stats", Some(GPU_FREQ_DICE_SUBG)), // gpu freq + ]; + + let soc = SocInfo::new()?; + let ior = IOReport::new(channels)?; + let hid = IOHIDSensors::new()?; + let (smc, smc_cpu_keys, smc_gpu_keys) = init_smc()?; + + Ok(Sampler { soc, ior, hid, smc, smc_cpu_keys, smc_gpu_keys }) + } + + fn get_temp_smc(&mut self) -> WithError { + let mut cpu_metrics = Vec::new(); + for sensor in &self.smc_cpu_keys { + let val = self.smc.read_val(sensor)?; + let val = f32::from_le_bytes(val.data[0..4].try_into().unwrap()); + cpu_metrics.push(val); + } + + let mut gpu_metrics = Vec::new(); + for sensor in &self.smc_gpu_keys { + let val = self.smc.read_val(sensor)?; + let val = f32::from_le_bytes(val.data[0..4].try_into().unwrap()); + gpu_metrics.push(val); + } + + let cpu_temp_avg = zero_div(cpu_metrics.iter().sum::(), cpu_metrics.len() as f32); + let gpu_temp_avg = zero_div(gpu_metrics.iter().sum::(), gpu_metrics.len() as f32); + + Ok(TempMetrics { cpu_temp_avg, gpu_temp_avg }) + } + + fn get_temp_hid(&mut self) -> WithError { + let metrics = self.hid.get_metrics(); + + let mut cpu_values = Vec::new(); + let mut gpu_values = Vec::new(); + + for (name, value) in &metrics { + if name.starts_with("pACC MTR Temp Sensor") || name.starts_with("eACC MTR Temp Sensor") { + // println!("{}: {}", name, value); + cpu_values.push(*value); + continue; + } + + if name.starts_with("GPU MTR Temp Sensor") { + // println!("{}: {}", name, value); + gpu_values.push(*value); + continue; + } + } + + let cpu_temp_avg = zero_div(cpu_values.iter().sum(), cpu_values.len() as f32); + let gpu_temp_avg = zero_div(gpu_values.iter().sum(), gpu_values.len() as f32); + + Ok(TempMetrics { cpu_temp_avg, gpu_temp_avg }) + } + + fn get_temp(&mut self) -> WithError { + // HID for M1, SMC for M2/M3 + // UPD: Looks like HID/SMC related to OS version, not to the chip (SMC available from macOS 14) + match self.smc_cpu_keys.len() > 0 { + true => self.get_temp_smc(), + false => self.get_temp_hid(), + } + } + + fn get_mem(&mut self) -> WithError { + let (ram_usage, ram_total) = libc_ram()?; + let (swap_usage, swap_total) = libc_swap()?; + Ok(MemMetrics { ram_total, ram_usage, swap_total, swap_usage }) + } + + fn get_sys_power(&mut self) -> WithError { + let val = self.smc.read_val("PSTR")?; + let val = f32::from_le_bytes(val.data.clone().try_into().unwrap()); + Ok(val) + } + + pub fn get_metrics(&mut self, duration: u64) -> WithError { + let mut rs = Metrics::default(); + + let mut ecpu_usages = Vec::new(); + let mut pcpu_usages = Vec::new(); + + for x in self.ior.get_sample(duration) { + // if x.group == "CPU Stats" && x.subgroup == CPU_FREQ_DICE_SUBG { + // match x.channel.as_str() { + // "ECPU" => rs.ecpu_usage = calc_freq(x.item, &self.soc.ecpu_freqs), + // "PCPU" => rs.pcpu_usage = calc_freq(x.item, &self.soc.pcpu_freqs), + // _ => {} + // } + // } + + if x.group == "CPU Stats" && x.subgroup == CPU_FREQ_CORE_SUBG { + if x.channel.contains("ECPU") { + ecpu_usages.push(calc_freq(x.item, &self.soc.ecpu_freqs)); + continue; + } + + if x.channel.contains("PCPU") { + pcpu_usages.push(calc_freq(x.item, &self.soc.pcpu_freqs)); + continue; + } + } + + if x.group == "GPU Stats" && x.subgroup == GPU_FREQ_DICE_SUBG { + match x.channel.as_str() { + "GPUPH" => rs.gpu_usage = calc_freq(x.item, &self.soc.gpu_freqs[1..].to_vec()), + _ => {} + } + } + + if x.group == "Energy Model" { + match x.channel.as_str() { + "CPU Energy" => rs.cpu_power += cfio_watts(x.item, &x.unit, duration)?, + "GPU Energy" => rs.gpu_power += cfio_watts(x.item, &x.unit, duration)?, + c if c.starts_with("ANE") => rs.ane_power += cfio_watts(x.item, &x.unit, duration)?, + _ => {} + } + } + } + + // println!("----------"); + // println!("{:?}", ecpu_usages); + // println!("{:?}", pcpu_usages); + // println!("1 {:?} {:?}", rs.ecpu_usage, rs.pcpu_usage); + rs.ecpu_usage = calc_freq_final(&ecpu_usages, &self.soc.ecpu_freqs); + rs.pcpu_usage = calc_freq_final(&pcpu_usages, &self.soc.pcpu_freqs); + // println!("2 {:?} {:?}", rs.ecpu_usage, rs.pcpu_usage); + + rs.all_power = rs.cpu_power + rs.gpu_power + rs.ane_power; + rs.memory = self.get_mem()?; + rs.temp = self.get_temp()?; + + rs.sys_power = match self.get_sys_power() { + Ok(val) => val.max(rs.all_power), + Err(_) => 0.0, + }; + + Ok(rs) + } +} diff --git a/platforms/unix/hardwareinfo/src/sources.rs b/platforms/unix/hardwareinfo/src/sources.rs new file mode 100644 index 0000000..25e8815 --- /dev/null +++ b/platforms/unix/hardwareinfo/src/sources.rs @@ -0,0 +1,862 @@ +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +use std::{ + collections::HashMap, + marker::{PhantomData, PhantomPinned}, + mem::{size_of, MaybeUninit}, + os::raw::c_void, + ptr::null, +}; + +use core_foundation::{ + array::{CFArrayGetCount, CFArrayGetValueAtIndex, CFArrayRef}, + base::{kCFAllocatorDefault, kCFAllocatorNull, CFAllocatorRef, CFRange, CFRelease, CFTypeRef}, + data::{CFDataGetBytes, CFDataGetLength, CFDataRef}, + dictionary::{ + kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks, CFDictionaryCreate, + CFDictionaryCreateMutableCopy, CFDictionaryGetCount, CFDictionaryGetKeysAndValues, + CFDictionaryGetValue, CFDictionaryRef, CFMutableDictionaryRef, + }, + number::{kCFNumberSInt32Type, CFNumberCreate, CFNumberRef}, + string::{kCFStringEncodingUTF8, CFStringCreateWithBytesNoCopy, CFStringGetCString, CFStringRef}, +}; + +pub type WithError = Result>; +pub type CVoidRef = *const std::ffi::c_void; + +// MARK: CFUtils + +pub fn cfnum(val: i32) -> CFNumberRef { + unsafe { CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &val as *const i32 as _) } +} + +pub fn cfstr(val: &str) -> CFStringRef { + // this creates broken objects if string len > 9 + // CFString::from_static_string(val).as_concrete_TypeRef() + // CFString::new(val).as_concrete_TypeRef() + + unsafe { + CFStringCreateWithBytesNoCopy( + kCFAllocatorDefault, + val.as_ptr(), + val.len() as isize, + kCFStringEncodingUTF8, + 0, + kCFAllocatorNull, + ) + } +} + +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() + } +} + +pub fn cfdict_keys(dict: CFDictionaryRef) -> Vec { + unsafe { + let count = CFDictionaryGetCount(dict) as usize; + let mut keys: Vec = Vec::with_capacity(count); + let mut vals: Vec = 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() + } +} + +pub fn cfdict_get_val(dict: CFDictionaryRef, key: &str) -> Option { + unsafe { + let key = cfstr(key); + let val = CFDictionaryGetValue(dict, key as _); + CFRelease(key as _); + + match val { + _ if val.is_null() => None, + _ => Some(val), + } + } +} + +// MARK: IOReport Bindings + +#[link(name = "IOKit", kind = "framework")] +#[rustfmt::skip] +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; + +#[link(name = "IOReport", kind = "dylib")] +#[rustfmt::skip] +extern "C" { + fn IOReportCopyAllChannels(a: u64, b: u64) -> CFDictionaryRef; + fn IOReportCopyChannelsInGroup(a: CFStringRef, b: CFStringRef, c: u64, d: u64, e: u64) -> CFDictionaryRef; + fn IOReportMergeChannels(a: CFDictionaryRef, b: CFDictionaryRef, nil: CFTypeRef); + 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; +} + +// MARK: IOReport helpers + +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), + } +} + +pub fn cfio_get_props(entry: u32, name: String) -> WithError { + unsafe { + let mut props: MaybeUninit = 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()) + } +} + +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) }; + res.push((from_cfstr(name), val)); + } + + res +} + +pub fn cfio_watts(item: CFDictionaryRef, unit: &String, duration: u64) -> WithError { + let val = unsafe { IOReportSimpleGetIntegerValue(item, 0) } as f32; + let val = val / (duration as f32 / 1000.0); + match unit.as_str() { + "mJ" => Ok(val / 1e3f32), + "uJ" => Ok(val / 1e6f32), + "nJ" => Ok(val / 1e9f32), + _ => Err(format!("Invalid energy unit: {}", unit).into()), + } +} + +// MARK: IOServiceIterator + +pub struct IOServiceIterator { + existing: u32, +} + +impl IOServiceIterator { + pub fn new(service_name: &str) -> WithError { + 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 { + let next = unsafe { IOIteratorNext(self.existing) }; + if next == 0 { + return None; + } + + let mut name = [0; 128]; // 128 defined in apple docs + 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)) + } +} + +// MARK: IOReportIterator + +pub struct IOReportIterator { + sample: CFDictionaryRef, + index: isize, + items: CFArrayRef, + items_size: isize, +} + +impl IOReportIterator { + pub fn new(data: CFDictionaryRef) -> Self { + let items = cfdict_get_val(data, "IOReportChannels").unwrap() as CFArrayRef; + let items_size = unsafe { CFArrayGetCount(items) } as isize; + Self { sample: data, items, items_size, index: 0 } + } +} + +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 { + if self.index >= self.items_size { + return None; + } + + let item = unsafe { CFArrayGetValueAtIndex(self.items, self.index) } 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(); + + self.index += 1; + Some(IOReportIteratorItem { group, subgroup, channel, unit, item }) + } +} + +// MARK: RAM + +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::(); + 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::(); + + 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 = (0 + + 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 + + 0) + * 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::(); + let mut xsw: libc::xsw_usage = std::mem::zeroed::(); + + 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)) +} + +// MARK: SockInfo + +#[derive(Debug, Default, Clone)] +pub struct SocInfo { + pub mac_model: String, + pub chip_name: String, + pub memory_gb: u8, + pub ecpu_cores: u8, + pub pcpu_cores: u8, + pub ecpu_freqs: Vec, + pub pcpu_freqs: Vec, + pub gpu_cores: u8, + pub gpu_freqs: Vec, +} + +impl SocInfo { + pub fn new() -> WithError { + get_soc_info() + } +} + +// dynamic voltage and frequency scaling +pub fn get_dvfs_mhz(dict: CFDictionaryRef, key: &str) -> (Vec, Vec) { + unsafe { + let obj = cfdict_get_val(dict, key).unwrap() 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); + + // obj_val is pairs of (freq, voltage) 4 bytes each + 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]]); + freqs[i] = freqs[i] / 1000 / 1000; // as MHz + } + + (volts, freqs) + } +} + +pub fn run_system_profiler() -> WithError { + // system_profiler -listDataTypes + let out = std::process::Command::new("system_profiler") + .args(&["SPHardwareDataType", "SPDisplaysDataType", "SPSoftwareDataType", "-json"]) + .output()?; + + let out = std::str::from_utf8(&out.stdout)?; + let out = serde_json::from_str::(out)?; + Ok(out) +} + +pub fn get_soc_info() -> WithError { + let out = run_system_profiler()?; + let mut info = SocInfo::default(); + + // SPHardwareDataType.0.chip_type + let chip_name = out["SPHardwareDataType"][0]["chip_type"].as_str().unwrap().to_string(); + + // SPHardwareDataType.0.machine_model + let mac_model = out["SPHardwareDataType"][0]["machine_model"].as_str().unwrap().to_string(); + + // SPHardwareDataType.0.physical_memory -> "x GB" + let mem_gb = out["SPHardwareDataType"][0]["physical_memory"].as_str(); + let mem_gb = mem_gb.expect("No memory found").strip_suffix(" GB").unwrap(); + let mem_gb = mem_gb.parse::().unwrap(); + + // SPHardwareDataType.0.number_processors -> "proc x:y:z" + let cpu_cores = out["SPHardwareDataType"][0]["number_processors"].as_str(); + let cpu_cores = cpu_cores.expect("No CPU cores found").strip_prefix("proc ").unwrap(); + let cpu_cores = cpu_cores.split(':').map(|x| x.parse::().unwrap()).collect::>(); + assert_eq!(cpu_cores.len(), 3, "Invalid number of CPU cores"); + let (ecpu_cores, pcpu_cores, _) = (cpu_cores[2], cpu_cores[1], cpu_cores[0]); + + let gpu_cores = match out["SPDisplaysDataType"][0]["sppci_cores"].as_str() { + Some(x) => x.parse::().unwrap(), + None => 0, + }; + + info.chip_name = chip_name; + info.mac_model = mac_model; + info.memory_gb = mem_gb as u8; + info.gpu_cores = gpu_cores as u8; + info.ecpu_cores = ecpu_cores as u8; + info.pcpu_cores = pcpu_cores as u8; + + // cpu frequencies + for (entry, name) in IOServiceIterator::new("AppleARMIODevice")? { + if name == "pmgr" { + let item = cfio_get_props(entry, name)?; + // `strings /usr/bin/powermetrics | grep voltage-states` uses non sram keys + // but their values are zero, so sram used here, its looks valid + info.ecpu_freqs = get_dvfs_mhz(item, "voltage-states1-sram").1; + info.pcpu_freqs = get_dvfs_mhz(item, "voltage-states5-sram").1; + info.gpu_freqs = get_dvfs_mhz(item, "voltage-states9").1; + unsafe { CFRelease(item as _) } + } + } + + if info.ecpu_freqs.len() == 0 || info.pcpu_freqs.len() == 0 { + return Err("No CPU cores found".into()); + } + + Ok(info) +} + +// MARK: IOReport + +unsafe fn cfio_get_chan(items: Vec<(&str, Option<&str>)>) -> WithError { + // if no items are provided, return all channels + if items.len() == 0 { + let c = IOReportCopyAllChannels(0, 0); + let r = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, CFDictionaryGetCount(c), c); + CFRelease(c as _); + return Ok(r); + } + + let mut channels = vec![]; + for (group, subgroup) in items { + let gname = cfstr(group); + let sname = subgroup.map_or(null(), |x| cfstr(x)); + let chan = IOReportCopyChannelsInGroup(gname, sname, 0, 0, 0); + channels.push(chan); + + CFRelease(gname as _); + if subgroup.is_some() { + CFRelease(sname as _); + } + } + + let chan = channels[0]; + for i in 1..channels.len() { + IOReportMergeChannels(chan, channels[i], null()); + } + + let size = CFDictionaryGetCount(chan); + let chan = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, size, chan); + + for i in 0..channels.len() { + CFRelease(channels[i] as _); + } + + if cfdict_get_val(chan, "IOReportChannels").is_none() { + return Err("Failed to get channels".into()); + } + + Ok(chan) +} + +unsafe fn cfio_get_subs(chan: CFMutableDictionaryRef) -> WithError { + let mut s: MaybeUninit = MaybeUninit::uninit(); + let rs = IOReportCreateSubscription(std::ptr::null(), chan, s.as_mut_ptr(), 0, std::ptr::null()); + if rs == std::ptr::null() { + return Err("Failed to create subscription".into()); + } + + s.assume_init(); + Ok(rs) +} + +pub struct IOReport { + subs: IOReportSubscriptionRef, + chan: CFMutableDictionaryRef, +} + +impl IOReport { + pub fn new(channels: Vec<(&str, Option<&str>)>) -> WithError { + let chan = unsafe { cfio_get_chan(channels)? }; + let subs = unsafe { cfio_get_subs(chan)? }; + + Ok(Self { subs, chan }) + } + + 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) + } + } +} + +impl Drop for IOReport { + fn drop(&mut self) { + unsafe { + CFRelease(self.chan as _); + CFRelease(self.subs as _); + } + } +} + +// MARK: IOHID Bindings +// referenced from: https://github.com/freedomtan/sensors/blob/master/sensors/sensors.m + +#[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] +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; +} + +// MARK: IOHIDSensors + +pub struct IOHIDSensors { + sensors: CFDictionaryRef, +} + +impl IOHIDSensors { + pub fn new() -> WithError { + let keys = vec![cfstr("PrimaryUsagePage"), cfstr("PrimaryUsage")]; + let nums = vec![cfnum(kHIDPage_AppleVendor), cfnum(kHIDUsage_AppleVendor_TemperatureSensor)]; + + let dict = unsafe { + CFDictionaryCreate( + kCFAllocatorDefault, + keys.as_ptr() as _, + nums.as_ptr() as _, + 2, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ) + }; + + Ok(Self { sensors: dict }) + } + + 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 _); + 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 _); + } + } +} + +// MARK: SMC Bindings + +#[link(name = "IOKit", kind = "framework")] +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, +} + +// MARK: SMC + +pub struct SMC { + conn: u32, + keys: HashMap, +} + +impl SMC { + pub fn new() -> WithError { + 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 { + let ival = input as *const _ as _; + let ilen = size_of::(); + let mut oval = KeyData::default(); + let mut olen = size_of::(); + + let rs = unsafe { + IOConnectCallStructMethod(self.conn, 2, ival, ilen, &mut oval as *mut _ as _, &mut olen) + }; + + if rs != 0 { + // println!("{:?}", input); + 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) + } + + pub fn key_by_index(&self, index: u32) -> WithError { + 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 { + if key.len() != 4 { + return Err("SMC key must be 4 bytes long".into()); + } + + // key is FourCC + let key = key.bytes().fold(0, |acc, x| (acc << 8) + x as u32); + if let Some(ki) = self.keys.get(&key) { + // println!("cache hit for {}", key); + return Ok(ki.clone()); + } + + 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 read_val(&mut self, key: &str) -> WithError { + let name = key.to_string(); + + let key_info = self.read_key_info(key)?; + let key = key.bytes().fold(0, |acc, x| (acc << 8) + x as u32); + // println!("{:?}", key_info); + + let ival = KeyData { data8: 5, key, key_info, ..Default::default() }; + let oval = self.read(&ival)?; + // println!("{:?}", oval.bytes); + + 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_all_keys(&mut self) -> WithError> { + let val = self.read_val("#KEY")?; + let val = u32::from_be_bytes(val.data[0..4].try_into().unwrap()); + + let mut keys = Vec::new(); + for i in 0..val { + let key = self.key_by_index(i)?; + let val = self.read_val(&key); + if val.is_err() { + continue; + } + + let val = val.unwrap(); + keys.push(val.name); + } + + Ok(keys) + } +} + +impl Drop for SMC { + fn drop(&mut self) { + unsafe { + IOServiceClose(self.conn); + } + } +}