use super::ioreport::{IOReport, IOReportMetrics};
use super::metrics::NativeMetricsData;
use super::smc::SMCMetrics;
use super::thermal::get_thermal_state;
use once_cell::sync::Lazy;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::Duration;
#[derive(Default)]
struct ManagerSlot {
manager: Option<Arc<NativeMetricsManager>>,
handles: usize,
}
static NATIVE_METRICS_MANAGER: Lazy<Mutex<ManagerSlot>> =
Lazy::new(|| Mutex::new(ManagerSlot::default()));
static FIRST_DATA_RECEIVED: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone)]
pub struct NativeMetricsConfig {
pub sample_interval_ms: u64,
pub sample_count: usize,
#[allow(dead_code)]
pub enable_smc: bool,
}
impl Default for NativeMetricsConfig {
fn default() -> Self {
Self {
sample_interval_ms: 100, sample_count: 4, enable_smc: true,
}
}
}
pub struct NativeMetricsManager {
config: NativeMetricsConfig,
#[allow(dead_code)]
ioreport: Mutex<Option<IOReport>>,
latest_data: RwLock<Option<NativeMetricsData>>,
last_collection_time: RwLock<Option<std::time::Instant>>,
collection_lock: Mutex<()>,
is_running: AtomicBool,
collector_handle: Mutex<Option<thread::JoinHandle<()>>>,
}
impl NativeMetricsManager {
const CACHE_DURATION_MS: u128 = 500;
pub fn new(_interval_ms: u64) -> Result<Self, Box<dyn std::error::Error>> {
let config = NativeMetricsConfig::default();
let ioreport = IOReport::new().map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
Ok(Self {
config,
ioreport: Mutex::new(Some(ioreport)),
latest_data: RwLock::new(None),
last_collection_time: RwLock::new(None),
collection_lock: Mutex::new(()),
is_running: AtomicBool::new(false),
collector_handle: Mutex::new(None),
})
}
#[allow(dead_code)]
pub fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
if self.is_running.load(Ordering::Acquire) {
return Ok(());
}
self.is_running.store(true, Ordering::Release);
let mut ioreport_guard = self.ioreport.lock().unwrap();
let ioreport = ioreport_guard.take().ok_or("IOReport already taken")?;
let config = self.config.clone();
let is_running = Arc::new(AtomicBool::new(true));
let is_running_clone = is_running.clone();
let (tx, rx) = std::sync::mpsc::channel::<NativeMetricsData>();
let handle = thread::spawn(move || {
Self::collector_loop(ioreport, config, is_running_clone, tx);
});
*self.collector_handle.lock().unwrap() = Some(handle);
let latest_data = Arc::new(RwLock::new(None::<NativeMetricsData>));
let latest_data_clone = latest_data.clone();
thread::spawn(move || {
while let Ok(data) = rx.recv() {
if let Ok(mut guard) = latest_data_clone.write() {
*guard = Some(data);
FIRST_DATA_RECEIVED.store(true, Ordering::Relaxed);
}
}
});
Ok(())
}
#[allow(dead_code)]
fn collector_loop(
mut ioreport: IOReport,
config: NativeMetricsConfig,
is_running: Arc<AtomicBool>,
tx: std::sync::mpsc::Sender<NativeMetricsData>,
) {
while is_running.load(Ordering::Relaxed) {
let mut samples: Vec<IOReportMetrics> = Vec::with_capacity(config.sample_count);
for _ in 0..config.sample_count {
if !is_running.load(Ordering::Relaxed) {
return;
}
match ioreport.get_sample(config.sample_interval_ms) {
Ok((iterator, duration_ns)) => {
let metrics = IOReportMetrics::from_sample(iterator, duration_ns);
samples.push(metrics);
}
Err(_e) => {
#[cfg(debug_assertions)]
eprintln!("IOReport sample failed: {_e}");
}
}
}
if samples.is_empty() {
thread::sleep(Duration::from_millis(config.sample_interval_ms));
continue;
}
let avg_metrics = Self::average_samples(&samples);
let smc_metrics = if config.enable_smc {
SMCMetrics::collect()
} else {
SMCMetrics::default()
};
let thermal_state = get_thermal_state();
let native_data =
NativeMetricsData::from_components(avg_metrics, smc_metrics, thermal_state);
if tx.send(native_data).is_err() {
break;
}
}
}
fn average_samples(samples: &[IOReportMetrics]) -> IOReportMetrics {
if samples.is_empty() {
return IOReportMetrics::default();
}
let count = samples.len() as f64;
let mut avg = IOReportMetrics::default();
for sample in samples {
avg.cpu_power += sample.cpu_power;
avg.gpu_power += sample.gpu_power;
avg.ane_power += sample.ane_power;
avg.dram_power += sample.dram_power;
avg.package_power += sample.package_power;
avg.s_cluster_freq += sample.s_cluster_freq;
avg.e_cluster_freq += sample.e_cluster_freq;
avg.p_cluster_freq += sample.p_cluster_freq;
avg.s_cluster_residency += sample.s_cluster_residency;
avg.e_cluster_residency += sample.e_cluster_residency;
avg.p_cluster_residency += sample.p_cluster_residency;
avg.gpu_freq += sample.gpu_freq;
avg.gpu_residency += sample.gpu_residency;
}
avg.cpu_power /= count;
avg.gpu_power /= count;
avg.ane_power /= count;
avg.dram_power /= count;
avg.package_power /= count;
avg.s_cluster_freq = (avg.s_cluster_freq as f64 / count) as u32;
avg.e_cluster_freq = (avg.e_cluster_freq as f64 / count) as u32;
avg.p_cluster_freq = (avg.p_cluster_freq as f64 / count) as u32;
avg.s_cluster_residency /= count;
avg.e_cluster_residency /= count;
avg.p_cluster_residency /= count;
avg.gpu_freq = (avg.gpu_freq as f64 / count) as u32;
avg.gpu_residency /= count;
if let Some(last) = samples.last() {
avg.s_cluster_data = last.s_cluster_data.clone();
avg.e_cluster_data = last.e_cluster_data.clone();
avg.p_cluster_data = last.p_cluster_data.clone();
}
avg
}
#[allow(dead_code)]
pub fn get_latest_data(&self) -> Result<NativeMetricsData, Box<dyn std::error::Error>> {
let guard = self.latest_data.read().map_err(|_| "Lock poisoned")?;
guard.clone().ok_or_else(|| "No data available yet".into())
}
#[allow(dead_code)]
pub fn get_latest_data_result(&self) -> Result<NativeMetricsData, Box<dyn std::error::Error>> {
self.get_latest_data()
}
#[allow(dead_code)]
pub fn has_data(&self) -> bool {
self.latest_data
.read()
.map(|guard| guard.is_some())
.unwrap_or(false)
}
pub fn collect_once(&self) -> Result<NativeMetricsData, Box<dyn std::error::Error>> {
let cache_duration_ms = Self::CACHE_DURATION_MS;
if let (Ok(time_guard), Ok(data_guard)) =
(self.last_collection_time.read(), self.latest_data.read())
&& let (Some(last_time), Some(data)) = (*time_guard, data_guard.clone())
&& last_time.elapsed().as_millis() < cache_duration_ms
{
return Ok(data);
}
let _lock = self
.collection_lock
.lock()
.map_err(|_| "Collection lock poisoned")?;
if let (Ok(time_guard), Ok(data_guard)) =
(self.last_collection_time.read(), self.latest_data.read())
&& let (Some(last_time), Some(data)) = (*time_guard, data_guard.clone())
&& last_time.elapsed().as_millis() < cache_duration_ms
{
return Ok(data);
}
let mut ioreport_guard = self.ioreport.lock().map_err(|_| "IOReport lock poisoned")?;
let ioreport = ioreport_guard.as_mut().ok_or("IOReport not initialized")?;
let avg_metrics = match ioreport.get_sample_since_last()? {
Some((iterator, duration_ns)) => IOReportMetrics::from_sample(iterator, duration_ns),
None => {
let (iterator, duration_ns) =
ioreport.get_sample(self.config.sample_interval_ms)?;
IOReportMetrics::from_sample(iterator, duration_ns)
}
};
let smc_metrics = SMCMetrics::collect();
let thermal_state = get_thermal_state();
let data = NativeMetricsData::from_components(avg_metrics, smc_metrics, thermal_state);
if let Ok(mut guard) = self.latest_data.write() {
*guard = Some(data.clone());
FIRST_DATA_RECEIVED.store(true, Ordering::Relaxed);
}
if let Ok(mut guard) = self.last_collection_time.write() {
*guard = Some(std::time::Instant::now());
}
Ok(data)
}
pub fn shutdown(&self) {
self.is_running.store(false, Ordering::Release);
if let Ok(mut guard) = self.collector_handle.lock()
&& let Some(handle) = guard.take()
{
let _ = handle.join();
}
FIRST_DATA_RECEIVED.store(false, Ordering::Relaxed);
}
}
impl Drop for NativeMetricsManager {
fn drop(&mut self) {
self.shutdown();
}
}
unsafe impl Send for NativeMetricsManager {}
unsafe impl Sync for NativeMetricsManager {}
pub fn initialize_native_metrics_manager(
interval_ms: u64,
) -> Result<(), Box<dyn std::error::Error>> {
let mut slot = NATIVE_METRICS_MANAGER.lock().map_err(|_| "Lock poisoned")?;
ensure_manager(&mut slot, interval_ms)
}
#[allow(dead_code)]
pub fn acquire_native_metrics_manager(interval_ms: u64) -> Result<(), Box<dyn std::error::Error>> {
let mut slot = NATIVE_METRICS_MANAGER.lock().map_err(|_| "Lock poisoned")?;
ensure_manager(&mut slot, interval_ms)?;
slot.handles += 1;
Ok(())
}
#[allow(dead_code)]
pub fn release_native_metrics_manager() {
let Ok(mut slot) = NATIVE_METRICS_MANAGER.lock() else {
return;
};
slot.handles = slot.handles.saturating_sub(1);
if slot.handles > 0 {
return;
}
let manager = slot.manager.take();
drop(slot);
finish_shutdown(manager);
}
fn ensure_manager(
slot: &mut ManagerSlot,
interval_ms: u64,
) -> Result<(), Box<dyn std::error::Error>> {
if slot.manager.is_some() {
return Ok(());
}
let manager = NativeMetricsManager::new(interval_ms)?;
let _ = manager.collect_once();
slot.manager = Some(Arc::new(manager));
Ok(())
}
pub fn get_native_metrics_manager() -> Option<Arc<NativeMetricsManager>> {
NATIVE_METRICS_MANAGER.lock().ok()?.manager.clone()
}
#[allow(dead_code)]
pub fn shutdown_native_metrics_manager() {
let manager = match NATIVE_METRICS_MANAGER.lock() {
Ok(mut slot) => {
slot.handles = 0;
slot.manager.take()
}
Err(_) => None,
};
finish_shutdown(manager);
}
fn finish_shutdown(manager: Option<Arc<NativeMetricsManager>>) {
if let Some(manager) = manager {
manager.shutdown();
}
FIRST_DATA_RECEIVED.store(false, Ordering::Relaxed);
}
#[allow(dead_code)]
pub fn has_native_metrics_data() -> bool {
FIRST_DATA_RECEIVED.load(Ordering::Relaxed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_defaults() {
let config = NativeMetricsConfig::default();
assert_eq!(config.sample_interval_ms, 100);
assert_eq!(config.sample_count, 4);
assert!(config.enable_smc);
}
#[test]
fn test_average_samples_empty() {
let result = NativeMetricsManager::average_samples(&[]);
assert_eq!(result.cpu_power, 0.0);
}
#[test]
fn test_average_samples() {
let samples = vec![
IOReportMetrics {
cpu_power: 2.0,
gpu_power: 1.0,
..Default::default()
},
IOReportMetrics {
cpu_power: 4.0,
gpu_power: 3.0,
..Default::default()
},
];
let avg = NativeMetricsManager::average_samples(&samples);
assert!((avg.cpu_power - 3.0).abs() < 0.01);
assert!((avg.gpu_power - 2.0).abs() < 0.01);
}
}