#![cfg(target_os = "macos")]
use crate::collect::model::FanTick;
use std::sync::mpsc::{self, Receiver};
use std::time::Duration;
#[derive(Debug, Clone, Default)]
pub struct MacosTick {
pub gpu_power_w: Option<f32>,
pub gpu_temp_c: Option<f32>,
pub system_power_w: Option<f32>,
pub cpu_power_w: Option<f32>,
pub ane_power_w: Option<f32>,
pub fans: Vec<FanTick>,
}
pub struct MacosSampler {
rx: Receiver<MacosTick>,
latest: Option<MacosTick>,
}
struct MacosSamplerWorker {
sampler: macpow::ioreport::IOReportSampler,
smc: macpow::smc::SmcConnection,
prev_sample: Option<macpow::ioreport::Sample>,
}
const MACOS_SAMPLE_FLOOR: Duration = Duration::from_millis(250);
impl MacosSampler {
pub fn try_init(tick_ms: u64) -> Option<Self> {
let (tx, rx) = mpsc::channel();
let interval = Duration::from_millis(tick_ms).max(MACOS_SAMPLE_FLOOR);
std::thread::Builder::new()
.name("syswatch-macos-sampler".into())
.spawn(move || run_sampler_loop(tx, interval))
.ok()?;
Some(Self { rx, latest: None })
}
pub fn tick(&mut self) -> Option<MacosTick> {
while let Ok(tick) = self.rx.try_recv() {
self.latest = Some(tick);
}
self.latest.clone()
}
}
const BACKOFF_INITIAL: Duration = Duration::from_secs(1);
const BACKOFF_MAX: Duration = Duration::from_secs(30);
fn run_sampler_loop(tx: mpsc::Sender<MacosTick>, interval: Duration) {
let mut backoff = BACKOFF_INITIAL;
loop {
let tx = tx.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let Some(mut worker) = MacosSamplerWorker::try_init() else {
return Err("MacosSamplerWorker::try_init returned None".to_string());
};
loop {
if tx.send(worker.sample_tick()).is_err() {
return Ok(());
}
std::thread::sleep(interval);
}
}));
match result {
Ok(Ok(())) => return,
Ok(Err(_msg)) => {}
Err(payload) => {
let msg = panic_message(&payload);
eprintln!(
"syswatch-macos-sampler: worker panicked ({msg}); restarting in {:?}",
backoff
);
}
}
std::thread::sleep(backoff);
backoff = (backoff * 2).min(BACKOFF_MAX);
}
}
fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&'static str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"non-string panic payload".to_string()
}
}
impl MacosSamplerWorker {
fn try_init() -> Option<Self> {
let sampler = macpow::ioreport::IOReportSampler::new().ok()?;
let mut smc = macpow::smc::SmcConnection::open().ok()?;
let handle = smc.start_temp_discovery();
smc.finish_temp_discovery(handle);
Some(Self {
sampler,
smc,
prev_sample: None,
})
}
fn sample_tick(&mut self) -> MacosTick {
let mut out = MacosTick::default();
if let Ok(cur) = self.sampler.sample() {
if let Some(prev) = self.prev_sample.as_ref() {
if let Ok(power) = self.sampler.parse_power(prev, &cur) {
out.gpu_power_w = Some(power.gpu_w);
out.cpu_power_w = Some(power.cpu_w);
out.ane_power_w = Some(power.ane_w);
out.system_power_w = Some(power.total_w);
}
}
self.prev_sample = Some(cur);
}
let temps = self.smc.read_temperatures();
out.gpu_temp_c = temps
.iter()
.filter(|t| t.category == "GPU" && !t.stale)
.map(|t| t.value_celsius)
.fold(None, |acc, v| Some(acc.map_or(v, |a: f32| a.max(v))));
out.fans = self
.smc
.read_fans()
.into_iter()
.map(|f| FanTick {
name: if f.name.is_empty() {
format!("fan{}", f.id)
} else {
f.name
},
rpm: f.actual_rpm.max(0.0) as u32,
target_rpm: if f.max_rpm > 0.0 {
Some(f.max_rpm as u32)
} else {
None
},
})
.collect();
out
}
}