use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct EntityId(pub u64);
impl EntityId {
pub fn derive(key: &str) -> EntityId {
let mut hash = 0xcbf2_9ce4_8422_2325u64;
for byte in key.as_bytes() {
hash ^= *byte as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
}
EntityId(if hash == 0 { 1 } else { hash })
}
pub fn raw(self) -> u64 {
self.0
}
pub fn short(self) -> String {
format!("e{:012x}", self.0 & 0xffff_ffff_ffff)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[repr(u16)]
pub enum EntityClass {
Machine = 1,
Package = 2,
PhysicalCore = 3,
LogicalCpu = 4,
Cache = 5,
NumaNode = 6,
ThermalZone = 7,
PowerDomain = 8,
InterruptSource = 9,
Unclassified = 0,
}
impl EntityClass {
pub fn as_u16(self) -> u16 {
self as u16
}
pub fn from_u16(value: u16) -> EntityClass {
match value {
1 => EntityClass::Machine,
2 => EntityClass::Package,
3 => EntityClass::PhysicalCore,
4 => EntityClass::LogicalCpu,
5 => EntityClass::Cache,
6 => EntityClass::NumaNode,
7 => EntityClass::ThermalZone,
8 => EntityClass::PowerDomain,
9 => EntityClass::InterruptSource,
_ => EntityClass::Unclassified,
}
}
pub fn label(self) -> &'static str {
match self {
EntityClass::Machine => "machine",
EntityClass::Package => "package",
EntityClass::PhysicalCore => "core",
EntityClass::LogicalCpu => "cpu",
EntityClass::Cache => "cache",
EntityClass::NumaNode => "numa",
EntityClass::ThermalZone => "thermal",
EntityClass::PowerDomain => "power",
EntityClass::InterruptSource => "irq",
EntityClass::Unclassified => "entity",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Entity {
pub id: EntityId,
pub key: String,
pub class_hint: EntityClass,
pub natural_index: Option<u32>,
}
impl Entity {
pub fn new(
key: impl Into<String>,
class_hint: EntityClass,
natural_index: Option<u32>,
) -> Entity {
let key = key.into();
Entity {
id: EntityId::derive(&key),
key,
class_hint,
natural_index,
}
}
}
pub mod keys {
pub fn machine() -> String {
"machine".to_string()
}
pub fn package(package_id: u32) -> String {
format!("package/{package_id}")
}
pub fn physical_core(package_id: u32, core_id: u32) -> String {
format!("core/{package_id}/{core_id}")
}
pub fn logical_cpu(cpu: u32) -> String {
format!("cpu/{cpu}")
}
pub fn cache(package_id: u32, level: u8, kind: &str, anchor_core: u32) -> String {
format!("cache/{package_id}/L{level}/{kind}/{anchor_core}")
}
pub fn numa_node(node: u32) -> String {
format!("numa/{node}")
}
pub fn thermal_zone(zone_type: &str, index: u32) -> String {
format!("thermal/{zone_type}/{index}")
}
pub fn power_domain(name: &str) -> String {
format!("power/{name}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_is_deterministic() {
assert_eq!(EntityId::derive("cpu/11"), EntityId::derive("cpu/11"));
assert_ne!(EntityId::derive("cpu/11"), EntityId::derive("cpu/12"));
}
#[test]
fn identity_never_collides_with_the_reserved_zero() {
for i in 0..10_000u32 {
assert_ne!(EntityId::derive(&format!("cpu/{i}")).raw(), 0);
}
}
#[test]
fn keys_are_distinct_across_classes() {
let ids = [
EntityId::derive(&keys::logical_cpu(0)),
EntityId::derive(&keys::physical_core(0, 0)),
EntityId::derive(&keys::package(0)),
EntityId::derive(&keys::numa_node(0)),
EntityId::derive(&keys::cache(0, 3, "unified", 0)),
EntityId::derive(&keys::machine()),
];
let mut unique: Vec<u64> = ids.iter().map(|i| i.raw()).collect();
unique.sort_unstable();
unique.dedup();
assert_eq!(unique.len(), ids.len(), "natural keys collided");
}
#[test]
fn identity_survives_state_change() {
let before = Entity::new(keys::logical_cpu(3), EntityClass::LogicalCpu, Some(3));
let after = Entity::new(keys::logical_cpu(3), EntityClass::LogicalCpu, Some(3));
assert_eq!(before.id, after.id);
}
#[test]
fn class_hint_round_trips_through_its_wire_form() {
for class in [
EntityClass::Machine,
EntityClass::Package,
EntityClass::PhysicalCore,
EntityClass::LogicalCpu,
EntityClass::Cache,
EntityClass::NumaNode,
EntityClass::ThermalZone,
EntityClass::PowerDomain,
EntityClass::InterruptSource,
EntityClass::Unclassified,
] {
assert_eq!(EntityClass::from_u16(class.as_u16()), class);
}
}
#[test]
fn unknown_class_codes_degrade_rather_than_fail() {
assert_eq!(EntityClass::from_u16(4242), EntityClass::Unclassified);
}
}