use std::fs;
use std::path::Path;
use crate::{
AffinityMask, CacheInfo, CoreKind, CpuFeatures, CpuInfo, Error, L3Domain, Lp, Result, Vendor,
};
use super::utils::{parse_range_list_str, parse_range_list_with};
pub(crate) mod features;
pub(crate) mod proc;
#[cfg(test)]
mod fixture_tests;
pub fn detect_cpu_info() -> Result<CpuInfo> {
detect_at(Path::new("/sys"), Path::new("/proc"))
}
fn read_str(path: &Path) -> Option<String> {
fs::read_to_string(path).ok().map(|s| s.trim().to_string())
}
fn read_u64(path: &Path) -> Option<u64> {
read_str(path)?.parse().ok()
}
fn parse_cache_size(s: &str) -> u64 {
if s.is_empty() {
return 0;
}
let (digits, mult) = match s.as_bytes()[s.len() - 1] {
b'K' => (&s[..s.len() - 1], 1024u64),
b'M' => (&s[..s.len() - 1], 1024 * 1024),
_ => (s, 1),
};
digits.parse::<u64>().map(|v| v * mult).unwrap_or(0)
}
pub(crate) fn detect_at(sysfs_root: &Path, procfs_root: &Path) -> Result<CpuInfo> {
let cpu_base = sysfs_root.join("devices/system/cpu");
if !cpu_base.exists() {
return Err(Error::Detection(format!(
"CPU sysfs path not found: {:?}",
cpu_base
)));
}
let online_str = read_str(&cpu_base.join("online"))
.ok_or_else(|| Error::Detection("Failed to read cpu/online".to_string()))?;
let mut online = parse_range_list_str(&online_str)?;
online.sort_unstable();
online.dedup();
if online.is_empty() {
return Err(Error::Detection("No online CPUs reported".to_string()));
}
if *online.last().unwrap() > u16::MAX as usize {
return Err(Error::Detection(format!(
"Logical processor id {} exceeds the supported range",
online.last().unwrap()
)));
}
let mut lps: Vec<Lp> = Vec::with_capacity(online.len());
let mut core_keys: Vec<u32> = Vec::new(); let mut socket_ids: Vec<u16> = Vec::new();
let mut capacities: Vec<Option<u64>> = Vec::with_capacity(online.len());
for &os_id in &online {
let topo = cpu_base.join(format!("cpu{}/topology", os_id));
let pkg = read_u64(&topo.join("physical_package_id")).unwrap_or(0) as u16;
let core_id = read_u64(&topo.join("core_id")).unwrap_or(0) as u16;
let key = (u32::from(pkg) << 16) | u32::from(core_id);
let (core_idx, smt_index) = match core_keys.iter().position(|&k| k == key) {
Some(idx) => {
let siblings = lps.iter().filter(|lp| lp.core == idx as u16).count();
(idx as u16, siblings as u8)
}
None => {
core_keys.push(key);
((core_keys.len() - 1) as u16, 0)
}
};
let socket_idx = match socket_ids.iter().position(|&s| s == pkg) {
Some(idx) => idx as u8,
None => {
socket_ids.push(pkg);
(socket_ids.len() - 1) as u8
}
};
let kind = match read_str(&topo.join("core_type")).as_deref() {
Some("performance") | Some("0") => CoreKind::Performance,
Some("efficiency") | Some("1") => CoreKind::Efficiency,
_ => CoreKind::Unknown,
};
capacities.push(read_u64(
&cpu_base.join(format!("cpu{}/cpu_capacity", os_id)),
));
lps.push(Lp {
os_id: os_id as u16,
core: core_idx,
socket: socket_idx,
l3_domain: Lp::NO_L3,
numa_node: 0,
kind,
smt_index,
perf_hint: 0,
cpu_part: 0,
});
}
let core_count = core_keys.len() as u16;
let socket_count = socket_ids.len() as u8;
let present: Vec<u64> = capacities.iter().flatten().copied().collect();
let cap_max = present.iter().copied().max().unwrap_or(0);
let cap_min = present.iter().copied().min().unwrap_or(0);
let capacity_applies = !present.is_empty() && cap_min < cap_max;
for (lp, capacity) in lps.iter_mut().zip(capacities.iter()) {
lp.perf_hint = capacity.unwrap_or(0).min(u16::MAX as u64) as u16;
if lp.kind != CoreKind::Unknown {
continue;
}
lp.kind = match capacity {
Some(cap) if capacity_applies => {
if cap * 4 >= cap_max * 3 {
CoreKind::Performance
} else if cap * 5 >= cap_max * 2 {
CoreKind::Efficiency
} else {
CoreKind::LpEfficiency
}
}
_ => CoreKind::Performance,
};
}
let mut l3_domains: Vec<L3Domain> = Vec::new();
let mut domain_first_lp: Vec<usize> = Vec::new();
for lp in lps.iter_mut() {
for index in 0..10u32 {
let idx_base = cpu_base.join(format!("cpu{}/cache/index{}", lp.os_id, index));
let level = match read_u64(&idx_base.join("level")) {
Some(l) => l,
None => break,
};
if level != 3 {
continue;
}
if read_str(&idx_base.join("type")).is_some_and(|t| t != "Unified") {
continue;
}
let shared = match read_str(&idx_base.join("shared_cpu_list")) {
Some(s) => s,
None => break,
};
let mut mask = AffinityMask::empty();
let mut first: Option<usize> = None;
if parse_range_list_with(&shared, |id| {
mask.add(id);
first = Some(first.map_or(id, |f| f.min(id)));
})
.is_err()
{
break;
}
let Some(first) = first else {
break;
};
let domain = match domain_first_lp.iter().position(|&k| k == first) {
Some(d) => d,
None => {
if l3_domains.len() >= Lp::NO_L3 as usize {
break;
}
let size = read_str(&idx_base.join("size"))
.map(|s| parse_cache_size(&s))
.unwrap_or(0);
domain_first_lp.push(first);
l3_domains.push(L3Domain {
size_bytes: size,
mask,
core_count: 0,
});
l3_domains.len() - 1
}
};
lp.l3_domain = domain as u8;
break;
}
}
for lp in &lps {
if lp.smt_index == 0 && lp.l3_domain != Lp::NO_L3 {
l3_domains[lp.l3_domain as usize].core_count += 1;
}
}
let mut l1d = [CacheInfo::default(); CoreKind::COUNT];
let mut l1i = [CacheInfo::default(); CoreKind::COUNT];
let mut l2 = [CacheInfo::default(); CoreKind::COUNT];
for lp in &lps {
let k = lp.kind.index();
if l1d[k].size_bytes != 0 && l2[k].size_bytes != 0 {
continue;
}
for index in 0..10u32 {
let idx_base = cpu_base.join(format!("cpu{}/cache/index{}", lp.os_id, index));
let level = match read_u64(&idx_base.join("level")) {
Some(l) => l,
None => break,
};
if level > 2 {
continue;
}
let ctype = read_str(&idx_base.join("type")).unwrap_or_default();
let mut ci = CacheInfo {
size_bytes: read_str(&idx_base.join("size"))
.map(|s| parse_cache_size(&s))
.unwrap_or(0),
line_bytes: read_u64(&idx_base.join("coherency_line_size")).unwrap_or(0) as u16,
shared_by: 0,
};
if let Some(shared) = read_str(&idx_base.join("shared_cpu_list")) {
let mut shared_by: u16 = 0;
if parse_range_list_with(&shared, |_| shared_by += 1).is_ok() {
ci.shared_by = shared_by;
}
}
match (level, ctype.as_str()) {
(2, _) => l2[k] = ci,
(1, "Data") => l1d[k] = ci,
(1, "Instruction") => l1i[k] = ci,
(1, "Unified") => {
l1d[k] = ci;
l1i[k] = ci;
}
_ => {}
}
}
}
let online_nodes: Vec<usize> = read_str(&sysfs_root.join("devices/system/node/online"))
.and_then(|s| parse_range_list_str(&s).ok())
.filter(|v| !v.is_empty())
.unwrap_or_else(|| (0..=u8::MAX as usize).collect());
let mut present_nodes: u32 = 0;
let mut claimed = vec![false; lps.len()];
let mut degenerate = false;
for node in online_nodes {
let cpulist = sysfs_root.join(format!("devices/system/node/node{}/cpulist", node));
let Some(list) = read_str(&cpulist) else {
continue;
};
present_nodes += 1;
let _ = parse_range_list_with(&list, |id| {
if let Some((i, lp)) = lps
.iter_mut()
.enumerate()
.find(|(_, lp)| lp.os_id as usize == id)
{
if claimed[i] {
degenerate = true;
}
claimed[i] = true;
lp.numa_node = node as u8;
}
});
}
let mut numa_node_count = present_nodes.min(u8::MAX as u32) as u8;
if numa_node_count == 0 {
numa_node_count = 1;
}
if degenerate {
numa_node_count = 1;
for lp in lps.iter_mut() {
lp.numa_node = 0;
}
}
let mut vendor = Vendor::Unknown;
let mut model_name = "Unknown".to_string();
let mut cpu_features = CpuFeatures::default();
#[cfg(target_arch = "x86_64")]
crate::platform::common_x86_64::detect_via_cpuid(
&mut vendor,
&mut model_name,
&mut cpu_features,
);
if vendor == Vendor::Unknown || model_name == "Unknown" || cpu_features.is_empty() {
proc::detect_via_proc_cpuinfo(procfs_root, &mut vendor, &mut model_name, &mut cpu_features);
}
if let Ok(content) = std::fs::read_to_string(procfs_root.join("cpuinfo")) {
for (os_id, part) in proc::parse_cpu_parts(&content) {
if let Some(lp) = lps.iter_mut().find(|lp| lp.os_id == os_id) {
lp.cpu_part = part;
}
}
}
let mut kind_core_counts = [0u16; CoreKind::COUNT];
for lp in &lps {
if lp.smt_index == 0 {
kind_core_counts[lp.kind.index()] += 1;
}
}
Ok(CpuInfo {
lps,
core_count,
socket_count,
numa_node_count,
kind_core_counts,
l3_domains,
l1d,
l1i,
l2,
vendor,
model_name,
features: cpu_features,
})
}