use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
pub use corescout_core::{LogicalId, PhysicalId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CoreType {
Performance,
Efficiency,
Unknown,
}
impl CoreType {
pub fn short(&self) -> &'static str {
match self {
CoreType::Performance => "P",
CoreType::Efficiency => "E",
CoreType::Unknown => "-",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CacheKind {
L1Data,
L1Instruction,
L2Unified,
L3Unified,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cache {
pub level: u8,
pub kind: CacheKind,
pub size_bytes: Option<u64>,
pub line_size_bytes: Option<u32>,
pub ways_of_associativity: Option<u32>,
pub shared_cpus: Vec<LogicalId>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrequencyInfo {
pub current_khz: Option<u64>,
pub min_khz: Option<u64>,
pub max_khz: Option<u64>,
pub base_khz: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FavoredHint {
pub highest_perf: u32,
pub nominal_perf: Option<u32>,
pub rank: u32,
pub source: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LogicalCpu {
pub id: LogicalId,
pub physical: PhysicalId,
pub package_id: u32,
pub core_id: u32,
pub numa_node: Option<u32>,
pub smt_siblings: Vec<LogicalId>,
pub core_type: CoreType,
pub online: bool,
pub frequency: FrequencyInfo,
pub favored: Option<FavoredHint>,
}
impl LogicalCpu {
pub fn is_smt(&self) -> bool {
!self.smt_siblings.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PhysicalCore {
pub id: PhysicalId,
pub package_id: u32,
pub core_id: u32,
pub numa_node: Option<u32>,
pub core_type: CoreType,
pub logical_cpus: Vec<LogicalId>,
}
impl PhysicalCore {
pub fn primary_cpu(&self) -> LogicalId {
self.logical_cpus[0]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NumaNode {
pub id: u32,
pub cpus: Vec<LogicalId>,
pub memory_kb: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Topology {
pub model_name: String,
pub vendor: String,
pub hybrid: bool,
pub logical_cpus: Vec<LogicalCpu>,
pub physical_cores: Vec<PhysicalCore>,
pub caches: Vec<Cache>,
pub numa_nodes: Vec<NumaNode>,
pub online_cpus: Vec<LogicalId>,
pub offline_cpus: Vec<LogicalId>,
pub process_affinity: Vec<LogicalId>,
}
impl Topology {
pub fn logical_count(&self) -> usize {
self.logical_cpus.len()
}
pub fn physical_count(&self) -> usize {
self.physical_cores.len()
}
pub fn smt_enabled(&self) -> bool {
self.physical_cores.iter().any(|c| c.logical_cpus.len() > 1)
}
pub fn smt_width(&self) -> usize {
self.physical_cores
.iter()
.map(|c| c.logical_cpus.len())
.max()
.unwrap_or(1)
}
pub fn cpu(&self, id: LogicalId) -> Option<&LogicalCpu> {
self.logical_cpus.iter().find(|c| c.id == id)
}
pub fn core(&self, id: PhysicalId) -> Option<&PhysicalCore> {
self.physical_cores.iter().find(|c| c.id == id)
}
pub fn core_of_cpu(&self, id: LogicalId) -> Option<&PhysicalCore> {
let cpu = self.cpu(id)?;
self.core(cpu.physical)
}
pub fn caches_for_cpu(&self, id: LogicalId, level: u8) -> Vec<&Cache> {
self.caches
.iter()
.filter(|c| c.level == level && c.shared_cpus.contains(&id))
.collect()
}
pub fn shares_cache(&self, a: LogicalId, b: LogicalId, level: u8) -> bool {
self.caches
.iter()
.any(|c| c.level == level && c.shared_cpus.contains(&a) && c.shared_cpus.contains(&b))
}
pub fn benchmarkable_cores(&self) -> Vec<&PhysicalCore> {
self.physical_cores
.iter()
.filter(|core| {
core.logical_cpus.iter().any(|cpu| {
self.online_cpus.contains(cpu) && self.process_affinity.contains(cpu)
})
})
.collect()
}
pub fn cores_by_type(&self) -> BTreeMap<&'static str, usize> {
let mut map = BTreeMap::new();
for core in &self.physical_cores {
let label = match core.core_type {
CoreType::Performance => "P-core",
CoreType::Efficiency => "E-core",
CoreType::Unknown => "core",
};
*map.entry(label).or_insert(0) += 1;
}
map
}
pub fn firmware_favored_cpu(&self) -> Option<&LogicalCpu> {
self.logical_cpus
.iter()
.filter(|c| c.favored.is_some())
.min_by_key(|c| c.favored.as_ref().map(|f| f.rank).unwrap_or(u32::MAX))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::fake_topology;
#[test]
fn smt_detection() {
let t = fake_topology();
assert!(t.smt_enabled());
assert_eq!(t.smt_width(), 2);
assert_eq!(t.physical_count(), 2);
assert_eq!(t.logical_count(), 4);
}
#[test]
fn cache_sharing_follows_the_hierarchy() {
let t = fake_topology();
assert!(t.shares_cache(0, 2, 2));
assert!(!t.shares_cache(0, 1, 2));
assert!(t.shares_cache(0, 1, 3));
}
#[test]
fn benchmarkable_cores_respect_affinity_and_offline() {
let mut t = fake_topology();
t.process_affinity = vec![0, 2];
t.online_cpus = vec![0, 2];
let cores = t.benchmarkable_cores();
assert_eq!(cores.len(), 1);
assert_eq!(cores[0].id, 0);
}
#[test]
fn core_lookup_by_logical_cpu() {
let t = fake_topology();
assert_eq!(t.core_of_cpu(3).unwrap().id, 1);
assert_eq!(t.core(1).unwrap().primary_cpu(), 1);
}
#[test]
fn topology_round_trips_through_json() {
let t = fake_topology();
let s = serde_json::to_string(&t).unwrap();
let back: Topology = serde_json::from_str(&s).unwrap();
assert_eq!(t, back);
}
}