windows_topology_sys/domain.rs
1// Copyright (c) 2026 Mike Grier
2//! The topology description: an open-kinded domain over a set of processors.
3//!
4//! This is the "interpretation" layer built on top of `relation.rs`'s
5//! faithful, Win32-shaped records (D-2 in `DESIGN-NOTES.md`). Everything here
6//! is plain data with no Win32 dependency, so it can be built either by
7//! discovering the running system or entirely by hand -- or fed in from
8//! elsewhere, which is this crate's whole reason for separating discovery
9//! from description.
10
11use std::collections::BTreeMap;
12
13use crate::CacheKind;
14use crate::observation::{Observation, Source};
15use crate::observed::Observed;
16use crate::processor_set::ProcessorSet;
17
18/// The identity of one logical processor: its group and its number within
19/// that group.
20///
21/// Never flattened to a single index (D-7 in `DESIGN-NOTES.md`): a Windows
22/// thread's affinity is a `GROUP_AFFINITY`, one group and a bitmask within
23/// it, so the group is a hard boundary a flattened index would lose.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct ProcessorId {
27 /// The processor group.
28 pub group: u16,
29 /// The processor's number within that group (0..`MAX_PROCESSORS_PER_GROUP`,
30 /// i.e. 0..`usize::BITS`).
31 pub number: u8,
32}
33
34/// One logical processor.
35#[derive(Clone, Debug, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct Processor {
38 /// This processor's identity.
39 pub id: ProcessorId,
40 /// Whether the processor is currently active. A processor slot can exist
41 /// -- count toward a group's maximum -- without being online, since
42 /// Windows reserves group capacity for processors that may be added
43 /// later.
44 pub online: bool,
45 /// A relative scheduling weight for this processor, higher meaning more
46 /// capable, on no fixed scale. On Windows this is the owning core's raw
47 /// `EfficiencyClass`; `0` for an offline processor or one with no known
48 /// owning core.
49 pub capacity: u32,
50}
51
52/// What a [`Domain`] represents.
53///
54/// Open-kinded rather than a fixed enumeration (D-4 in `DESIGN-NOTES.md`):
55/// Linux alone models `die`, `cluster`, and (on `s390x`) `book` and `drawer`,
56/// none of which Windows reports and none of which a cache domain reliably
57/// approximates. Enumerating every level any architecture will ever have is a
58/// losing game, so an unrecognised kind is carried in [`DomainKind::Other`]
59/// rather than rejected.
60#[derive(Clone, Debug, PartialEq)]
61#[non_exhaustive]
62pub enum DomainKind {
63 /// A Windows processor group.
64 Group,
65 /// A physical processor package (socket).
66 Package,
67 /// A processor die, on systems that report the distinction from package.
68 Die,
69 /// A processor module, on systems that report it.
70 Module,
71 /// A physical core: one or more logical processors as SMT siblings.
72 Core {
73 /// Whether this core has more than one logical processor.
74 simultaneous_multithreading: bool,
75 /// The scheduler's raw Windows efficiency class for this core.
76 efficiency_class: u8,
77 },
78 /// A cache level shared by a set of logical processors.
79 Cache {
80 /// Cache level (1, 2, 3, ...).
81 level: u8,
82 /// Associativity Windows reported, or `0xFF` for fully associative.
83 associativity: u8,
84 /// Cache line size in bytes.
85 line_size: u16,
86 /// Total cache size in bytes.
87 size_bytes: u32,
88 /// What the cache holds.
89 cache_type: CacheKind,
90 },
91 /// A memory locality domain -- a NUMA node modelled as a memory domain
92 /// that may contain no processors at all (D-5), because CXL expanders,
93 /// persistent memory, HBM tiers, and coherent GPU memory all present that
94 /// way.
95 Memory {
96 /// The domain's memory capacity.
97 ///
98 /// [`Observed::NotObserved`] from `discover`: Windows's own enumeration
99 /// (`GetLogicalProcessorInformationEx`) does not report a NUMA node's
100 /// capacity at all, so this crate never learns it. A hand-written or
101 /// fed-in description may supply one.
102 ///
103 /// # Why this is an `Observed` and not an `Option`
104 ///
105 /// So that a description **omitting** the field and one writing
106 /// `"memory_bytes": null` stop being the same value: the first is
107 /// `NotObserved` (nobody addressed it) and the second is `Absent` (the
108 /// writer addressed it and had no value). Both still mean "the capacity
109 /// is unknown" to a planner, so the distinction is one of **provenance
110 /// of the description**, not of planning -- recorded plainly because
111 /// `M5+.2` framed it as more than that.
112 ///
113 /// The distinction that actually matters was already available and is
114 /// preserved: `Known(0)` is a node with genuinely no memory -- the
115 /// CXL-expander shape [D-5](../DESIGN-NOTES.md) exists to represent --
116 /// and is not confusable with an unknown capacity. That is
117 /// [D-11](../DESIGN-NOTES.md)'s point, and it is why `Some(0)` was
118 /// rejected as a stand-in in the first place.
119 memory_bytes: Observed<u64>,
120 },
121 /// A domain kind this crate does not have a name for, carrying its raw
122 /// name and whatever attributes came with it, so a description this
123 /// crate cannot fully interpret still round-trips losslessly.
124 Other {
125 /// The raw kind name.
126 name: String,
127 /// Whatever attributes accompanied this domain, beyond `id` and
128 /// `processors`.
129 attributes: BTreeMap<String, AttributeValue>,
130 },
131}
132
133/// A plain value for an unrecognised [`DomainKind::Other`]'s attributes.
134///
135/// Deliberately not `serde_json::Value`: this crate depends only on `serde`'s
136/// traits, optionally, following `windows-file-watcher`'s D-72 precedent --
137/// never on a specific format crate. A consumer chooses their own serializer.
138#[derive(Clone, Debug, PartialEq)]
139#[non_exhaustive]
140pub enum AttributeValue {
141 /// A JSON `null`.
142 Null,
143 /// A JSON boolean.
144 Bool(bool),
145 /// A JSON number that decoded as a non-negative integer, preserved
146 /// exactly (PR #20 review response). Collapsing every number to `f64`
147 /// loses precision above 2^53 (~9 PB), silently corrupting both the
148 /// documented lossless round-trip for an unknown attribute and known
149 /// numeric field decoding (e.g. `memory_bytes`).
150 UnsignedInteger(u64),
151 /// A JSON number that decoded as a negative integer, preserved exactly.
152 SignedInteger(i64),
153 /// A JSON number with a fractional part (or one too large for either
154 /// integer variant), represented as `f64`.
155 Float(f64),
156 /// A JSON string.
157 String(String),
158 /// A JSON array.
159 Array(Vec<AttributeValue>),
160 /// A JSON object.
161 Object(BTreeMap<String, AttributeValue>),
162}
163
164/// One domain: a named relationship over a set of processors.
165///
166/// Domains reference processors; they do not nest (D-6 in `DESIGN-NOTES.md`).
167/// No hierarchy is imposed, so this type never asserts that a package
168/// contains a node or that a node contains a cache -- chiplets and CXL
169/// already violate assumptions like that, and Linux's own levels do not form
170/// a strict hierarchy either.
171///
172/// With the `serde` feature, serializes as `{"kind": <string>, "processors":
173/// [...], ...fields specific to that kind}` -- an internally tagged shape,
174/// implemented by hand rather than derived, because `kind` is open (D-4) and
175/// an unrecognised one must still round-trip its attributes.
176///
177/// An `"id"` is written when the relationship walk labelled this relation, and
178/// is **optional on read and discarded**. It carries no model meaning: a label
179/// belongs to the observation that issued it (D-15), and a file cannot
180/// establish which source observed anything (D-12).
181///
182/// **This JSON shape is explicitly not covered by this crate's semver
183/// contract (D-8 in `DESIGN-NOTES.md`).** The Rust API above (`Domain`,
184/// `DomainKind`, and friends) is covered by semver as always; the wire shape
185/// they produce and accept is allowed to change in a minor release. This is
186/// what makes D-9's deferrals (an HMAT-style attributed-relation model,
187/// devices as topology participants) safe to defer rather than merely
188/// convenient: adding them later is a schema evolution, not a breaking
189/// change to a promise this crate never made.
190#[derive(Clone, Debug, PartialEq)]
191pub struct Domain {
192 /// What this domain represents.
193 pub kind: DomainKind,
194
195 /// The logical processors this domain covers. Empty for a memory-only
196 /// domain (D-5).
197 pub processors: ProcessorSet,
198 /// Which sources reported this relation, and what each called it.
199 ///
200 /// Empty for a relation nobody reported -- one built by hand, which is
201 /// honest rather than a gap: no platform API said anything about it. A
202 /// relation both Windows APIs describe carries **two** observations, which
203 /// is what makes agreement visible without either label being discarded
204 /// (D-15, D-19).
205 pub observations: Vec<Observation>,
206}
207
208impl Domain {
209 /// What `source` called this relation, if `source` reported it.
210 ///
211 /// The replacement for the removed `id` field, and it takes a source
212 /// because there is no single answer without one: the two Windows APIs
213 /// agree on the core partition while labelling it `[0, 2, 4, ..., 14]` and
214 /// `[0, 1, ..., 7]`, so "the id" was never well defined once both were
215 /// consulted (D-15).
216 ///
217 /// A caller wanting a *stable* handle for grouping should use the
218 /// relation's position in [`MachineMemoryTopology::domains`][domains]
219 /// instead: an observation label is meaningful only to the source that
220 /// issued it, and a relation may have none at all.
221 ///
222 /// [domains]: crate::MachineMemoryTopology::domains
223 #[must_use]
224 pub fn label_from(&self, source: Source) -> Option<u32> {
225 self.observations
226 .iter()
227 .find(|observation| observation.source == source)
228 .map(|observation| observation.label)
229 }
230
231 /// Whether `source` reported this relation.
232 #[must_use]
233 pub fn observed_by(&self, source: Source) -> bool {
234 self.observations
235 .iter()
236 .any(|observation| observation.source == source)
237 }
238}
239
240/// Manual `Serialize`/`Deserialize` for the open-kinded types.
241///
242/// `AttributeValue` and `Domain` cannot be `#[derive(Serialize, Deserialize)]`:
243/// `AttributeValue` is a self-describing value (its own shape is not known
244/// until read), and `Domain`'s wire shape is internally tagged on `kind` with
245/// the kind-specific fields flattened alongside `id`/`processors` -- and an
246/// unrecognised `kind` must still capture whatever fields came with it (D-4).
247/// Both are read by buffering the whole object into a
248/// `BTreeMap<String, AttributeValue>` first, since which fields are expected
249/// is only known once `kind` itself has been read, and a hand-written or
250/// fed-in description cannot be relied on to place `kind` first.
251#[cfg(feature = "serde")]
252mod serde_impl {
253 use std::collections::BTreeMap;
254 use std::fmt;
255
256 use serde::de::{Error, MapAccess, SeqAccess, Visitor};
257 use serde::ser::{Error as SerError, SerializeMap};
258 use serde::{Deserialize, Deserializer, Serialize, Serializer};
259
260 use super::{AttributeValue, Domain, DomainKind};
261 use crate::CacheKind;
262 use crate::processor_set::MAX_PROCESSORS_PER_GROUP;
263 use crate::processor_set::ProcessorSet;
264
265 impl Serialize for AttributeValue {
266 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
267 match self {
268 AttributeValue::Null => serializer.serialize_unit(),
269 AttributeValue::Bool(b) => serializer.serialize_bool(*b),
270 AttributeValue::UnsignedInteger(n) => serializer.serialize_u64(*n),
271 AttributeValue::SignedInteger(n) => serializer.serialize_i64(*n),
272 AttributeValue::Float(n) => serializer.serialize_f64(*n),
273 AttributeValue::String(s) => serializer.serialize_str(s),
274 AttributeValue::Array(items) => items.serialize(serializer),
275 AttributeValue::Object(map) => map.serialize(serializer),
276 }
277 }
278 }
279
280 impl<'de> Deserialize<'de> for AttributeValue {
281 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
282 struct ValueVisitor;
283
284 impl<'de> Visitor<'de> for ValueVisitor {
285 type Value = AttributeValue;
286
287 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 f.write_str("a JSON-like value")
289 }
290 fn visit_unit<E: Error>(self) -> Result<Self::Value, E> {
291 Ok(AttributeValue::Null)
292 }
293 fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
294 Ok(AttributeValue::Null)
295 }
296 fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
297 Ok(AttributeValue::Bool(v))
298 }
299 fn visit_i64<E: Error>(self, v: i64) -> Result<Self::Value, E> {
300 Ok(AttributeValue::SignedInteger(v))
301 }
302 fn visit_u64<E: Error>(self, v: u64) -> Result<Self::Value, E> {
303 Ok(AttributeValue::UnsignedInteger(v))
304 }
305 fn visit_f64<E: Error>(self, v: f64) -> Result<Self::Value, E> {
306 Ok(AttributeValue::Float(v))
307 }
308 fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
309 Ok(AttributeValue::String(v.to_string()))
310 }
311 fn visit_string<E: Error>(self, v: String) -> Result<Self::Value, E> {
312 Ok(AttributeValue::String(v))
313 }
314 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
315 let mut items = Vec::new();
316 while let Some(item) = seq.next_element()? {
317 items.push(item);
318 }
319 Ok(AttributeValue::Array(items))
320 }
321 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
322 let mut object = BTreeMap::new();
323 while let Some((k, v)) = map.next_entry()? {
324 object.insert(k, v);
325 }
326 Ok(AttributeValue::Object(object))
327 }
328 }
329
330 deserializer.deserialize_any(ValueVisitor)
331 }
332 }
333
334 fn as_bool<E: Error>(value: AttributeValue) -> Result<bool, E> {
335 match value {
336 AttributeValue::Bool(b) => Ok(b),
337 _ => Err(E::custom("expected a boolean")),
338 }
339 }
340
341 /// A number is preserved exactly when it decoded as an integer variant;
342 /// only a [`AttributeValue::Float`] (a fractional source, or one too
343 /// large for `i64`/`u64`) is converted, and even then only when it is a
344 /// non-negative whole number representable in `f64` (PR #20 review
345 /// response: the same ~9 PB ceiling D-11 already accepts for
346 /// `memory_bytes`, not the silent precision loss the old
347 /// `f64`-for-everything encoding had).
348 fn as_u64<E: Error>(value: AttributeValue) -> Result<u64, E> {
349 match value {
350 AttributeValue::UnsignedInteger(n) => Ok(n),
351 AttributeValue::SignedInteger(n) => {
352 u64::try_from(n).map_err(|_| E::custom("expected a non-negative whole number"))
353 }
354 // **Exclusive at the top, and that is not a style choice.**
355 // `u64::MAX as f64` rounds *up* to 2^64, which is one greater than
356 // any `u64`. An inclusive bound therefore admitted exactly 2^64,
357 // and `n as u64` saturates it to `u64::MAX` -- so a description
358 // carrying 18446744073709551616 was silently read as a different
359 // number. Excluding the bound rejects it instead, and costs
360 // nothing: the largest `f64` below 2^64 is 2^64 - 2048, which is a
361 // representable `u64` and still accepted. Raised in the PR #56
362 // review.
363 AttributeValue::Float(n) if n.fract() == 0.0 && (0.0..u64::MAX as f64).contains(&n) => {
364 Ok(n as u64)
365 }
366 _ => Err(E::custom("expected a non-negative whole number")),
367 }
368 }
369
370 fn as_u32<E: Error>(value: AttributeValue) -> Result<u32, E> {
371 u32::try_from(as_u64(value)?).map_err(|_| E::custom("number is too large for this field"))
372 }
373
374 /// As [`as_u64`], but over the signed range: `CacheKind::Other` carries a
375 /// raw `PROCESSOR_CACHE_TYPE`, a C enum backed by `i32`, which is not
376 /// guaranteed non-negative. Preserved exactly for either integer variant;
377 /// only a [`AttributeValue::Float`] is converted, subject to the same
378 /// fractional/range check as before.
379 fn as_i64<E: Error>(value: AttributeValue) -> Result<i64, E> {
380 match value {
381 AttributeValue::SignedInteger(n) => Ok(n),
382 AttributeValue::UnsignedInteger(n) => {
383 i64::try_from(n).map_err(|_| E::custom("expected a whole number"))
384 }
385 // Exclusive at the top for the reason `as_u64` gives, and found by
386 // sweeping for the same shape rather than reported: `i64::MAX as
387 // f64` rounds up to 2^63, which no `i64` can hold, and the cast
388 // would saturate it to `i64::MAX`. The *lower* bound stays
389 // inclusive because `i64::MIN as f64` is -2^63 exactly -- it is a
390 // power of two and representable, so it converts back losslessly.
391 AttributeValue::Float(n)
392 if n.fract() == 0.0 && (i64::MIN as f64..i64::MAX as f64).contains(&n) =>
393 {
394 Ok(n as i64)
395 }
396 _ => Err(E::custom("expected a whole number")),
397 }
398 }
399
400 fn as_i32<E: Error>(value: AttributeValue) -> Result<i32, E> {
401 i32::try_from(as_i64(value)?).map_err(|_| E::custom("number is too large for this field"))
402 }
403
404 fn as_u16<E: Error>(value: AttributeValue) -> Result<u16, E> {
405 u16::try_from(as_u64(value)?).map_err(|_| E::custom("number is too large for this field"))
406 }
407
408 fn as_u8<E: Error>(value: AttributeValue) -> Result<u8, E> {
409 u8::try_from(as_u64(value)?).map_err(|_| E::custom("number is too large for this field"))
410 }
411
412 /// `CacheKind`'s own derived `Serialize` produces a bare string for a
413 /// unit variant and `{"other": <number>}` for `Other`; decoded here by
414 /// hand against the already-buffered value rather than by re-entering
415 /// serde's enum machinery, which expects a real `Deserializer` rather
416 /// than a value already read into an [`AttributeValue`].
417 fn cache_kind_from_value<E: Error>(value: AttributeValue) -> Result<CacheKind, E> {
418 match value {
419 AttributeValue::String(s) => match s.as_str() {
420 "unified" => Ok(CacheKind::Unified),
421 "instruction" => Ok(CacheKind::Instruction),
422 "data" => Ok(CacheKind::Data),
423 "trace" => Ok(CacheKind::Trace),
424 other => Err(E::custom(format!("unrecognised cache_type \"{other}\""))),
425 },
426 AttributeValue::Object(mut map) if map.len() == 1 => match map.remove("other") {
427 Some(raw) => Ok(CacheKind::Other(as_i32(raw)?)),
428 None => Err(E::custom("cache_type object must have an \"other\" key")),
429 },
430 _ => Err(E::custom(
431 "cache_type must be a string or {\"other\": <number>}",
432 )),
433 }
434 }
435
436 /// Convert a buffered `"processors"` value (a JSON array of
437 /// `{"group":_,"number":_}` objects) into a [`ProcessorSet`], without
438 /// going through `ProcessorSet`'s own `Deserialize` impl: that impl
439 /// expects a real `Deserializer`, and this value has already been
440 /// buffered into an [`AttributeValue`].
441 fn processors_from_value<E: Error>(value: AttributeValue) -> Result<ProcessorSet, E> {
442 let AttributeValue::Array(items) = value else {
443 return Err(E::custom("domain \"processors\" must be an array"));
444 };
445 let mut set = ProcessorSet::empty();
446 for item in items {
447 let AttributeValue::Object(mut object) = item else {
448 return Err(E::custom(
449 "each processor must be an object with \"group\" and \"number\"",
450 ));
451 };
452 let group = as_u16(take(&mut object, "group")?)?;
453 let number = as_u8(take(&mut object, "number")?)?;
454 if u32::from(number) >= MAX_PROCESSORS_PER_GROUP {
455 // A real Windows GROUP_AFFINITY.Mask is one machine word, so
456 // a group genuinely cannot hold this processor: reject rather
457 // than silently truncate (D-10 in `DESIGN-NOTES.md`).
458 return Err(E::custom(format!(
459 "processor number {number} is out of range: a processor group has at most \
460 {MAX_PROCESSORS_PER_GROUP} processors on this platform"
461 )));
462 }
463 set.insert(group, number);
464 }
465 Ok(set)
466 }
467
468 fn take<E: Error>(
469 fields: &mut BTreeMap<String, AttributeValue>,
470 key: &str,
471 ) -> Result<AttributeValue, E> {
472 fields
473 .remove(key)
474 .ok_or_else(|| E::custom(format!("domain is missing required field \"{key}\"")))
475 }
476
477 /// Every kind name this crate decodes into a named [`DomainKind`].
478 ///
479 /// **Must list exactly the arms the deserializer matches before its
480 /// `other =>` fallback**, and `every_well_known_name_decodes_to_a_named_kind`
481 /// asserts that it does rather than leaving the two to drift.
482 pub(super) const WELL_KNOWN_KIND_NAMES: &[&str] = &[
483 "group", "package", "die", "module", "core", "cache", "memory",
484 ];
485
486 impl Serialize for Domain {
487 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
488 let mut map = serializer.serialize_map(None)?;
489 let kind_name: &str = match &self.kind {
490 DomainKind::Group => "group",
491 DomainKind::Package => "package",
492 DomainKind::Die => "die",
493 DomainKind::Module => "module",
494 DomainKind::Core { .. } => "core",
495 DomainKind::Cache { .. } => "cache",
496 DomainKind::Memory { .. } => "memory",
497 DomainKind::Other { name, .. } => {
498 // The same rule as the attribute-name collision check
499 // below, for the same reason and by the same remedy.
500 //
501 // `Other` exists so a description this crate cannot fully
502 // interpret "still round-trips losslessly", and a name this
503 // crate *does* interpret breaks exactly that: the document
504 // would say `"kind": "group"`, and reading it back yields
505 // `DomainKind::Group`, not the `Other` that was written.
506 // `Group`, `Package`, `Die` and `Module` carry no fields,
507 // so that substitution succeeds silently and the attributes
508 // are dropped on the floor; `core`, `cache` and `memory`
509 // fail loudly on a missing field, or -- worse -- succeed as
510 // a different kind when the attributes happen to match.
511 //
512 // Refused rather than escaped or renamed, because both of
513 // those would change a name the caller chose. Raised in the
514 // PR #56 review.
515 if WELL_KNOWN_KIND_NAMES.contains(&name.as_str()) {
516 return Err(S::Error::custom(format!(
517 "domain kind name \"{name}\" collides with a kind this crate names \
518 itself, so the document would deserialize as that kind rather than \
519 as `Other`"
520 )));
521 }
522 name.as_str()
523 }
524 };
525 map.serialize_entry("kind", kind_name)?;
526 // The wire shape keeps an "id" because a description is written by
527 // hand and a human-meaningful number is worth having. It is the
528 // relationship walk's label where there is one -- not "the id",
529 // which stopped being well defined once two sources labelled the
530 // same relation differently (D-15).
531 if let Some(label) = self.label_from(crate::observation::Source::RelationshipWalk) {
532 map.serialize_entry("id", &label)?;
533 }
534 map.serialize_entry("processors", &self.processors)?;
535 match &self.kind {
536 DomainKind::Core {
537 simultaneous_multithreading,
538 efficiency_class,
539 } => {
540 map.serialize_entry(
541 "simultaneous_multithreading",
542 simultaneous_multithreading,
543 )?;
544 map.serialize_entry("efficiency_class", efficiency_class)?;
545 }
546 DomainKind::Cache {
547 level,
548 associativity,
549 line_size,
550 size_bytes,
551 cache_type,
552 } => {
553 map.serialize_entry("level", level)?;
554 map.serialize_entry("associativity", associativity)?;
555 map.serialize_entry("line_size", line_size)?;
556 map.serialize_entry("size_bytes", size_bytes)?;
557 map.serialize_entry("cache_type", cache_type)?;
558 }
559 DomainKind::Memory { memory_bytes } => {
560 // Three states, three wire shapes: a number, an explicit
561 // `null` for "addressed and unknown", and omission for
562 // "nobody said". Writing `null` for both would put the
563 // ambiguity back on the wire that the type just removed.
564 match memory_bytes {
565 crate::observed::Observed::Known(bytes) => {
566 map.serialize_entry("memory_bytes", bytes)?;
567 }
568 crate::observed::Observed::Absent => {
569 map.serialize_entry("memory_bytes", &Option::<u64>::None)?;
570 }
571 crate::observed::Observed::NotObserved => {}
572 }
573 }
574 DomainKind::Other { attributes, .. } => {
575 for (key, value) in attributes {
576 if matches!(key.as_str(), "kind" | "id" | "processors") {
577 // These three keys are always written above;
578 // silently letting an attribute reuse one would
579 // corrupt the wire shape rather than round-trip.
580 return Err(S::Error::custom(format!(
581 "domain attribute \"{key}\" collides with the reserved \
582 \"kind\"/\"id\"/\"processors\" field names"
583 )));
584 }
585 map.serialize_entry(key, value)?;
586 }
587 }
588 DomainKind::Group | DomainKind::Package | DomainKind::Die | DomainKind::Module => {}
589 }
590 map.end()
591 }
592 }
593
594 impl<'de> Deserialize<'de> for Domain {
595 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
596 let mut fields = BTreeMap::<String, AttributeValue>::deserialize(deserializer)?;
597
598 let kind_name = match take::<D::Error>(&mut fields, "kind")? {
599 AttributeValue::String(s) => s,
600 _ => return Err(D::Error::custom("domain \"kind\" must be a string")),
601 };
602 // Read and discarded, and OPTIONAL. The wire "id" is one source's
603 // label; a file cannot establish which source observed the relation
604 // (D-12), so it never becomes an observation. Since it carries no
605 // model meaning, requiring it would reject a description that is
606 // otherwise complete -- including one this crate itself writes for a
607 // relation no source labelled.
608 let _ = fields.remove("id");
609 let processors = processors_from_value(take::<D::Error>(&mut fields, "processors")?)?;
610 // A described relation carries NO platform observation, and the
611 // wire shape does not encode them.
612 //
613 // This is the same downgrade `Provenance::downgraded_to` performs
614 // one level up (D-12): a file saying "the relationship walk
615 // observed this" cannot establish that it did, so the claim is not
616 // carried across the boundary. Serializing observations faithfully
617 // would carry exactly the claim D-12 refuses.
618 //
619 // Nor is a `Source::Description` observation synthesized here. That
620 // would restate what the object already says -- a deserialized
621 // topology's `Provenance` is capped at `Restored` -- and D-22 has
622 // just established these two are different questions that should
623 // not duplicate each other.
624 let observations = Vec::new();
625
626 let kind = match kind_name.as_str() {
627 "group" => DomainKind::Group,
628 "package" => DomainKind::Package,
629 "die" => DomainKind::Die,
630 "module" => DomainKind::Module,
631 "core" => DomainKind::Core {
632 simultaneous_multithreading: as_bool(take(
633 &mut fields,
634 "simultaneous_multithreading",
635 )?)?,
636 efficiency_class: as_u8(take(&mut fields, "efficiency_class")?)?,
637 },
638 "cache" => DomainKind::Cache {
639 level: as_u8(take(&mut fields, "level")?)?,
640 associativity: as_u8(take(&mut fields, "associativity")?)?,
641 line_size: as_u16(take(&mut fields, "line_size")?)?,
642 size_bytes: as_u32(take(&mut fields, "size_bytes")?)?,
643 cache_type: cache_kind_from_value(take(&mut fields, "cache_type")?)?,
644 },
645 "memory" => DomainKind::Memory {
646 memory_bytes: match fields.remove("memory_bytes") {
647 None => crate::observed::Observed::NotObserved,
648 Some(AttributeValue::Null) => crate::observed::Observed::Absent,
649 Some(value) => crate::observed::Observed::Known(as_u64(value)?),
650 },
651 },
652 // Every arm above must appear in `WELL_KNOWN_KIND_NAMES`, or
653 // `Serialize` would let an `Other` claim that name and the
654 // document would decode as the named kind instead. The test
655 // named on that constant is what enforces it.
656 other => DomainKind::Other {
657 name: other.to_string(),
658 attributes: fields,
659 },
660 };
661
662 Ok(Domain {
663 kind,
664 processors,
665 observations,
666 })
667 }
668 }
669}
670
671/// Everything this crate knows about one processor, with each absence named.
672///
673/// Produced by [`MachineMemoryTopology::shard_set`](crate::MachineMemoryTopology::shard_set). Deliberately **not** a
674/// second copy of [`Processor`]: that type is the platform's own record, while
675/// this is the assembled answer to "may this processor host work, and where
676/// does it allocate from" -- gathered from both Win32 sources plus the derived
677/// relations.
678///
679/// # No sentinels, anywhere
680///
681/// Every optional field is an [`Observed`], so "the platform said zero" and
682/// "nobody asked" are different values rather than the same one. The field
683/// this exists to replace, [`Processor::capacity`], spells three facts as `0`
684/// -- offline, in no core, and efficiency class zero -- and the third is every
685/// processor on every non-hybrid machine.
686#[derive(Clone, Debug, PartialEq)]
687pub struct ProcessorFacts<'a> {
688 /// The processor's identity, always `(group, number)` and never flattened
689 /// (D-7): a Windows affinity is a `GROUP_AFFINITY`, so a bare index names a
690 /// different processor in every group.
691 pub id: ProcessorId,
692 /// Whether the slot is active. An offline slot exists and counts toward its
693 /// group's maximum, so planning work onto one is planning a thread that
694 /// cannot run.
695 pub online: bool,
696 /// The core relation this processor belongs to, if any names it.
697 ///
698 /// `None` is a firmware gap the crate tolerates by design, not a
699 /// contradiction -- see [`Self::efficiency_class`], which is
700 /// [`Observed::NotObserved`] in exactly that case rather than `0`.
701 pub core: Option<&'a Domain>,
702 /// Whether the owning core has more than one logical processor.
703 pub simultaneous_multithreading: Observed<bool>,
704 /// The scheduler's efficiency class for the owning core.
705 ///
706 /// [`Observed::NotObserved`] when no core names this processor -- which is
707 /// the distinction `Processor::capacity` cannot make. On a hybrid part
708 /// Windows orders class `0` as the *least* performant, so an unknown
709 /// processor reported as `0` is indistinguishable from an efficiency core:
710 /// a policy excluding efficiency cores silently drops a possible
711 /// performance core, and one tiering them mis-tiers it. Neither fails a
712 /// functional test.
713 pub efficiency_class: Observed<u8>,
714 /// Whether the scheduler is currently avoiding this processor.
715 ///
716 /// [`Observed::NotObserved`] when the CPU-set enumeration was not
717 /// consulted, which is any topology not produced by
718 /// [`MachineMemoryTopology::discover`](crate::MachineMemoryTopology::discover). Parked is **not** offline: the
719 /// processor is active and the scheduler is merely avoiding it.
720 pub parked: Observed<bool>,
721 /// Whether this processor is allocated to *this* process.
722 ///
723 /// A planner ignoring it places work on processors the process may not use,
724 /// which is a wrong plan rather than a slow one.
725 ///
726 /// **Measured to carry no information on Windows 11 25H2** (D-23): the
727 /// whole `AllFlags` byte reads `0x00` for every processor even after CPU
728 /// sets are successfully allocated to this process, so `Known(false)` here
729 /// reports a byte the kernel did not write.
730 ///
731 /// It also does not mean what its name suggests -- allocation is the
732 /// explicit `SetProcessDefaultCpuSets` kind, not "may we run here". Do not
733 /// branch on it; see [`CpuSet::allocated_to_target_process`](crate::CpuSet::allocated_to_target_process).
734 pub allocated_to_this_process: Observed<bool>,
735 /// The memory domain this processor allocates from, or
736 /// [`Observed::NotObserved`] for the **unplaced** case, which has no honest
737 /// fallback -- see [`MachineMemoryTopology::memory_domain_of`](crate::MachineMemoryTopology::memory_domain_of).
738 pub memory_domain: Observed<&'a Domain>,
739}
740
741#[cfg(test)]
742mod tests;