use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ThermalState {
Nominal,
Throttled { cpu_speed_pct: u32, sched_pct: u32 },
Unknown,
}
#[derive(Debug, Clone)]
pub struct HwSnapshot {
pub os: &'static str,
pub arch: &'static str,
pub cpu_brand: String,
pub total_cpus: usize,
pub perf_cores: usize,
pub l1d_bytes: usize,
pub l2_bytes: usize,
pub cache_line: usize,
pub thermal: ThermalState,
}
impl HwSnapshot {
pub fn collect() -> Self {
let total_cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2);
let mut snap = Self {
os: std::env::consts::OS,
arch: std::env::consts::ARCH,
cpu_brand: String::new(),
total_cpus,
perf_cores: 0,
l1d_bytes: 0,
l2_bytes: 0,
cache_line: 0,
thermal: ThermalState::Unknown,
};
#[cfg(target_os = "macos")]
{
snap.cpu_brand = sysctl_str("machdep.cpu.brand_string").unwrap_or_default();
snap.perf_cores = sysctl_usize("hw.perflevel0.physicalcpu").unwrap_or(0);
snap.l1d_bytes = sysctl_usize("hw.l1dcachesize").unwrap_or(0);
snap.l2_bytes = sysctl_usize("hw.l2cachesize").unwrap_or(0);
snap.cache_line = sysctl_usize("hw.cachelinesize").unwrap_or(0);
snap.thermal = read_pmset_thermal().unwrap_or(ThermalState::Unknown);
}
snap
}
pub fn fingerprint(&self) -> u64 {
let mut h = DefaultHasher::new();
self.os.hash(&mut h);
self.arch.hash(&mut h);
self.cpu_brand.hash(&mut h);
self.total_cpus.hash(&mut h);
self.perf_cores.hash(&mut h);
self.l1d_bytes.hash(&mut h);
self.l2_bytes.hash(&mut h);
self.cache_line.hash(&mut h);
h.finish()
}
pub fn is_throttled(&self) -> bool {
matches!(self.thermal, ThermalState::Throttled { .. })
}
}
#[cfg(target_os = "macos")]
fn sysctl_usize(name: &str) -> Option<usize> {
use std::ffi::CString;
let cname = CString::new(name).ok()?;
let mut val: u64 = 0;
let mut len = std::mem::size_of::<u64>();
unsafe extern "C" {
fn sysctlbyname(
name: *const std::os::raw::c_char,
oldp: *mut std::os::raw::c_void,
oldlenp: *mut usize,
newp: *mut std::os::raw::c_void,
newlen: usize,
) -> std::os::raw::c_int;
}
let rc = unsafe {
sysctlbyname(
cname.as_ptr(),
&mut val as *mut u64 as *mut _,
&mut len,
std::ptr::null_mut(),
0,
)
};
if rc == 0 { Some(val as usize) } else { None }
}
#[cfg(target_os = "macos")]
fn sysctl_str(name: &str) -> Option<String> {
use std::ffi::CString;
let cname = CString::new(name).ok()?;
unsafe extern "C" {
fn sysctlbyname(
name: *const std::os::raw::c_char,
oldp: *mut std::os::raw::c_void,
oldlenp: *mut usize,
newp: *mut std::os::raw::c_void,
newlen: usize,
) -> std::os::raw::c_int;
}
let mut len: usize = 0;
let rc = unsafe {
sysctlbyname(
cname.as_ptr(),
std::ptr::null_mut(),
&mut len,
std::ptr::null_mut(),
0,
)
};
if rc != 0 || len == 0 {
return None;
}
let mut buf = vec![0u8; len];
let rc = unsafe {
sysctlbyname(
cname.as_ptr(),
buf.as_mut_ptr() as *mut _,
&mut len,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
return None;
}
if let Some(&0) = buf.last() {
buf.pop();
}
String::from_utf8(buf).ok()
}
#[cfg(target_os = "macos")]
fn read_pmset_thermal() -> Option<ThermalState> {
let out = Command::new("pmset").args(["-g", "therm"]).output().ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout);
let mut cpu_speed = 100u32;
let mut sched = 100u32;
for line in s.lines() {
if let Some(rest) = line.split('=').nth(1) {
let val = rest.trim().parse::<u32>().ok();
if line.contains("CPU_Speed_Limit") {
if let Some(v) = val {
cpu_speed = v;
}
} else if line.contains("CPU_Scheduler_Limit")
&& let Some(v) = val
{
sched = v;
}
}
}
Some(if cpu_speed < 100 || sched < 100 {
ThermalState::Throttled {
cpu_speed_pct: cpu_speed,
sched_pct: sched,
}
} else {
ThermalState::Nominal
})
}
#[cfg(not(target_os = "macos"))]
#[allow(dead_code)]
fn read_pmset_thermal() -> Option<ThermalState> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshot_doesnt_panic() {
let snap = HwSnapshot::collect();
assert!(!snap.os.is_empty());
assert!(!snap.arch.is_empty());
}
#[test]
fn fingerprint_is_stable_across_collects() {
let a = HwSnapshot::collect();
let b = HwSnapshot::collect();
assert_eq!(a.fingerprint(), b.fingerprint());
}
}