use std::fs;
use std::path::Path;
use numaperf_core::{CpuSet, NodeId, NumaError};
use crate::node::NumaNode;
use crate::Topology;
const SYSFS_NODE_PATH: &str = "/sys/devices/system/node";
pub fn discover() -> Result<Topology, NumaError> {
let node_path = Path::new(SYSFS_NODE_PATH);
if !node_path.exists() {
return Err(NumaError::topology(std::io::Error::new(
std::io::ErrorKind::NotFound,
"NUMA sysfs not found",
)));
}
let mut nodes = Vec::new();
let entries = fs::read_dir(node_path).map_err(NumaError::topology)?;
for entry in entries {
let entry = entry.map_err(NumaError::topology)?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
if let Some(suffix) = name_str.strip_prefix("node") {
if let Ok(id) = suffix.parse::<u32>() {
let node = discover_node(&entry.path(), NodeId::new(id))?;
nodes.push(node);
}
}
}
nodes.sort_by_key(|n| n.id().as_u32());
if nodes.is_empty() {
return Err(NumaError::topology(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no NUMA nodes found",
)));
}
let nodes = read_distances(node_path, nodes)?;
Ok(Topology::from_nodes(nodes))
}
fn discover_node(node_path: &Path, id: NodeId) -> Result<NumaNode, NumaError> {
let cpulist_path = node_path.join("cpulist");
let cpus = read_cpulist(&cpulist_path)?;
let meminfo_path = node_path.join("meminfo");
let memory_bytes = read_meminfo(&meminfo_path).ok();
let mut node = NumaNode::new(id, cpus);
if let Some(mem) = memory_bytes {
node = node.with_memory(mem);
}
Ok(node)
}
fn read_cpulist(path: &Path) -> Result<CpuSet, NumaError> {
let content = fs::read_to_string(path).map_err(NumaError::topology)?;
CpuSet::parse(content.trim()).map_err(|e| {
NumaError::topology(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("failed to parse cpulist: {}", e),
))
})
}
fn read_meminfo(path: &Path) -> Result<u64, NumaError> {
let content = fs::read_to_string(path).map_err(NumaError::topology)?;
for line in content.lines() {
if line.contains("MemTotal:") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
if let Ok(kb) = parts[3].parse::<u64>() {
return Ok(kb * 1024); }
}
}
}
Err(NumaError::topology(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"MemTotal not found in meminfo",
)))
}
fn read_distances(node_path: &Path, mut nodes: Vec<NumaNode>) -> Result<Vec<NumaNode>, NumaError> {
for node in &mut nodes {
let distance_path = node_path
.join(format!("node{}", node.id().as_u32()))
.join("distance");
if let Ok(content) = fs::read_to_string(&distance_path) {
let distances: Vec<u32> = content
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if !distances.is_empty() {
node.set_distances(distances);
}
}
}
Ok(nodes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cpulist_parse() {
let set = CpuSet::parse("0-3,8-11").unwrap();
assert_eq!(set.count(), 8);
}
#[test]
#[ignore] fn test_discover_real() {
let topo = discover().expect("should discover topology");
assert!(topo.node_count() > 0);
assert!(topo.cpu_count() > 0);
}
}