use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CacheLevel {
L1I,
L1D,
L2,
L3,
}
impl CacheLevel {
pub fn label(self) -> &'static str {
match self {
CacheLevel::L1I => "L1I",
CacheLevel::L1D => "L1D",
CacheLevel::L2 => "L2",
CacheLevel::L3 => "L3",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheNode {
pub level: CacheLevel,
pub size_bytes: u64,
pub shared_by_cores: Vec<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThreadNode {
pub id: u32,
pub apic_id: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CoreNode {
pub id: u32,
pub threads: Vec<ThreadNode>,
pub l1i_bytes: u64,
pub l1d_bytes: u64,
pub l2_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ComputeDomain {
pub id: u32,
pub cores: Vec<CoreNode>,
pub shared_caches: Vec<CacheNode>,
}
impl ComputeDomain {
pub fn thread_count(&self) -> usize {
self.cores.iter().map(|c| c.threads.len()).sum()
}
pub fn l3_bytes(&self) -> Option<u64> {
self.shared_caches
.iter()
.find(|c| c.level == CacheLevel::L3)
.map(|c| c.size_bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Package {
pub id: u32,
pub domains: Vec<ComputeDomain>,
}
impl Package {
pub fn core_count(&self) -> usize {
self.domains.iter().map(|d| d.cores.len()).sum()
}
pub fn thread_count(&self) -> usize {
self.domains.iter().map(|d| d.thread_count()).sum()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct TopologyGraph {
pub packages: Vec<Package>,
}
impl TopologyGraph {
pub fn new() -> Self {
Self::default()
}
pub fn core_count(&self) -> usize {
self.packages.iter().map(|p| p.core_count()).sum()
}
pub fn thread_count(&self) -> usize {
self.packages.iter().map(|p| p.thread_count()).sum()
}
pub fn domain_count(&self) -> usize {
self.packages.iter().map(|p| p.domains.len()).sum()
}
pub fn total_l3_bytes(&self) -> u64 {
self.packages
.iter()
.flat_map(|p| &p.domains)
.filter_map(|d| d.l3_bytes())
.sum()
}
pub fn typical_l2_bytes(&self) -> u64 {
self.packages
.first()
.and_then(|p| p.domains.first())
.and_then(|d| d.cores.first())
.map(|c| c.l2_bytes)
.unwrap_or(0)
}
pub fn typical_l1d_bytes(&self) -> u64 {
self.packages
.first()
.and_then(|p| p.domains.first())
.and_then(|d| d.cores.first())
.map(|c| c.l1d_bytes)
.unwrap_or(0)
}
pub fn fingerprint_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(self.packages.len() as u32).to_le_bytes());
for pkg in &self.packages {
out.extend_from_slice(&pkg.id.to_le_bytes());
out.extend_from_slice(&(pkg.domains.len() as u32).to_le_bytes());
for dom in &pkg.domains {
out.extend_from_slice(&dom.id.to_le_bytes());
out.extend_from_slice(&(dom.cores.len() as u32).to_le_bytes());
for core in &dom.cores {
out.extend_from_slice(&core.id.to_le_bytes());
out.extend_from_slice(&(core.threads.len() as u32).to_le_bytes());
}
}
}
out
}
pub fn cache_fingerprint_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
for pkg in &self.packages {
for dom in &pkg.domains {
if let Some(l3) = dom.l3_bytes() {
out.extend_from_slice(&l3.to_le_bytes());
}
for core in &dom.cores {
out.extend_from_slice(&core.l1i_bytes.to_le_bytes());
out.extend_from_slice(&core.l1d_bytes.to_le_bytes());
out.extend_from_slice(&core.l2_bytes.to_le_bytes());
}
}
}
out
}
pub fn summary_lines(&self) -> Vec<String> {
let mut lines = Vec::new();
lines.push(format!(
"packages={} domains(CCDs)={} cores={} threads={} L3_total={}",
self.packages.len(),
self.domain_count(),
self.core_count(),
self.thread_count(),
format_bytes(self.total_l3_bytes())
));
for pkg in &self.packages {
for dom in &pkg.domains {
lines.push(format!(
" package {} domain {} : cores={} threads={} L3={}",
pkg.id,
dom.id,
dom.cores.len(),
dom.thread_count(),
dom.l3_bytes()
.map(format_bytes)
.unwrap_or_else(|| "n/a".into())
));
}
}
lines
}
}
pub fn format_bytes(n: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if n >= GB {
format!("{:.2} GiB", n as f64 / GB as f64)
} else if n >= MB {
format!("{:.2} MiB", n as f64 / MB as f64)
} else if n >= KB {
format!("{:.2} KiB", n as f64 / KB as f64)
} else {
format!("{n} B")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph_counts() {
let g = TopologyGraph::new();
assert_eq!(g.core_count(), 0);
assert_eq!(g.total_l3_bytes(), 0);
}
}