1use std::path::PathBuf;
24
25use crate::platform::Platform;
26use crate::topology::{CacheKind, Topology};
27use corescout_core::error::Result;
28use corescout_mirror::entity::{keys, Entity, EntityClass};
29use corescout_mirror::relation::{Relation, RelationKind};
30
31#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Roots {
38 pub sys: PathBuf,
39 pub proc: PathBuf,
40}
41
42impl Default for Roots {
43 fn default() -> Self {
44 Roots::system()
45 }
46}
47
48impl Roots {
49 pub fn system() -> Roots {
50 Roots {
51 sys: PathBuf::from("/sys"),
52 proc: PathBuf::from("/proc"),
53 }
54 }
55
56 pub fn new(sys: impl Into<PathBuf>, proc: impl Into<PathBuf>) -> Roots {
57 Roots {
58 sys: sys.into(),
59 proc: proc.into(),
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct Substrate {
67 pub topology: Topology,
68 pub roots: Roots,
69}
70
71impl Substrate {
72 pub fn discover(platform: &dyn Platform) -> Result<Substrate> {
74 Ok(Substrate {
75 topology: platform.discover_topology()?,
76 roots: Roots::system(),
77 })
78 }
79
80 pub fn new(topology: Topology, roots: Roots) -> Substrate {
81 Substrate { topology, roots }
82 }
83
84 pub fn rediscover(&self) -> Result<Substrate> {
90 let sysfs = crate::platform::linux::sysfs::Sysfs::with_roots(
91 self.roots.sys.clone(),
92 self.roots.proc.clone(),
93 );
94 Ok(Substrate {
95 topology: sysfs.read_topology(None)?,
96 roots: self.roots.clone(),
97 })
98 }
99
100 pub fn structure(&self) -> (Vec<Entity>, Vec<Relation>) {
107 let t = &self.topology;
108 let mut entities = Vec::new();
109 let mut relations = Vec::new();
110
111 let machine = push(
114 &mut entities,
115 Entity::new(keys::machine(), EntityClass::Machine, None),
116 );
117
118 let mut packages: Vec<u32> = t.physical_cores.iter().map(|c| c.package_id).collect();
119 packages.sort_unstable();
120 packages.dedup();
121 let package_rows: Vec<(u32, u32)> = packages
122 .iter()
123 .map(|pkg| {
124 let row = push(
125 &mut entities,
126 Entity::new(keys::package(*pkg), EntityClass::Package, Some(*pkg)),
127 );
128 relations.push(Relation::new(machine, row, RelationKind::Contains));
129 (*pkg, row)
130 })
131 .collect();
132 let package_row = |pkg: u32| -> Option<u32> {
133 package_rows
134 .iter()
135 .find(|(p, _)| *p == pkg)
136 .map(|(_, r)| *r)
137 };
138
139 let numa_rows: Vec<(u32, u32)> = t
140 .numa_nodes
141 .iter()
142 .map(|node| {
143 let row = push(
144 &mut entities,
145 Entity::new(
146 keys::numa_node(node.id),
147 EntityClass::NumaNode,
148 Some(node.id),
149 ),
150 );
151 relations.push(Relation::new(machine, row, RelationKind::Contains));
152 (node.id, row)
153 })
154 .collect();
155
156 let mut core_rows: Vec<(u32, u32)> = Vec::new();
159 for core in &t.physical_cores {
160 let row = push(
161 &mut entities,
162 Entity::new(
163 keys::physical_core(core.package_id, core.core_id),
164 EntityClass::PhysicalCore,
165 Some(core.core_id),
166 ),
167 );
168 if let Some(pkg) = package_row(core.package_id) {
169 relations.push(Relation::new(pkg, row, RelationKind::Contains));
170 }
171 core_rows.push((core.id, row));
172 }
173 let core_row =
174 |id: u32| -> Option<u32> { core_rows.iter().find(|(c, _)| *c == id).map(|(_, r)| *r) };
175
176 let mut cpu_rows: Vec<(u32, u32)> = Vec::new();
177 for cpu in t.logical_cpus.iter().filter(|c| c.online) {
178 let row = push(
179 &mut entities,
180 Entity::new(
181 keys::logical_cpu(cpu.id),
182 EntityClass::LogicalCpu,
183 Some(cpu.id),
184 ),
185 );
186 if let Some(core) = core_row(cpu.physical) {
187 relations.push(Relation::new(core, row, RelationKind::Contains));
188 }
189 cpu_rows.push((cpu.id, row));
190 }
191 let cpu_row =
192 |id: u32| -> Option<u32> { cpu_rows.iter().find(|(c, _)| *c == id).map(|(_, r)| *r) };
193
194 for cpu in t.logical_cpus.iter().filter(|c| c.online) {
197 let Some(from) = cpu_row(cpu.id) else {
198 continue;
199 };
200 for sibling in &cpu.smt_siblings {
201 if let Some(to) = cpu_row(*sibling) {
202 relations.push(Relation::new(from, to, RelationKind::SmtSibling));
203 }
204 }
205 }
206
207 for (node_id, node_row) in &numa_rows {
209 let Some(node) = t.numa_nodes.iter().find(|n| n.id == *node_id) else {
210 continue;
211 };
212 for cpu in &node.cpus {
213 if let Some(row) = cpu_row(*cpu) {
214 relations.push(Relation::new(*node_row, row, RelationKind::NumaLocal));
215 }
216 }
217 }
218
219 for cache in &t.caches {
223 let anchor_cpu = cache.shared_cpus.iter().copied().min();
224 let Some(anchor_cpu) = anchor_cpu else {
225 continue;
226 };
227 let anchor_core = t.core_of_cpu(anchor_cpu);
228 let (package_id, anchor_core_id) = match anchor_core {
229 Some(core) => (core.package_id, core.core_id),
230 None => (0, anchor_cpu),
231 };
232 let kind = cache_kind_key(cache.kind);
233 let row = push(
234 &mut entities,
235 Entity::new(
236 keys::cache(package_id, cache.level, kind, anchor_core_id),
237 EntityClass::Cache,
238 Some(cache.level as u32),
239 ),
240 );
241 if let Some(pkg) = package_row(package_id) {
242 relations.push(Relation::new(pkg, row, RelationKind::Contains));
243 }
244 for cpu in &cache.shared_cpus {
245 if let Some(cpu_row) = cpu_row(*cpu) {
246 relations.push(
247 Relation::new(row, cpu_row, RelationKind::CacheMember)
248 .with(0, cache.level as f64)
249 .with(1, cache.size_bytes.map(|b| b as f64).unwrap_or(f64::NAN))
250 .with(2, cache.shared_cpus.len() as f64),
251 );
252 }
253 }
254 }
255
256 (entities, relations)
257 }
258}
259
260fn push(entities: &mut Vec<Entity>, entity: Entity) -> u32 {
261 entities.push(entity);
262 (entities.len() - 1) as u32
263}
264
265fn cache_kind_key(kind: CacheKind) -> &'static str {
268 match kind {
269 CacheKind::L1Data => "data",
270 CacheKind::L1Instruction => "instruction",
271 CacheKind::L2Unified | CacheKind::L3Unified => "unified",
272 CacheKind::Other => "other",
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::test_support::fake_topology;
280 use corescout_mirror::relation::RelationView;
281
282 fn structure() -> (Vec<Entity>, Vec<Relation>) {
283 Substrate::new(
284 fake_topology(),
285 Roots::new("/nonexistent/sys", "/nonexistent/proc"),
286 )
287 .structure()
288 }
289
290 #[test]
291 fn the_machine_is_always_row_zero() {
292 let (entities, _) = structure();
293 assert_eq!(entities[0].class_hint, EntityClass::Machine);
294 }
295
296 #[test]
297 fn every_structural_component_becomes_an_entity() {
298 let (entities, _) = structure();
299 let count = |class: EntityClass| entities.iter().filter(|e| e.class_hint == class).count();
300 assert_eq!(count(EntityClass::Machine), 1);
301 assert_eq!(count(EntityClass::Package), 1);
302 assert_eq!(count(EntityClass::NumaNode), 1);
303 assert_eq!(count(EntityClass::PhysicalCore), 2);
304 assert_eq!(count(EntityClass::LogicalCpu), 4);
305 assert_eq!(count(EntityClass::Cache), 3, "two L2s and one L3");
306 }
307
308 #[test]
309 fn entity_keys_are_unique() {
310 let (entities, _) = structure();
311 let mut keys: Vec<&str> = entities.iter().map(|e| e.key.as_str()).collect();
312 keys.sort_unstable();
313 let before = keys.len();
314 keys.dedup();
315 assert_eq!(keys.len(), before, "duplicate entity key");
316 }
317
318 #[test]
319 fn identity_is_derived_from_the_key() {
320 let (entities, _) = structure();
321 for entity in &entities {
322 assert_eq!(entity.id, corescout_mirror::EntityId::derive(&entity.key));
323 }
324 }
325
326 #[test]
327 fn containment_forms_a_tree_from_the_machine() {
328 let (entities, relations) = structure();
329 let view = RelationView::new(&relations);
330 for row in 1..entities.len() as u32 {
333 let containers = view
334 .to(row)
335 .filter(|r| r.kind == RelationKind::Contains)
336 .count();
337 assert!(
338 containers <= 1,
339 "entity {} has {} containers",
340 entities[row as usize].key,
341 containers
342 );
343 }
344 assert!(view.from(0).count() >= 2, "the machine contains things");
345 }
346
347 #[test]
348 fn smt_siblings_are_symmetric_edges() {
349 let (entities, relations) = structure();
350 let view = RelationView::new(&relations);
351 let row_of = |key: &str| entities.iter().position(|e| e.key == key).unwrap() as u32;
352 let cpu0 = row_of("cpu/0");
353 let cpu2 = row_of("cpu/2");
354 assert!(view.connected(cpu0, cpu2, RelationKind::SmtSibling));
355 assert!(view.connected(cpu2, cpu0, RelationKind::SmtSibling));
356 }
357
358 #[test]
359 fn cache_membership_carries_level_and_size() {
360 let (entities, relations) = structure();
361 let view = RelationView::new(&relations);
362 let l3_row = entities
363 .iter()
364 .position(|e| e.key.contains("L3"))
365 .expect("an L3 entity") as u32;
366 let members: Vec<&Relation> = view
367 .from(l3_row)
368 .filter(|r| r.kind == RelationKind::CacheMember)
369 .collect();
370 assert_eq!(members.len(), 4, "the fixture L3 is shared by four CPUs");
371 assert_eq!(members[0].attributes[0], 3.0, "level in slot 0");
372 assert_eq!(
373 members[0].attributes[1],
374 (32 << 20) as f64,
375 "size in slot 1"
376 );
377 assert_eq!(members[0].attributes[2], 4.0, "sharer count in slot 2");
378 }
379
380 #[test]
381 fn offline_cpus_do_not_become_entities() {
382 let mut topology = fake_topology();
384 topology.logical_cpus[3].online = false;
385 topology.online_cpus = vec![0, 1, 2];
386 topology.offline_cpus = vec![3];
387 let (entities, _) = Substrate::new(topology, Roots::system()).structure();
388 assert!(!entities.iter().any(|e| e.key == "cpu/3"));
389 assert!(entities.iter().any(|e| e.key == "cpu/1"));
390 }
391
392 #[test]
393 fn structure_is_deterministic_across_runs() {
394 let (a_entities, a_relations) = structure();
397 let (b_entities, b_relations) = structure();
398 assert_eq!(a_entities, b_entities);
399 assert_eq!(a_relations.len(), b_relations.len());
400 }
401}