Skip to content

Commit 1fcca0a

Browse files
authored
Merge pull request #48 from swiftraccoon/fix/m5-voltage-states-crash
Fix crash on Apple M5 Max due to renumbered voltage-states keys
2 parents 25e6f3d + 9154d23 commit 1fcca0a

File tree

4 files changed

+164
-31
lines changed

4 files changed

+164
-31
lines changed

src/app.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -366,10 +366,10 @@ impl App {
366366

367367
fn render(&mut self, f: &mut Frame) {
368368
let label_l = format!(
369-
"{} ({}E+{}P+{}GPU {}GB)",
369+
"{} ({}{}+{}{}+{}GPU {}GB)",
370370
self.soc.chip_name,
371-
self.soc.ecpu_cores,
372-
self.soc.pcpu_cores,
371+
self.soc.ecpu_cores, self.soc.ecpu_label,
372+
self.soc.pcpu_cores, self.soc.pcpu_label,
373373
self.soc.gpu_cores,
374374
self.soc.memory_gb,
375375
);
@@ -391,8 +391,10 @@ impl App {
391391

392392
// 1st row
393393
let (c1, c2) = h_stack(iarea[0]);
394-
self.render_freq_block(f, c1, "E-CPU", &self.ecpu_freq);
395-
self.render_freq_block(f, c2, "P-CPU", &self.pcpu_freq);
394+
let ecpu_block_label = format!("{}-CPU", self.soc.ecpu_label);
395+
let pcpu_block_label = format!("{}-CPU", self.soc.pcpu_label);
396+
self.render_freq_block(f, c1, &ecpu_block_label, &self.ecpu_freq);
397+
self.render_freq_block(f, c2, &pcpu_block_label, &self.pcpu_freq);
396398

397399
// 2nd row
398400
let (c1, c2) = h_stack(iarea[1]);

src/debug.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ pub fn print_debug() -> WithError<()> {
4444
continue;
4545
}
4646

47-
let (volts, freqs) = get_dvfs_mhz(item, &key);
47+
let Some((volts, freqs)) = get_dvfs_mhz(item, &key) else {
48+
println!("{:>32}: (not found)", key);
49+
continue;
50+
};
4851
let volts = volts.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(" ");
4952
let freqs = freqs.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(" ");
5053
println!("{:>32}: (v) {}", key, volts);

src/metrics.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ fn init_smc() -> WithError<(SMC, Vec<String>, Vec<String>)> {
111111
// Basically in the code that can be found publicly "Tp" is used for CPU and "Tg" for GPU.
112112

113113
match name {
114-
// "Tp" – performance cores, "Te" – efficiency cores
115-
name if name.starts_with("Tp") || name.starts_with("Te") => cpu_sensors.push(name.clone()),
114+
// "Tp" – performance cores, "Te" – efficiency cores, "Ts" – super cores (M5+)
115+
name if name.starts_with("Tp") || name.starts_with("Te") || name.starts_with("Ts") => cpu_sensors.push(name.clone()),
116116
name if name.starts_with("Tg") => gpu_sensors.push(name.clone()),
117117
_ => (),
118118
}
@@ -235,13 +235,14 @@ impl Sampler {
235235

236236
for x in sample {
237237
if x.group == "CPU Stats" && x.subgroup == CPU_FREQ_CORE_SUBG {
238-
if x.channel.contains("ECPU") {
239-
ecpu_usages.push(calc_freq(x.item, &self.soc.ecpu_freqs));
238+
if x.channel.starts_with("PCPU") {
239+
pcpu_usages.push(calc_freq(x.item, &self.soc.pcpu_freqs));
240240
continue;
241241
}
242242

243-
if x.channel.contains("PCPU") {
244-
pcpu_usages.push(calc_freq(x.item, &self.soc.pcpu_freqs));
243+
// ECPU on M1-M4, MCPU on M5+ (Performance cores)
244+
if x.channel.starts_with("ECPU") || x.channel.starts_with("MCPU") {
245+
ecpu_usages.push(calc_freq(x.item, &self.soc.ecpu_freqs));
245246
continue;
246247
}
247248
}
@@ -267,6 +268,8 @@ impl Sampler {
267268
}
268269
}
269270

271+
// Filter dead/disabled cores (e.g. M5 Max MCPU0 cluster is all-DOWN)
272+
ecpu_usages.retain(|&(_, pct)| pct > 0.0);
270273
rs.ecpu_usage = calc_freq_final(&ecpu_usages, &self.soc.ecpu_freqs);
271274
rs.pcpu_usage = calc_freq_final(&pcpu_usages, &self.soc.pcpu_freqs);
272275
results.push(rs);

src/sources.rs

Lines changed: 144 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,8 @@ pub struct SocInfo {
376376
pub memory_gb: u8,
377377
pub ecpu_cores: u8,
378378
pub pcpu_cores: u8,
379+
pub ecpu_label: String, // "E" on M1-M4, "P" on M5+
380+
pub pcpu_label: String, // "P" on M1-M4, "S" on M5+
379381
pub ecpu_freqs: Vec<u32>,
380382
pub pcpu_freqs: Vec<u32>,
381383
pub gpu_cores: u8,
@@ -389,9 +391,9 @@ impl SocInfo {
389391
}
390392

391393
// dynamic voltage and frequency scaling
392-
pub fn get_dvfs_mhz(dict: CFDictionaryRef, key: &str) -> (Vec<u32>, Vec<u32>) {
394+
pub fn get_dvfs_mhz(dict: CFDictionaryRef, key: &str) -> Option<(Vec<u32>, Vec<u32>)> {
393395
unsafe {
394-
let obj = cfdict_get_val(dict, key).unwrap() as CFDataRef;
396+
let obj = cfdict_get_val(dict, key)? as CFDataRef;
395397
let obj_len = CFDataGetLength(obj);
396398
let obj_val = vec![0u8; obj_len as usize];
397399
CFDataGetBytes(obj, CFRange::init(0, obj_len), obj_val.as_ptr() as *mut u8);
@@ -404,10 +406,40 @@ pub fn get_dvfs_mhz(dict: CFDictionaryRef, key: &str) -> (Vec<u32>, Vec<u32>) {
404406
freqs[i] = u32::from_le_bytes([x[0], x[1], x[2], x[3]]);
405407
}
406408

407-
(volts, freqs)
409+
Some((volts, freqs))
408410
}
409411
}
410412

413+
// Parse acc-clusters bytes into (ecpu_key, pcpu_key) voltage-states key names.
414+
// Each 8-byte entry: byte 0 = voltage-states index, byte 1 = cluster type
415+
// (0 = efficiency/lowest tier, higher = higher perf tier).
416+
// Picks highest type as pcpu, second-highest as ecpu — handles M5 Max where
417+
// type 0 (E-core cluster) is absent and the two active tiers are 1 and 2.
418+
fn parse_acc_clusters(data: &[u8]) -> Option<(String, String)> {
419+
let mut clusters: Vec<(u8, String)> = Vec::new();
420+
for chunk in data.chunks_exact(8) {
421+
clusters.push((chunk[1], format!("voltage-states{}-sram", chunk[0])));
422+
}
423+
clusters.sort_by_key(|c| c.0);
424+
if clusters.len() < 2 { return None; }
425+
let ecpu_key = clusters[clusters.len() - 2].1.clone();
426+
let pcpu_key = clusters.last()?.1.clone();
427+
Some((ecpu_key, pcpu_key))
428+
}
429+
430+
// Read acc-clusters from pmgr dict and parse into (ecpu_key, pcpu_key).
431+
fn parse_acc_clusters_from(dict: CFDictionaryRef) -> Option<(String, String)> {
432+
let obj = cfdict_get_val(dict, "acc-clusters")? as CFDataRef;
433+
434+
let len = unsafe { CFDataGetLength(obj) } as usize;
435+
if len < 8 { return None; }
436+
437+
let mut data = vec![0u8; len];
438+
unsafe { CFDataGetBytes(obj, CFRange::init(0, len as _), data.as_mut_ptr()) };
439+
440+
parse_acc_clusters(&data)
441+
}
442+
411443
pub fn run_system_profiler() -> WithError<serde_json::Value> {
412444
// system_profiler -listDataTypes
413445
let out = std::process::Command::new("system_profiler")
@@ -423,6 +455,29 @@ fn to_mhz(vals: Vec<u32>, scale: u32) -> Vec<u32> {
423455
vals.iter().map(|x| *x / scale).collect()
424456
}
425457

458+
// Parse "proc T:P:E" (macOS 15) or "proc T:P_or_S:E:M" (macOS 26+) into (ecpu, pcpu, has_mcpu).
459+
// macOS 26 always uses 4 fields regardless of chip; the 4th field (M-cores) is >0 only on M5+.
460+
fn parse_cpu_cores(s: &str) -> (u64, u64, bool) {
461+
let fields: Vec<u64> = s
462+
.strip_prefix("proc ")
463+
.unwrap_or("")
464+
.split(':')
465+
.map(|x| x.parse().unwrap_or(0))
466+
.collect();
467+
468+
match fields.len() {
469+
4 => {
470+
// macOS 26+: "proc total:P_or_S:E:M"
471+
// M5+: E=0, M>0 → ecpu=M (Performance cores), pcpu=S (Super cores)
472+
// M1-M4: M=0, E>0 → ecpu=E (Efficiency cores), pcpu=P (Performance cores)
473+
let (e, m) = (fields[2], fields[3]);
474+
if m > 0 { (m, fields[1], true) } else { (e, fields[1], false) }
475+
}
476+
3 => (fields[2], fields[1], false), // macOS 15: "proc total:P:E"
477+
_ => (0, 0, false),
478+
}
479+
}
480+
426481
pub fn get_soc_info() -> WithError<SocInfo> {
427482
let out = run_system_profiler()?;
428483
let mut info = SocInfo::default();
@@ -443,19 +498,10 @@ pub fn get_soc_info() -> WithError<SocInfo> {
443498
.parse::<u64>()
444499
.unwrap_or(0);
445500

446-
// SPHardwareDataType.0.number_processors -> "proc x:y:z"
447-
let cpu_cores = out["SPHardwareDataType"][0]["number_processors"]
448-
.as_str()
449-
.and_then(|cores| cores.strip_prefix("proc "))
450-
.unwrap_or("")
451-
.split(':')
452-
.map(|x| x.parse::<u64>().unwrap_or(0))
453-
.collect::<Vec<_>>();
454-
let (ecpu_cores, pcpu_cores) = if cpu_cores.len() == 3 {
455-
(cpu_cores[2], cpu_cores[1])
456-
} else {
457-
(0, 0) // Fallback in case of invalid data
458-
};
501+
// SPHardwareDataType.0.number_processors -> "proc x:y:z" or "proc x:y:z:w"
502+
let number_processors =
503+
out["SPHardwareDataType"][0]["number_processors"].as_str().unwrap_or("");
504+
let (ecpu_cores, pcpu_cores, has_mcpu) = parse_cpu_cores(number_processors);
459505

460506
// SPDisplaysDataType.0.sppci_cores
461507
let gpu_cores =
@@ -473,6 +519,8 @@ pub fn get_soc_info() -> WithError<SocInfo> {
473519
info.gpu_cores = gpu_cores as u8;
474520
info.ecpu_cores = ecpu_cores as u8;
475521
info.pcpu_cores = pcpu_cores as u8;
522+
info.ecpu_label = if has_mcpu { "P".into() } else { "E".into() };
523+
info.pcpu_label = if has_mcpu { "S".into() } else { "P".into() };
476524

477525
// CPU frequencies
478526
for (entry, name) in IOServiceIterator::new("AppleARMIODevice")? {
@@ -481,9 +529,24 @@ pub fn get_soc_info() -> WithError<SocInfo> {
481529
// 1) `strings /usr/bin/powermetrics | grep voltage-states` uses non-sram keys
482530
// but their values are zero, so sram used here; it looks valid.
483531
// 2) sudo powermetrics --samplers cpu_power -i 1000 -n 1 | grep "active residency" | grep "Cluster"
484-
info.ecpu_freqs = to_mhz(get_dvfs_mhz(item, "voltage-states1-sram").1, cpu_scale);
485-
info.pcpu_freqs = to_mhz(get_dvfs_mhz(item, "voltage-states5-sram").1, cpu_scale);
486-
info.gpu_freqs = to_mhz(get_dvfs_mhz(item, "voltage-states9").1, gpu_scale);
532+
// Try legacy keys first (M1-M4), fall back to acc-clusters discovery (M5+)
533+
if let Some((_, freqs)) = get_dvfs_mhz(item, "voltage-states1-sram") {
534+
info.ecpu_freqs = to_mhz(freqs, cpu_scale);
535+
} else if let Some((ecpu_key, _)) = parse_acc_clusters_from(item) {
536+
if let Some((_, freqs)) = get_dvfs_mhz(item, &ecpu_key) {
537+
info.ecpu_freqs = to_mhz(freqs, cpu_scale);
538+
}
539+
}
540+
if let Some((_, freqs)) = get_dvfs_mhz(item, "voltage-states5-sram") {
541+
info.pcpu_freqs = to_mhz(freqs, cpu_scale);
542+
} else if let Some((_, pcpu_key)) = parse_acc_clusters_from(item) {
543+
if let Some((_, freqs)) = get_dvfs_mhz(item, &pcpu_key) {
544+
info.pcpu_freqs = to_mhz(freqs, cpu_scale);
545+
}
546+
}
547+
if let Some((_, freqs)) = get_dvfs_mhz(item, "voltage-states9") {
548+
info.gpu_freqs = to_mhz(freqs, gpu_scale);
549+
}
487550
unsafe { CFRelease(item as _) }
488551
}
489552
}
@@ -915,3 +978,65 @@ impl Drop for SMC {
915978
}
916979
}
917980
}
981+
982+
#[cfg(test)]
983+
mod tests {
984+
use super::*;
985+
986+
#[test]
987+
fn parse_acc_clusters_m5_max() {
988+
// Real acc-clusters bytes captured from M5 Max via ioreg
989+
let data = [
990+
0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
991+
0x17, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
992+
0x05, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
993+
];
994+
let (e, p) = parse_acc_clusters(&data).unwrap();
995+
// Second-highest type (1 = Performance) as ecpu, highest (2 = Super) as pcpu
996+
assert_eq!(e, "voltage-states23-sram");
997+
assert_eq!(p, "voltage-states5-sram");
998+
}
999+
1000+
#[test]
1001+
fn parse_acc_clusters_incomplete() {
1002+
assert!(parse_acc_clusters(&[]).is_none());
1003+
// Single cluster – need both ecpu and pcpu
1004+
assert!(parse_acc_clusters(&[1, 0, 0, 0, 0, 0, 0, 0]).is_none());
1005+
}
1006+
1007+
#[test]
1008+
fn parse_cpu_cores_macos26_4field() {
1009+
// Real data captured from macOS 26 machines
1010+
// M5 Max: 18 total, 6 super, 0 efficiency, 12 performance(M-cores)
1011+
assert_eq!(parse_cpu_cores("proc 18:6:0:12"), (12, 6, true));
1012+
// M4 Max: 16 total, 12 performance, 4 efficiency, 0 M-cores
1013+
assert_eq!(parse_cpu_cores("proc 16:12:4:0"), (4, 12, false));
1014+
// M3 Air: 8 total, 4 performance, 4 efficiency, 0 M-cores
1015+
assert_eq!(parse_cpu_cores("proc 8:4:4:0"), (4, 4, false));
1016+
}
1017+
1018+
#[test]
1019+
fn parse_cpu_cores_macos15_3field() {
1020+
// Real data: M3 Air on macOS 15.6.1
1021+
assert_eq!(parse_cpu_cores("proc 8:4:4"), (4, 4, false));
1022+
}
1023+
1024+
#[test]
1025+
fn parse_cpu_cores_invalid() {
1026+
assert_eq!(parse_cpu_cores(""), (0, 0, false));
1027+
assert_eq!(parse_cpu_cores("garbage"), (0, 0, false));
1028+
assert_eq!(parse_cpu_cores("10:8:2"), (0, 0, false)); // missing "proc " prefix
1029+
assert_eq!(parse_cpu_cores("proc 8"), (0, 0, false)); // too few fields
1030+
assert_eq!(parse_cpu_cores("proc 8:4"), (0, 0, false)); // 2 fields, unsupported
1031+
assert_eq!(parse_cpu_cores("proc 24:6:0:12:6"), (0, 0, false)); // unknown future format
1032+
}
1033+
1034+
#[test]
1035+
fn to_mhz_scales() {
1036+
// M4+: KHz scale
1037+
assert_eq!(to_mhz(vec![4608000, 3000000], 1000), vec![4608, 3000]);
1038+
// M1-M3: MHz scale
1039+
assert_eq!(to_mhz(vec![3_000_000_000, 2_000_000_000], 1000 * 1000), vec![3000, 2000]);
1040+
assert_eq!(to_mhz(vec![], 1000), Vec::<u32>::new());
1041+
}
1042+
}

0 commit comments

Comments
 (0)