use std::collections::BTreeMap;
use crate::CacheKind;
use crate::observation::{Observation, Source};
use crate::observed::Observed;
use crate::processor_set::ProcessorSet;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ProcessorId {
pub group: u16,
pub number: u8,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Processor {
pub id: ProcessorId,
pub online: bool,
pub capacity: u32,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum DomainKind {
Group,
Package,
Die,
Module,
Core {
simultaneous_multithreading: bool,
efficiency_class: u8,
},
Cache {
level: u8,
associativity: u8,
line_size: u16,
size_bytes: u32,
cache_type: CacheKind,
},
Memory {
memory_bytes: Observed<u64>,
},
Other {
name: String,
attributes: BTreeMap<String, AttributeValue>,
},
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum AttributeValue {
Null,
Bool(bool),
UnsignedInteger(u64),
SignedInteger(i64),
Float(f64),
String(String),
Array(Vec<AttributeValue>),
Object(BTreeMap<String, AttributeValue>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Domain {
pub kind: DomainKind,
pub processors: ProcessorSet,
pub observations: Vec<Observation>,
}
impl Domain {
#[must_use]
pub fn label_from(&self, source: Source) -> Option<u32> {
self.observations
.iter()
.find(|observation| observation.source == source)
.map(|observation| observation.label)
}
#[must_use]
pub fn observed_by(&self, source: Source) -> bool {
self.observations
.iter()
.any(|observation| observation.source == source)
}
}
#[cfg(feature = "serde")]
mod serde_impl {
use std::collections::BTreeMap;
use std::fmt;
use serde::de::{Error, MapAccess, SeqAccess, Visitor};
use serde::ser::{Error as SerError, SerializeMap};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::{AttributeValue, Domain, DomainKind};
use crate::CacheKind;
use crate::processor_set::MAX_PROCESSORS_PER_GROUP;
use crate::processor_set::ProcessorSet;
impl Serialize for AttributeValue {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
AttributeValue::Null => serializer.serialize_unit(),
AttributeValue::Bool(b) => serializer.serialize_bool(*b),
AttributeValue::UnsignedInteger(n) => serializer.serialize_u64(*n),
AttributeValue::SignedInteger(n) => serializer.serialize_i64(*n),
AttributeValue::Float(n) => serializer.serialize_f64(*n),
AttributeValue::String(s) => serializer.serialize_str(s),
AttributeValue::Array(items) => items.serialize(serializer),
AttributeValue::Object(map) => map.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for AttributeValue {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = AttributeValue;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a JSON-like value")
}
fn visit_unit<E: Error>(self) -> Result<Self::Value, E> {
Ok(AttributeValue::Null)
}
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
Ok(AttributeValue::Null)
}
fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
Ok(AttributeValue::Bool(v))
}
fn visit_i64<E: Error>(self, v: i64) -> Result<Self::Value, E> {
Ok(AttributeValue::SignedInteger(v))
}
fn visit_u64<E: Error>(self, v: u64) -> Result<Self::Value, E> {
Ok(AttributeValue::UnsignedInteger(v))
}
fn visit_f64<E: Error>(self, v: f64) -> Result<Self::Value, E> {
Ok(AttributeValue::Float(v))
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(AttributeValue::String(v.to_string()))
}
fn visit_string<E: Error>(self, v: String) -> Result<Self::Value, E> {
Ok(AttributeValue::String(v))
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut items = Vec::new();
while let Some(item) = seq.next_element()? {
items.push(item);
}
Ok(AttributeValue::Array(items))
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut object = BTreeMap::new();
while let Some((k, v)) = map.next_entry()? {
object.insert(k, v);
}
Ok(AttributeValue::Object(object))
}
}
deserializer.deserialize_any(ValueVisitor)
}
}
fn as_bool<E: Error>(value: AttributeValue) -> Result<bool, E> {
match value {
AttributeValue::Bool(b) => Ok(b),
_ => Err(E::custom("expected a boolean")),
}
}
fn as_u64<E: Error>(value: AttributeValue) -> Result<u64, E> {
match value {
AttributeValue::UnsignedInteger(n) => Ok(n),
AttributeValue::SignedInteger(n) => {
u64::try_from(n).map_err(|_| E::custom("expected a non-negative whole number"))
}
AttributeValue::Float(n) if n.fract() == 0.0 && (0.0..u64::MAX as f64).contains(&n) => {
Ok(n as u64)
}
_ => Err(E::custom("expected a non-negative whole number")),
}
}
fn as_u32<E: Error>(value: AttributeValue) -> Result<u32, E> {
u32::try_from(as_u64(value)?).map_err(|_| E::custom("number is too large for this field"))
}
fn as_i64<E: Error>(value: AttributeValue) -> Result<i64, E> {
match value {
AttributeValue::SignedInteger(n) => Ok(n),
AttributeValue::UnsignedInteger(n) => {
i64::try_from(n).map_err(|_| E::custom("expected a whole number"))
}
AttributeValue::Float(n)
if n.fract() == 0.0 && (i64::MIN as f64..i64::MAX as f64).contains(&n) =>
{
Ok(n as i64)
}
_ => Err(E::custom("expected a whole number")),
}
}
fn as_i32<E: Error>(value: AttributeValue) -> Result<i32, E> {
i32::try_from(as_i64(value)?).map_err(|_| E::custom("number is too large for this field"))
}
fn as_u16<E: Error>(value: AttributeValue) -> Result<u16, E> {
u16::try_from(as_u64(value)?).map_err(|_| E::custom("number is too large for this field"))
}
fn as_u8<E: Error>(value: AttributeValue) -> Result<u8, E> {
u8::try_from(as_u64(value)?).map_err(|_| E::custom("number is too large for this field"))
}
fn cache_kind_from_value<E: Error>(value: AttributeValue) -> Result<CacheKind, E> {
match value {
AttributeValue::String(s) => match s.as_str() {
"unified" => Ok(CacheKind::Unified),
"instruction" => Ok(CacheKind::Instruction),
"data" => Ok(CacheKind::Data),
"trace" => Ok(CacheKind::Trace),
other => Err(E::custom(format!("unrecognised cache_type \"{other}\""))),
},
AttributeValue::Object(mut map) if map.len() == 1 => match map.remove("other") {
Some(raw) => Ok(CacheKind::Other(as_i32(raw)?)),
None => Err(E::custom("cache_type object must have an \"other\" key")),
},
_ => Err(E::custom(
"cache_type must be a string or {\"other\": <number>}",
)),
}
}
fn processors_from_value<E: Error>(value: AttributeValue) -> Result<ProcessorSet, E> {
let AttributeValue::Array(items) = value else {
return Err(E::custom("domain \"processors\" must be an array"));
};
let mut set = ProcessorSet::empty();
for item in items {
let AttributeValue::Object(mut object) = item else {
return Err(E::custom(
"each processor must be an object with \"group\" and \"number\"",
));
};
let group = as_u16(take(&mut object, "group")?)?;
let number = as_u8(take(&mut object, "number")?)?;
if u32::from(number) >= MAX_PROCESSORS_PER_GROUP {
return Err(E::custom(format!(
"processor number {number} is out of range: a processor group has at most \
{MAX_PROCESSORS_PER_GROUP} processors on this platform"
)));
}
set.insert(group, number);
}
Ok(set)
}
fn take<E: Error>(
fields: &mut BTreeMap<String, AttributeValue>,
key: &str,
) -> Result<AttributeValue, E> {
fields
.remove(key)
.ok_or_else(|| E::custom(format!("domain is missing required field \"{key}\"")))
}
pub(super) const WELL_KNOWN_KIND_NAMES: &[&str] = &[
"group", "package", "die", "module", "core", "cache", "memory",
];
impl Serialize for Domain {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
let kind_name: &str = match &self.kind {
DomainKind::Group => "group",
DomainKind::Package => "package",
DomainKind::Die => "die",
DomainKind::Module => "module",
DomainKind::Core { .. } => "core",
DomainKind::Cache { .. } => "cache",
DomainKind::Memory { .. } => "memory",
DomainKind::Other { name, .. } => {
if WELL_KNOWN_KIND_NAMES.contains(&name.as_str()) {
return Err(S::Error::custom(format!(
"domain kind name \"{name}\" collides with a kind this crate names \
itself, so the document would deserialize as that kind rather than \
as `Other`"
)));
}
name.as_str()
}
};
map.serialize_entry("kind", kind_name)?;
if let Some(label) = self.label_from(crate::observation::Source::RelationshipWalk) {
map.serialize_entry("id", &label)?;
}
map.serialize_entry("processors", &self.processors)?;
match &self.kind {
DomainKind::Core {
simultaneous_multithreading,
efficiency_class,
} => {
map.serialize_entry(
"simultaneous_multithreading",
simultaneous_multithreading,
)?;
map.serialize_entry("efficiency_class", efficiency_class)?;
}
DomainKind::Cache {
level,
associativity,
line_size,
size_bytes,
cache_type,
} => {
map.serialize_entry("level", level)?;
map.serialize_entry("associativity", associativity)?;
map.serialize_entry("line_size", line_size)?;
map.serialize_entry("size_bytes", size_bytes)?;
map.serialize_entry("cache_type", cache_type)?;
}
DomainKind::Memory { memory_bytes } => {
match memory_bytes {
crate::observed::Observed::Known(bytes) => {
map.serialize_entry("memory_bytes", bytes)?;
}
crate::observed::Observed::Absent => {
map.serialize_entry("memory_bytes", &Option::<u64>::None)?;
}
crate::observed::Observed::NotObserved => {}
}
}
DomainKind::Other { attributes, .. } => {
for (key, value) in attributes {
if matches!(key.as_str(), "kind" | "id" | "processors") {
return Err(S::Error::custom(format!(
"domain attribute \"{key}\" collides with the reserved \
\"kind\"/\"id\"/\"processors\" field names"
)));
}
map.serialize_entry(key, value)?;
}
}
DomainKind::Group | DomainKind::Package | DomainKind::Die | DomainKind::Module => {}
}
map.end()
}
}
impl<'de> Deserialize<'de> for Domain {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let mut fields = BTreeMap::<String, AttributeValue>::deserialize(deserializer)?;
let kind_name = match take::<D::Error>(&mut fields, "kind")? {
AttributeValue::String(s) => s,
_ => return Err(D::Error::custom("domain \"kind\" must be a string")),
};
let _ = fields.remove("id");
let processors = processors_from_value(take::<D::Error>(&mut fields, "processors")?)?;
let observations = Vec::new();
let kind = match kind_name.as_str() {
"group" => DomainKind::Group,
"package" => DomainKind::Package,
"die" => DomainKind::Die,
"module" => DomainKind::Module,
"core" => DomainKind::Core {
simultaneous_multithreading: as_bool(take(
&mut fields,
"simultaneous_multithreading",
)?)?,
efficiency_class: as_u8(take(&mut fields, "efficiency_class")?)?,
},
"cache" => DomainKind::Cache {
level: as_u8(take(&mut fields, "level")?)?,
associativity: as_u8(take(&mut fields, "associativity")?)?,
line_size: as_u16(take(&mut fields, "line_size")?)?,
size_bytes: as_u32(take(&mut fields, "size_bytes")?)?,
cache_type: cache_kind_from_value(take(&mut fields, "cache_type")?)?,
},
"memory" => DomainKind::Memory {
memory_bytes: match fields.remove("memory_bytes") {
None => crate::observed::Observed::NotObserved,
Some(AttributeValue::Null) => crate::observed::Observed::Absent,
Some(value) => crate::observed::Observed::Known(as_u64(value)?),
},
},
other => DomainKind::Other {
name: other.to_string(),
attributes: fields,
},
};
Ok(Domain {
kind,
processors,
observations,
})
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProcessorFacts<'a> {
pub id: ProcessorId,
pub online: bool,
pub core: Option<&'a Domain>,
pub simultaneous_multithreading: Observed<bool>,
pub efficiency_class: Observed<u8>,
pub parked: Observed<bool>,
pub allocated_to_this_process: Observed<bool>,
pub memory_domain: Observed<&'a Domain>,
}
#[cfg(test)]
mod tests;