use std::path::PathBuf;
use crate::platform::Platform;
use crate::topology::{CacheKind, Topology};
use corescout_core::error::Result;
use corescout_mirror::entity::{keys, Entity, EntityClass};
use corescout_mirror::relation::{Relation, RelationKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Roots {
pub sys: PathBuf,
pub proc: PathBuf,
}
impl Default for Roots {
fn default() -> Self {
Roots::system()
}
}
impl Roots {
pub fn system() -> Roots {
Roots {
sys: PathBuf::from("/sys"),
proc: PathBuf::from("/proc"),
}
}
pub fn new(sys: impl Into<PathBuf>, proc: impl Into<PathBuf>) -> Roots {
Roots {
sys: sys.into(),
proc: proc.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct Substrate {
pub topology: Topology,
pub roots: Roots,
}
impl Substrate {
pub fn discover(platform: &dyn Platform) -> Result<Substrate> {
Ok(Substrate {
topology: platform.discover_topology()?,
roots: Roots::system(),
})
}
pub fn new(topology: Topology, roots: Roots) -> Substrate {
Substrate { topology, roots }
}
pub fn rediscover(&self) -> Result<Substrate> {
let sysfs = crate::platform::linux::sysfs::Sysfs::with_roots(
self.roots.sys.clone(),
self.roots.proc.clone(),
);
Ok(Substrate {
topology: sysfs.read_topology(None)?,
roots: self.roots.clone(),
})
}
pub fn structure(&self) -> (Vec<Entity>, Vec<Relation>) {
let t = &self.topology;
let mut entities = Vec::new();
let mut relations = Vec::new();
let machine = push(
&mut entities,
Entity::new(keys::machine(), EntityClass::Machine, None),
);
let mut packages: Vec<u32> = t.physical_cores.iter().map(|c| c.package_id).collect();
packages.sort_unstable();
packages.dedup();
let package_rows: Vec<(u32, u32)> = packages
.iter()
.map(|pkg| {
let row = push(
&mut entities,
Entity::new(keys::package(*pkg), EntityClass::Package, Some(*pkg)),
);
relations.push(Relation::new(machine, row, RelationKind::Contains));
(*pkg, row)
})
.collect();
let package_row = |pkg: u32| -> Option<u32> {
package_rows
.iter()
.find(|(p, _)| *p == pkg)
.map(|(_, r)| *r)
};
let numa_rows: Vec<(u32, u32)> = t
.numa_nodes
.iter()
.map(|node| {
let row = push(
&mut entities,
Entity::new(
keys::numa_node(node.id),
EntityClass::NumaNode,
Some(node.id),
),
);
relations.push(Relation::new(machine, row, RelationKind::Contains));
(node.id, row)
})
.collect();
let mut core_rows: Vec<(u32, u32)> = Vec::new();
for core in &t.physical_cores {
let row = push(
&mut entities,
Entity::new(
keys::physical_core(core.package_id, core.core_id),
EntityClass::PhysicalCore,
Some(core.core_id),
),
);
if let Some(pkg) = package_row(core.package_id) {
relations.push(Relation::new(pkg, row, RelationKind::Contains));
}
core_rows.push((core.id, row));
}
let core_row =
|id: u32| -> Option<u32> { core_rows.iter().find(|(c, _)| *c == id).map(|(_, r)| *r) };
let mut cpu_rows: Vec<(u32, u32)> = Vec::new();
for cpu in t.logical_cpus.iter().filter(|c| c.online) {
let row = push(
&mut entities,
Entity::new(
keys::logical_cpu(cpu.id),
EntityClass::LogicalCpu,
Some(cpu.id),
),
);
if let Some(core) = core_row(cpu.physical) {
relations.push(Relation::new(core, row, RelationKind::Contains));
}
cpu_rows.push((cpu.id, row));
}
let cpu_row =
|id: u32| -> Option<u32> { cpu_rows.iter().find(|(c, _)| *c == id).map(|(_, r)| *r) };
for cpu in t.logical_cpus.iter().filter(|c| c.online) {
let Some(from) = cpu_row(cpu.id) else {
continue;
};
for sibling in &cpu.smt_siblings {
if let Some(to) = cpu_row(*sibling) {
relations.push(Relation::new(from, to, RelationKind::SmtSibling));
}
}
}
for (node_id, node_row) in &numa_rows {
let Some(node) = t.numa_nodes.iter().find(|n| n.id == *node_id) else {
continue;
};
for cpu in &node.cpus {
if let Some(row) = cpu_row(*cpu) {
relations.push(Relation::new(*node_row, row, RelationKind::NumaLocal));
}
}
}
for cache in &t.caches {
let anchor_cpu = cache.shared_cpus.iter().copied().min();
let Some(anchor_cpu) = anchor_cpu else {
continue;
};
let anchor_core = t.core_of_cpu(anchor_cpu);
let (package_id, anchor_core_id) = match anchor_core {
Some(core) => (core.package_id, core.core_id),
None => (0, anchor_cpu),
};
let kind = cache_kind_key(cache.kind);
let row = push(
&mut entities,
Entity::new(
keys::cache(package_id, cache.level, kind, anchor_core_id),
EntityClass::Cache,
Some(cache.level as u32),
),
);
if let Some(pkg) = package_row(package_id) {
relations.push(Relation::new(pkg, row, RelationKind::Contains));
}
for cpu in &cache.shared_cpus {
if let Some(cpu_row) = cpu_row(*cpu) {
relations.push(
Relation::new(row, cpu_row, RelationKind::CacheMember)
.with(0, cache.level as f64)
.with(1, cache.size_bytes.map(|b| b as f64).unwrap_or(f64::NAN))
.with(2, cache.shared_cpus.len() as f64),
);
}
}
}
(entities, relations)
}
}
fn push(entities: &mut Vec<Entity>, entity: Entity) -> u32 {
entities.push(entity);
(entities.len() - 1) as u32
}
fn cache_kind_key(kind: CacheKind) -> &'static str {
match kind {
CacheKind::L1Data => "data",
CacheKind::L1Instruction => "instruction",
CacheKind::L2Unified | CacheKind::L3Unified => "unified",
CacheKind::Other => "other",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::fake_topology;
use corescout_mirror::relation::RelationView;
fn structure() -> (Vec<Entity>, Vec<Relation>) {
Substrate::new(
fake_topology(),
Roots::new("/nonexistent/sys", "/nonexistent/proc"),
)
.structure()
}
#[test]
fn the_machine_is_always_row_zero() {
let (entities, _) = structure();
assert_eq!(entities[0].class_hint, EntityClass::Machine);
}
#[test]
fn every_structural_component_becomes_an_entity() {
let (entities, _) = structure();
let count = |class: EntityClass| entities.iter().filter(|e| e.class_hint == class).count();
assert_eq!(count(EntityClass::Machine), 1);
assert_eq!(count(EntityClass::Package), 1);
assert_eq!(count(EntityClass::NumaNode), 1);
assert_eq!(count(EntityClass::PhysicalCore), 2);
assert_eq!(count(EntityClass::LogicalCpu), 4);
assert_eq!(count(EntityClass::Cache), 3, "two L2s and one L3");
}
#[test]
fn entity_keys_are_unique() {
let (entities, _) = structure();
let mut keys: Vec<&str> = entities.iter().map(|e| e.key.as_str()).collect();
keys.sort_unstable();
let before = keys.len();
keys.dedup();
assert_eq!(keys.len(), before, "duplicate entity key");
}
#[test]
fn identity_is_derived_from_the_key() {
let (entities, _) = structure();
for entity in &entities {
assert_eq!(entity.id, corescout_mirror::EntityId::derive(&entity.key));
}
}
#[test]
fn containment_forms_a_tree_from_the_machine() {
let (entities, relations) = structure();
let view = RelationView::new(&relations);
for row in 1..entities.len() as u32 {
let containers = view
.to(row)
.filter(|r| r.kind == RelationKind::Contains)
.count();
assert!(
containers <= 1,
"entity {} has {} containers",
entities[row as usize].key,
containers
);
}
assert!(view.from(0).count() >= 2, "the machine contains things");
}
#[test]
fn smt_siblings_are_symmetric_edges() {
let (entities, relations) = structure();
let view = RelationView::new(&relations);
let row_of = |key: &str| entities.iter().position(|e| e.key == key).unwrap() as u32;
let cpu0 = row_of("cpu/0");
let cpu2 = row_of("cpu/2");
assert!(view.connected(cpu0, cpu2, RelationKind::SmtSibling));
assert!(view.connected(cpu2, cpu0, RelationKind::SmtSibling));
}
#[test]
fn cache_membership_carries_level_and_size() {
let (entities, relations) = structure();
let view = RelationView::new(&relations);
let l3_row = entities
.iter()
.position(|e| e.key.contains("L3"))
.expect("an L3 entity") as u32;
let members: Vec<&Relation> = view
.from(l3_row)
.filter(|r| r.kind == RelationKind::CacheMember)
.collect();
assert_eq!(members.len(), 4, "the fixture L3 is shared by four CPUs");
assert_eq!(members[0].attributes[0], 3.0, "level in slot 0");
assert_eq!(
members[0].attributes[1],
(32 << 20) as f64,
"size in slot 1"
);
assert_eq!(members[0].attributes[2], 4.0, "sharer count in slot 2");
}
#[test]
fn offline_cpus_do_not_become_entities() {
let mut topology = fake_topology();
topology.logical_cpus[3].online = false;
topology.online_cpus = vec![0, 1, 2];
topology.offline_cpus = vec![3];
let (entities, _) = Substrate::new(topology, Roots::system()).structure();
assert!(!entities.iter().any(|e| e.key == "cpu/3"));
assert!(entities.iter().any(|e| e.key == "cpu/1"));
}
#[test]
fn structure_is_deterministic_across_runs() {
let (a_entities, a_relations) = structure();
let (b_entities, b_relations) = structure();
assert_eq!(a_entities, b_entities);
assert_eq!(a_relations.len(), b_relations.len());
}
}