use core_foundation::dictionary::CFDictionaryRef;
use serde::Serialize;
use crate::sources::{
IOHIDSensors, IOReport, SMC, SocInfo, cfio_get_residencies, cfio_watts, libc_ram, libc_swap,
};
type WithError<T> = Result<T, Box<dyn std::error::Error>>;
const CPU_FREQ_CORE_SUBG: &str = "CPU Core Performance States";
const GPU_FREQ_DICE_SUBG: &str = "GPU Performance States";
#[derive(Debug, Default, Serialize)]
pub struct TempMetrics {
pub cpu_temp_avg: f32, pub gpu_temp_avg: f32, }
#[derive(Debug, Default, Serialize)]
pub struct MemMetrics {
pub ram_total: u64, pub ram_usage: u64, pub swap_total: u64, pub swap_usage: u64, }
#[derive(Debug, Default, Serialize)]
pub struct Metrics {
pub temp: TempMetrics,
pub memory: MemMetrics,
pub ecpu_usage: (u32, f32), pub pcpu_usage: (u32, f32), pub cpu_usage_pct: f32, pub gpu_usage: (u32, f32), pub cpu_power: f32, pub gpu_power: f32, pub ane_power: f32, pub all_power: f32, pub sys_power: f32, pub ram_power: f32, pub gpu_ram_power: f32, }
pub fn zero_div<T: core::ops::Div<Output = T> + Default + PartialEq>(a: T, b: T) -> T {
let zero: T = Default::default();
if b == zero { zero } else { a / b }
}
fn calc_freq(item: CFDictionaryRef, freqs: &[u32]) -> (u32, f32) {
let items = cfio_get_residencies(item); let (len1, len2) = (items.len(), freqs.len());
assert!(len1 > len2, "cacl_freq invalid data: {} vs {}", len1, len2);
let offset = items.iter().position(|x| x.0 != "IDLE" && x.0 != "DOWN" && x.0 != "OFF").unwrap();
let usage = items.iter().map(|x| x.1 as f64).skip(offset).sum::<f64>();
let total = items.iter().map(|x| x.1 as f64).sum::<f64>();
let count = freqs.len();
let mut avg_freq = 0f64;
for i in 0..count {
let percent = zero_div(items[i + offset].1 as _, usage);
avg_freq += percent * freqs[i] as f64;
}
let usage_ratio = zero_div(usage, total);
let min_freq = *freqs.first().unwrap() as f64;
let max_freq = *freqs.last().unwrap() as f64;
let from_max = (avg_freq.max(min_freq) * usage_ratio) / max_freq;
(avg_freq as u32, from_max as f32)
}
fn calc_freq_final(items: &[(u32, f32)], freqs: &[u32]) -> (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).sum(), items.len() as f32);
let min_freq = *freqs.first().unwrap() as f32;
(avg_freq.max(min_freq) as u32, avg_perc)
}
fn init_smc() -> WithError<(SMC, Vec<String>, Vec<String>)> {
let mut smc = SMC::new()?;
const FLOAT_TYPE: u32 = 1718383648;
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 != FLOAT_TYPE {
continue;
}
let _ = match smc.read_val(name) {
Ok(val) => val,
Err(_) => continue,
};
match name {
name if name.starts_with("Tp") || name.starts_with("Te") || name.starts_with("Ts") => {
cpu_sensors.push(name.clone())
}
name if name.starts_with("Tg") => gpu_sensors.push(name.clone()),
_ => (),
}
}
Ok((smc, cpu_sensors, gpu_sensors))
}
pub struct Sampler {
soc: SocInfo,
ior: IOReport,
hid: IOHIDSensors,
smc: SMC,
smc_cpu_keys: Vec<String>,
smc_gpu_keys: Vec<String>,
}
impl Sampler {
pub fn new() -> WithError<Self> {
let channels = vec![
("Energy Model", None), ("CPU Stats", Some(CPU_FREQ_CORE_SUBG)), ("GPU Stats", Some(GPU_FREQ_DICE_SUBG)), ];
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<TempMetrics> {
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());
if val != 0.0 {
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());
if val != 0.0 {
gpu_metrics.push(val);
}
}
let cpu_temp_avg = zero_div(cpu_metrics.iter().sum::<f32>(), cpu_metrics.len() as f32);
let gpu_temp_avg = zero_div(gpu_metrics.iter().sum::<f32>(), gpu_metrics.len() as f32);
Ok(TempMetrics { cpu_temp_avg, gpu_temp_avg })
}
fn get_temp_hid(&mut self) -> WithError<TempMetrics> {
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") {
cpu_values.push(*value);
continue;
}
if name.starts_with("GPU MTR Temp Sensor") {
if *value > 0.0 && *value <= 150.0 {
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<TempMetrics> {
match !self.smc_cpu_keys.is_empty() {
true => self.get_temp_smc(),
false => self.get_temp_hid(),
}
}
fn get_mem(&mut self) -> WithError<MemMetrics> {
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<f32> {
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: u32) -> WithError<Metrics> {
let measures: usize = 4;
let mut results: Vec<Metrics> = Vec::with_capacity(measures);
for (sample, dt) in self.ior.get_samples(duration as u64, measures) {
let mut ecpu_usages = Vec::new();
let mut pcpu_usages = Vec::new();
let mut rs = Metrics::default();
for x in sample {
if x.group == "CPU Stats" && x.subgroup == CPU_FREQ_CORE_SUBG {
if x.channel.starts_with("PCPU") {
pcpu_usages.push(calc_freq(x.item, &self.soc.pcpu_freqs));
continue;
}
if x.channel.starts_with("ECPU") || x.channel.starts_with("MCPU") {
ecpu_usages.push(calc_freq(x.item, &self.soc.ecpu_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..]),
_ => {}
}
}
if x.group == "Energy Model" {
match x.channel.as_str() {
"GPU Energy" => rs.gpu_power += cfio_watts(x.item, &x.unit, dt)?,
c if c.ends_with("CPU Energy") => rs.cpu_power += cfio_watts(x.item, &x.unit, dt)?,
c if c.starts_with("ANE") => rs.ane_power += cfio_watts(x.item, &x.unit, dt)?,
c if c.starts_with("DRAM") => rs.ram_power += cfio_watts(x.item, &x.unit, dt)?,
c if c.starts_with("GPU SRAM") => rs.gpu_ram_power += cfio_watts(x.item, &x.unit, dt)?,
_ => {}
}
}
}
ecpu_usages.retain(|&(_, pct)| pct > 0.0);
rs.ecpu_usage = calc_freq_final(&ecpu_usages, &self.soc.ecpu_freqs);
rs.pcpu_usage = calc_freq_final(&pcpu_usages, &self.soc.pcpu_freqs);
results.push(rs);
}
let ecores = self.soc.ecpu_cores as f32;
let pcores = self.soc.pcpu_cores as f32;
let tcores = ecores + pcores;
let mut rs = Metrics::default();
rs.ecpu_usage.0 = zero_div(results.iter().map(|x| x.ecpu_usage.0).sum(), measures as _);
rs.ecpu_usage.1 = zero_div(results.iter().map(|x| x.ecpu_usage.1).sum(), measures as _);
rs.pcpu_usage.0 = zero_div(results.iter().map(|x| x.pcpu_usage.0).sum(), measures as _);
rs.pcpu_usage.1 = zero_div(results.iter().map(|x| x.pcpu_usage.1).sum(), measures as _);
rs.cpu_usage_pct = zero_div(rs.ecpu_usage.1 * ecores + rs.pcpu_usage.1 * pcores, tcores);
rs.gpu_usage.0 = zero_div(results.iter().map(|x| x.gpu_usage.0).sum(), measures as _);
rs.gpu_usage.1 = zero_div(results.iter().map(|x| x.gpu_usage.1).sum(), measures as _);
rs.cpu_power = zero_div(results.iter().map(|x| x.cpu_power).sum(), measures as _);
rs.gpu_power = zero_div(results.iter().map(|x| x.gpu_power).sum(), measures as _);
rs.ane_power = zero_div(results.iter().map(|x| x.ane_power).sum(), measures as _);
rs.ram_power = zero_div(results.iter().map(|x| x.ram_power).sum(), measures as _);
rs.gpu_ram_power = zero_div(results.iter().map(|x| x.gpu_ram_power).sum(), measures as _);
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)
}
pub fn get_soc_info(&self) -> &SocInfo {
&self.soc
}
}