use ados_protocol::hwcaps::{CoreCluster, Evidence, Midr, Probed};
const CPUINFO_PATH: &str = "/proc/cpuinfo";
pub fn probe_cores() -> Probed<Vec<CoreCluster>> {
if !cfg!(target_os = "linux") {
return Probed::NotProbed;
}
match std::fs::read_to_string(CPUINFO_PATH) {
Ok(text) => parse_cpuinfo(&text),
Err(_) => Probed::absent(ados_protocol::hwcaps::AbsenceReason::NodeMissing),
}
}
fn parse_cpuinfo(text: &str) -> Probed<Vec<CoreCluster>> {
let midrs = midrs_per_processor(text);
if midrs.is_empty() {
return Probed::absent(ados_protocol::hwcaps::AbsenceReason::NodeMissing);
}
let clusters = cluster_consecutive(&midrs);
let lead_midr = clusters.first().map(|c| c.midr.0).unwrap_or(0);
Probed::present(clusters, Evidence::ProcCpuinfo { midr: lead_midr })
}
fn midrs_per_processor(text: &str) -> Vec<Midr> {
let mut out = Vec::new();
let mut implementer: Option<u32> = None;
let mut part: Option<u32> = None;
let flush = |implementer: &mut Option<u32>, part: &mut Option<u32>, out: &mut Vec<Midr>| {
if let (Some(imp), Some(prt)) = (*implementer, *part) {
out.push(combine_midr(imp, prt));
}
*implementer = None;
*part = None;
};
for line in text.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
flush(&mut implementer, &mut part, &mut out);
continue;
}
let Some((key, val)) = trimmed.split_once(':') else {
continue;
};
let key = key.trim();
let val = val.trim();
match key {
"processor" => {
flush(&mut implementer, &mut part, &mut out);
}
"CPU implementer" => implementer = parse_hex(val),
"CPU part" => part = parse_hex(val),
_ => {}
}
}
flush(&mut implementer, &mut part, &mut out);
out
}
fn cluster_consecutive(midrs: &[Midr]) -> Vec<CoreCluster> {
let mut clusters: Vec<CoreCluster> = Vec::new();
for midr in midrs {
match clusters.last_mut() {
Some(last) if last.midr.0 == midr.0 => last.count = last.count.saturating_add(1),
_ => clusters.push(CoreCluster {
cortex: midr
.cortex_name()
.map(str::to_string)
.unwrap_or_else(|| format!("{:#06x}", midr.0)),
midr: *midr,
count: 1,
}),
}
}
clusters
}
fn combine_midr(implementer: u32, part: u32) -> Midr {
Midr(((implementer & 0xFF) << 24) | ((part & 0xFFF) << 4))
}
fn parse_hex(val: &str) -> Option<u32> {
let stripped = val.strip_prefix("0x").or_else(|| val.strip_prefix("0X"));
u32::from_str_radix(stripped.unwrap_or(val), 16).ok()
}
#[cfg(test)]
mod tests {
use super::*;
const A733_CPUINFO: &str = "\
processor\t: 0
BogoMIPS\t: 48.00
Features\t: fp asimd evtstrm aes pmull sha1 sha2 crc32
CPU implementer\t: 0x41
CPU architecture: 8
CPU variant\t: 0x4
CPU part\t: 0xd0b
CPU revision\t: 0
processor\t: 1
CPU implementer\t: 0x41
CPU part\t: 0xd0b
CPU revision\t: 0
processor\t: 2
CPU implementer\t: 0x41
CPU part\t: 0xd05
CPU revision\t: 0
processor\t: 3
CPU implementer\t: 0x41
CPU part\t: 0xd05
CPU revision\t: 0
processor\t: 4
CPU implementer\t: 0x41
CPU part\t: 0xd05
CPU revision\t: 0
processor\t: 5
CPU implementer\t: 0x41
CPU part\t: 0xd05
CPU revision\t: 0
processor\t: 6
CPU implementer\t: 0x41
CPU part\t: 0xd05
CPU revision\t: 0
processor\t: 7
CPU implementer\t: 0x41
CPU part\t: 0xd05
CPU revision\t: 0
";
#[test]
fn parses_a733_two_clusters() {
let probed = parse_cpuinfo(A733_CPUINFO);
let clusters = probed.value().expect("A733 should probe present");
assert_eq!(clusters.len(), 2, "A733 is a two-cluster big.LITTLE");
assert_eq!(clusters[0].cortex, "Cortex-A76");
assert_eq!(clusters[0].count, 2);
assert_eq!(clusters[0].midr.cortex_name(), Some("Cortex-A76"));
assert_eq!(clusters[1].cortex, "Cortex-A55");
assert_eq!(clusters[1].count, 6);
assert_eq!(clusters[1].midr.cortex_name(), Some("Cortex-A55"));
}
#[test]
fn present_evidence_carries_lead_midr() {
let probed = parse_cpuinfo(A733_CPUINFO);
match probed {
Probed::Present { evidence, .. } => {
assert_eq!(evidence, Evidence::ProcCpuinfo { midr: 0x4100_d0b0 });
}
other => panic!("expected Present, got {other:?}"),
}
}
#[test]
fn fields_in_either_order_within_a_block() {
let text = "\
processor\t: 0
CPU part\t: 0xd08
CPU implementer\t: 0x41
";
let clusters = parse_cpuinfo(text).value().cloned().unwrap();
assert_eq!(clusters.len(), 1);
assert_eq!(clusters[0].cortex, "Cortex-A72");
assert_eq!(clusters[0].count, 1);
}
#[test]
fn unknown_part_falls_back_to_raw_midr_hex() {
let text = "\
processor\t: 0
CPU implementer\t: 0x41
CPU part\t: 0xfff
";
let clusters = parse_cpuinfo(text).value().cloned().unwrap();
assert_eq!(clusters.len(), 1);
assert_eq!(clusters[0].cortex, "0x4100fff0");
assert_eq!(clusters[0].midr.0, 0x4100_fff0);
}
#[test]
fn identical_non_adjacent_runs_stay_separate_clusters() {
let text = "\
processor\t: 0
CPU implementer\t: 0x41
CPU part\t: 0xd0b
processor\t: 1
CPU implementer\t: 0x41
CPU part\t: 0xd05
processor\t: 2
CPU implementer\t: 0x41
CPU part\t: 0xd0b
";
let clusters = parse_cpuinfo(text).value().cloned().unwrap();
assert_eq!(clusters.len(), 3);
assert_eq!(clusters[0].cortex, "Cortex-A76");
assert_eq!(clusters[1].cortex, "Cortex-A55");
assert_eq!(clusters[2].cortex, "Cortex-A76");
}
#[test]
fn cpuinfo_without_arm_fields_is_node_missing() {
let text = "\
processor\t: 0
vendor_id\t: GenuineIntel
model name\t: Some CPU
";
assert!(matches!(
parse_cpuinfo(text),
Probed::Absent {
reason: ados_protocol::hwcaps::AbsenceReason::NodeMissing
}
));
}
#[test]
fn part_without_0x_prefix_still_parses() {
let text = "\
processor\t: 0
CPU implementer\t: 41
CPU part\t: d03
";
let clusters = parse_cpuinfo(text).value().cloned().unwrap();
assert_eq!(clusters[0].cortex, "Cortex-A53");
}
}