use crate::device::macos_native::get_native_metrics_manager;
use crate::device::{
AppleSiliconCpuInfo, CoreType, CoreUtilization, CpuInfo, CpuPlatformType, CpuReader,
CpuSocketInfo,
};
use crate::utils::system::get_hostname;
use chrono::Local;
use std::process::Command;
use std::sync::{Mutex, RwLock};
use sysinfo::System;
type CpuHardwareParseResult = Result<(String, u32, u32, u32, u32, u32), Box<dyn std::error::Error>>;
type IntelCpuInfo = (String, u32, u32, u32, u32, u32);
pub struct MacOsCpuReader {
is_apple_silicon: bool,
system: RwLock<System>,
first_refresh_done: RwLock<bool>,
cached_cpu_model: Mutex<Option<String>>,
cached_p_core_count: Mutex<Option<u32>>,
cached_e_core_count: Mutex<Option<u32>>,
cached_gpu_core_count: Mutex<Option<u32>>,
cached_p_core_l2_cache_mb: Mutex<Option<u32>>,
cached_e_core_l2_cache_mb: Mutex<Option<u32>>,
cached_intel_info: Mutex<Option<IntelCpuInfo>>,
}
impl Default for MacOsCpuReader {
fn default() -> Self {
Self::new()
}
}
impl MacOsCpuReader {
pub fn new() -> Self {
let is_apple_silicon = Self::detect_apple_silicon();
let system = System::new();
Self {
is_apple_silicon,
system: RwLock::new(system),
first_refresh_done: RwLock::new(false),
cached_cpu_model: Mutex::new(None),
cached_p_core_count: Mutex::new(None),
cached_e_core_count: Mutex::new(None),
cached_gpu_core_count: Mutex::new(None),
cached_p_core_l2_cache_mb: Mutex::new(None),
cached_e_core_l2_cache_mb: Mutex::new(None),
cached_intel_info: Mutex::new(None),
}
}
fn detect_apple_silicon() -> bool {
if let Ok(output) = Command::new("uname").arg("-m").output() {
let architecture = String::from_utf8_lossy(&output.stdout);
return architecture.trim() == "arm64";
}
false
}
fn get_cpu_info_from_system(&self) -> Result<CpuInfo, Box<dyn std::error::Error>> {
let hostname = get_hostname();
let instance = hostname.clone();
let time = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
if self.is_apple_silicon {
self.get_apple_silicon_cpu_info(hostname, instance, time)
} else {
self.get_intel_mac_cpu_info(hostname, instance, time)
}
}
fn get_apple_silicon_cpu_info(
&self,
hostname: String,
instance: String,
time: String,
) -> Result<CpuInfo, Box<dyn std::error::Error>> {
let cached_values = {
let cpu_model = self.cached_cpu_model.lock().unwrap().clone();
let p_core_count = *self.cached_p_core_count.lock().unwrap();
let e_core_count = *self.cached_e_core_count.lock().unwrap();
let gpu_core_count = *self.cached_gpu_core_count.lock().unwrap();
(cpu_model, p_core_count, e_core_count, gpu_core_count)
};
let (cpu_model, p_core_count, e_core_count, gpu_core_count) = if let (
Some(cpu_model),
Some(p_core_count),
Some(e_core_count),
Some(gpu_core_count),
) = cached_values
{
(cpu_model, p_core_count, e_core_count, gpu_core_count)
} else {
self.parse_apple_silicon_hardware_info_fast()?
};
self.ensure_cpu_refreshed();
let cpu_utilization = self.system.read().unwrap().global_cpu_usage() as f64;
let native_data = get_native_metrics_manager().and_then(|m| m.collect_once().ok());
let (base_frequency, max_frequency, p_cluster_freq, e_cluster_freq): (
u32,
u32,
Option<u32>,
Option<u32>,
) = {
if let Some(ref data) = native_data {
let avg_freq = (data.p_cluster_frequency + data.e_cluster_frequency) / 2;
(
avg_freq,
data.p_cluster_frequency, Some(data.p_cluster_frequency),
Some(data.e_cluster_frequency),
)
} else {
(
self.get_cpu_base_frequency()?,
self.get_cpu_max_frequency()?,
None,
None,
)
}
};
let per_core_utilization =
self.get_per_core_utilization_no_refresh(e_core_count as usize, p_core_count as usize);
let temperature = self.get_cpu_temperature();
let power_consumption = native_data.as_ref().map(|d| d.cpu_power_mw / 1000.0);
let total_cores = p_core_count + e_core_count;
let total_threads = total_cores;
let p_core_l2_cache_mb = self.get_p_core_l2_cache_size().ok();
let e_core_l2_cache_mb = self.get_e_core_l2_cache_size().ok();
let (p_core_utilization, e_core_utilization) = if !per_core_utilization.is_empty() {
let mut p_sum = 0.0;
let mut p_count = 0;
let mut e_sum = 0.0;
let mut e_count = 0;
for core in &per_core_utilization {
match core.core_type {
CoreType::Performance => {
p_sum += core.utilization;
p_count += 1;
}
CoreType::Efficiency => {
e_sum += core.utilization;
e_count += 1;
}
_ => {}
}
}
let p_util = if p_count > 0 {
p_sum / p_count as f64
} else {
cpu_utilization * 0.6
};
let e_util = if e_count > 0 {
e_sum / e_count as f64
} else {
cpu_utilization * 0.4
};
(p_util, e_util)
} else {
self.get_apple_silicon_core_utilization()
.unwrap_or((cpu_utilization * 0.6, cpu_utilization * 0.4))
};
let apple_silicon_info = Some(AppleSiliconCpuInfo {
p_core_count,
e_core_count,
gpu_core_count,
p_core_utilization,
e_core_utilization,
ane_ops_per_second: None, p_cluster_frequency_mhz: p_cluster_freq,
e_cluster_frequency_mhz: e_cluster_freq,
p_core_l2_cache_mb,
e_core_l2_cache_mb,
});
let per_socket_info = vec![CpuSocketInfo {
socket_id: 0,
utilization: cpu_utilization,
cores: total_cores,
threads: total_threads,
temperature,
frequency_mhz: base_frequency,
}];
Ok(CpuInfo {
host_id: hostname.clone(), hostname,
instance,
cpu_model,
architecture: "arm64".to_string(),
platform_type: CpuPlatformType::AppleSilicon,
socket_count: 1,
total_cores,
total_threads,
base_frequency_mhz: base_frequency,
max_frequency_mhz: max_frequency,
cache_size_mb: p_core_l2_cache_mb.unwrap_or(0) + e_core_l2_cache_mb.unwrap_or(0), utilization: cpu_utilization,
temperature,
power_consumption,
per_socket_info,
apple_silicon_info,
per_core_utilization,
time,
})
}
fn get_intel_mac_cpu_info(
&self,
hostname: String,
instance: String,
time: String,
) -> Result<CpuInfo, Box<dyn std::error::Error>> {
let cached_info = self.cached_intel_info.lock().unwrap().clone();
let (cpu_model, socket_count, total_cores, total_threads, base_frequency, cache_size) =
if let Some(info) = cached_info {
info
} else {
let output = Command::new("system_profiler")
.arg("SPHardwareDataType")
.output()?;
let hardware_info = String::from_utf8_lossy(&output.stdout);
self.parse_intel_mac_hardware_info(&hardware_info)?
};
let cpu_utilization = self.get_cpu_utilization_sysinfo()?;
let temperature = self.get_cpu_temperature();
let power_consumption = None;
let mut per_socket_info = Vec::new();
for socket_id in 0..socket_count {
per_socket_info.push(CpuSocketInfo {
socket_id,
utilization: cpu_utilization,
cores: total_cores / socket_count,
threads: total_threads / socket_count,
temperature,
frequency_mhz: base_frequency,
});
}
Ok(CpuInfo {
host_id: hostname.clone(), hostname,
instance,
cpu_model,
architecture: "x86_64".to_string(),
platform_type: CpuPlatformType::Intel,
socket_count,
total_cores,
total_threads,
base_frequency_mhz: base_frequency,
max_frequency_mhz: base_frequency, cache_size_mb: cache_size,
utilization: cpu_utilization,
temperature,
power_consumption,
per_socket_info,
apple_silicon_info: None,
per_core_utilization: Vec::new(), time,
})
}
fn parse_apple_silicon_hardware_info_fast(
&self,
) -> Result<(String, u32, u32, u32), Box<dyn std::error::Error>> {
if let (Some(cpu_model), Some(p_core_count), Some(e_core_count), Some(gpu_core_count)) = (
self.cached_cpu_model.lock().unwrap().clone(),
*self.cached_p_core_count.lock().unwrap(),
*self.cached_e_core_count.lock().unwrap(),
*self.cached_gpu_core_count.lock().unwrap(),
) {
return Ok((cpu_model, p_core_count, e_core_count, gpu_core_count));
}
let cpu_model = self.get_cpu_model_sysctl()?;
let output = Command::new("sysctl")
.args(["hw.perflevel0.physicalcpu", "hw.perflevel1.physicalcpu"])
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
let mut p_core_count = 0u32;
let mut e_core_count = 0u32;
for line in output_str.lines() {
if line.starts_with("hw.perflevel0.physicalcpu:") {
if let Some(value) = line.split(':').nth(1) {
p_core_count = value.trim().parse().unwrap_or(0);
}
} else if line.starts_with("hw.perflevel1.physicalcpu:") {
if let Some(value) = line.split(':').nth(1) {
e_core_count = value.trim().parse().unwrap_or(0);
}
}
}
let gpu_core_count = self.get_gpu_core_count_ioreg().unwrap_or(0);
if p_core_count == 0 || e_core_count == 0 {
return Err("Failed to get core counts".into());
}
*self.cached_cpu_model.lock().unwrap() = Some(cpu_model.clone());
*self.cached_p_core_count.lock().unwrap() = Some(p_core_count);
*self.cached_e_core_count.lock().unwrap() = Some(e_core_count);
*self.cached_gpu_core_count.lock().unwrap() = Some(gpu_core_count);
Ok((cpu_model, p_core_count, e_core_count, gpu_core_count))
}
fn get_cpu_model_sysctl(&self) -> Result<String, Box<dyn std::error::Error>> {
let output = Command::new("sysctl")
.args(["-n", "machdep.cpu.brand_string"])
.output()?;
let cpu_brand = String::from_utf8_lossy(&output.stdout).trim().to_string();
if cpu_brand.starts_with("Apple M") {
Ok(cpu_brand)
} else if cpu_brand.is_empty() {
Err("Failed to get CPU model from sysctl".into())
} else {
Ok(cpu_brand)
}
}
fn get_gpu_core_count_ioreg(&self) -> Result<u32, Box<dyn std::error::Error>> {
let output = Command::new("ioreg")
.args(["-rc", "AGXAccelerator", "-d1"])
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.contains("\"gpu-core-count\"") {
let parts: Vec<&str> = line.split('=').collect();
if parts.len() >= 2 {
if let Ok(count) = parts[1].trim().parse::<u32>() {
return Ok(count);
}
}
}
}
self.estimate_gpu_cores_from_model()
}
fn estimate_gpu_cores_from_model(&self) -> Result<u32, Box<dyn std::error::Error>> {
if let Some(cpu_model) = self.cached_cpu_model.lock().unwrap().clone() {
let model = cpu_model.as_str();
let core_count = match model {
s if s.contains("M1 ")
&& !s.contains("Pro")
&& !s.contains("Max")
&& !s.contains("Ultra") =>
{
8
}
s if s.contains("M1 Pro") => 16,
s if s.contains("M1 Max") => 32,
s if s.contains("M1 Ultra") => 64,
s if s.contains("M2 ")
&& !s.contains("Pro")
&& !s.contains("Max")
&& !s.contains("Ultra") =>
{
10
}
s if s.contains("M2 Pro") => 19,
s if s.contains("M2 Max") => 38,
s if s.contains("M2 Ultra") => 76,
s if s.contains("M3 ") && !s.contains("Pro") && !s.contains("Max") => 10,
s if s.contains("M3 Pro") => 18,
s if s.contains("M3 Max") => 40,
s if s.contains("M4 ") && !s.contains("Pro") && !s.contains("Max") => 10,
s if s.contains("M4 Pro") => 20,
s if s.contains("M4 Max") => 40,
_ => 8, };
return Ok(core_count);
}
Err("Cannot estimate GPU cores without CPU model".into())
}
#[allow(dead_code)]
fn get_p_core_count(&self) -> Result<u32, Box<dyn std::error::Error>> {
let output = Command::new("sysctl")
.arg("hw.perflevel0.physicalcpu")
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
if let Some(value_str) = output_str.split(':').nth(1) {
let count = value_str.trim().parse::<u32>()?;
Ok(count)
} else {
Err("Failed to parse P-core count".into())
}
}
#[allow(dead_code)]
fn get_e_core_count(&self) -> Result<u32, Box<dyn std::error::Error>> {
let output = Command::new("sysctl")
.arg("hw.perflevel1.physicalcpu")
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
if let Some(value_str) = output_str.split(':').nth(1) {
let count = value_str.trim().parse::<u32>()?;
Ok(count)
} else {
Err("Failed to parse E-core count".into())
}
}
#[allow(dead_code)] fn get_gpu_core_count(&self) -> Result<u32, Box<dyn std::error::Error>> {
let output = Command::new("system_profiler")
.arg("SPDisplaysDataType")
.arg("-json")
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.contains("sppci_cores") {
if let Some(value_part) = line.split(':').nth(1) {
if let Some(start_quote) = value_part.find('"') {
if let Some(end_quote) = value_part[start_quote + 1..].find('"') {
let core_str =
&value_part[start_quote + 1..start_quote + 1 + end_quote];
if let Ok(count) = core_str.parse::<u32>() {
return Ok(count);
}
}
}
}
}
}
Err("Failed to parse GPU core count".into())
}
fn get_p_core_l2_cache_size(&self) -> Result<u32, Box<dyn std::error::Error>> {
if let Some(cached) = *self.cached_p_core_l2_cache_mb.lock().unwrap() {
return Ok(cached);
}
let output = Command::new("sysctl")
.arg("hw.perflevel0.l2cachesize")
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
if let Some(value_str) = output_str.split(':').nth(1) {
let cache_bytes = value_str.trim().parse::<u64>()?;
let cache_mb = (cache_bytes / 1024 / 1024) as u32;
*self.cached_p_core_l2_cache_mb.lock().unwrap() = Some(cache_mb);
Ok(cache_mb)
} else {
Err("Failed to parse P-core L2 cache size".into())
}
}
fn get_e_core_l2_cache_size(&self) -> Result<u32, Box<dyn std::error::Error>> {
if let Some(cached) = *self.cached_e_core_l2_cache_mb.lock().unwrap() {
return Ok(cached);
}
let output = Command::new("sysctl")
.arg("hw.perflevel1.l2cachesize")
.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
if let Some(value_str) = output_str.split(':').nth(1) {
let cache_bytes = value_str.trim().parse::<u64>()?;
let cache_mb = (cache_bytes / 1024 / 1024) as u32;
*self.cached_e_core_l2_cache_mb.lock().unwrap() = Some(cache_mb);
Ok(cache_mb)
} else {
Err("Failed to parse E-core L2 cache size".into())
}
}
fn parse_intel_mac_hardware_info(&self, hardware_info: &str) -> CpuHardwareParseResult {
if let Some(cached_info) = self.cached_intel_info.lock().unwrap().clone() {
return Ok(cached_info);
}
let mut cpu_model = String::new();
let mut socket_count = 1u32;
let mut total_cores = 0u32;
let mut total_threads = 0u32;
let mut base_frequency = 0u32;
let mut cache_size = 0u32;
for line in hardware_info.lines() {
let line = line.trim();
if line.starts_with("Processor Name:") {
cpu_model = line.split(':').nth(1).unwrap_or("").trim().to_string();
} else if line.starts_with("Processor Speed:") {
if let Some(ghz) = crate::parse_colon_value!(line, f64) {
base_frequency = (ghz * 1000.0) as u32;
}
} else if line.starts_with("Number of Processors:") {
if let Some(procs) = crate::parse_colon_value!(line, u32) {
socket_count = procs;
}
} else if line.starts_with("Total Number of Cores:") {
if let Some(cores) = crate::parse_colon_value!(line, u32) {
total_cores = cores;
total_threads = cores * 2; }
} else if line.starts_with("L3 Cache:") {
if let Some(size) = crate::parse_colon_value!(line, u32) {
cache_size = size;
}
}
}
let result = (
cpu_model,
socket_count,
total_cores,
total_threads,
base_frequency,
cache_size,
);
*self.cached_intel_info.lock().unwrap() = Some(result.clone());
Ok(result)
}
fn get_cpu_utilization_sysinfo(&self) -> Result<f64, Box<dyn std::error::Error>> {
if !*self.first_refresh_done.read().unwrap() {
self.system.write().unwrap().refresh_cpu_usage();
std::thread::sleep(std::time::Duration::from_millis(100));
*self.first_refresh_done.write().unwrap() = true;
}
self.system.write().unwrap().refresh_cpu_usage();
let cpu_usage = self.system.read().unwrap().global_cpu_usage() as f64;
Ok(cpu_usage)
}
fn ensure_cpu_refreshed(&self) {
if !*self.first_refresh_done.read().unwrap() {
self.system.write().unwrap().refresh_cpu_usage();
std::thread::sleep(std::time::Duration::from_millis(100));
*self.first_refresh_done.write().unwrap() = true;
}
self.system.write().unwrap().refresh_cpu_usage();
}
#[allow(dead_code)] fn get_cpu_utilization_iostat(&self) -> Result<f64, Box<dyn std::error::Error>> {
let output = Command::new("iostat").args(["-c", "1"]).output()?;
let iostat_output = String::from_utf8_lossy(&output.stdout);
for line in iostat_output.lines() {
if line.contains("avg-cpu") {
continue;
}
if line
.trim()
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit())
{
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() >= 6 {
let idle = fields[5].parse::<f64>().unwrap_or(0.0);
return Ok(100.0 - idle);
}
}
}
Ok(0.0)
}
fn get_apple_silicon_core_utilization(&self) -> Result<(f64, f64), Box<dyn std::error::Error>> {
let total_cpu_util = self.system.read().unwrap().global_cpu_usage() as f64;
if let Some(manager) = get_native_metrics_manager() {
if let Ok(data) = manager.collect_once() {
let p_residency = data.p_cluster_active_residency.clamp(0.0, 100.0);
let e_residency = data.e_cluster_active_residency.clamp(0.0, 100.0);
let total_residency = p_residency + e_residency;
if total_residency > 0.0 {
let p_ratio = p_residency / total_residency;
let e_ratio = e_residency / total_residency;
return Ok((
(total_cpu_util * p_ratio * 1.2).clamp(0.0, 100.0),
(total_cpu_util * e_ratio * 0.8).clamp(0.0, 100.0),
));
}
}
}
Ok((total_cpu_util * 0.6, total_cpu_util * 0.4))
}
fn get_cpu_base_frequency(&self) -> Result<u32, Box<dyn std::error::Error>> {
if self.is_apple_silicon {
Ok(3000) } else {
Ok(2400) }
}
fn get_cpu_max_frequency(&self) -> Result<u32, Box<dyn std::error::Error>> {
if self.is_apple_silicon {
Ok(3500) } else {
Ok(3000) }
}
fn get_cpu_temperature(&self) -> Option<u32> {
None
}
#[allow(dead_code)] fn get_cpu_power_consumption(&self) -> Option<f64> {
if let Some(manager) = get_native_metrics_manager() {
if let Ok(data) = manager.collect_once() {
return Some(data.cpu_power_mw / 1000.0); }
}
None
}
#[allow(dead_code)] fn get_per_core_utilization(
&self,
e_core_count: usize,
p_core_count: usize,
) -> Vec<CoreUtilization> {
self.system.write().unwrap().refresh_cpu_usage();
self.get_per_core_utilization_no_refresh(e_core_count, p_core_count)
}
fn get_per_core_utilization_no_refresh(
&self,
e_core_count: usize,
p_core_count: usize,
) -> Vec<CoreUtilization> {
let mut per_core_utilization = Vec::new();
let system = self.system.read().unwrap();
let cpus = system.cpus();
for (core_id, cpu) in cpus.iter().enumerate() {
let utilization = cpu.cpu_usage() as f64;
let core_type = if self.is_apple_silicon {
if core_id < e_core_count {
CoreType::Efficiency
} else if core_id < e_core_count + p_core_count {
CoreType::Performance
} else {
CoreType::Standard }
} else {
CoreType::Standard };
per_core_utilization.push(CoreUtilization {
core_id: core_id as u32,
core_type,
utilization,
});
}
per_core_utilization
}
}
impl CpuReader for MacOsCpuReader {
fn get_cpu_info(&self) -> Vec<CpuInfo> {
match self.get_cpu_info_from_system() {
Ok(cpu_info) => {
vec![cpu_info]
}
Err(e) => {
eprintln!("Error reading CPU info: {e}");
vec![]
}
}
}
}