windows_topology_sys/observation.rs
1// Copyright (c) 2026 Mike Grier
2//! Who reported a relation, and what they called it.
3//!
4//! Windows describes processor structure through two APIs that overlap without
5//! agreeing on names, so "which source said this" is a fact the model has to
6//! carry rather than infer. See D-15 and D-18 in `DESIGN-NOTES.md`.
7
8/// Which platform API reported something.
9///
10/// Not a trust ordering. Both sources are cheap reads of the running system
11/// and neither is more authoritative than the other -- where they disagree,
12/// [D-15](../DESIGN-NOTES.md) keeps both rather than picking a winner, and
13/// [D-17](../DESIGN-NOTES.md) expects genuine disagreement in the field. Trust
14/// in the *object* is [`Provenance`](crate::Provenance), which is a different
15/// question (D-22): where this says who spoke, that says how the collection
16/// happened.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
20#[non_exhaustive]
21pub enum Source {
22 /// `GetLogicalProcessorInformationEx` -- the relationship walk.
23 RelationshipWalk,
24 /// `GetSystemCpuSetInformation` -- the CPU-set enumeration.
25 CpuSets,
26 /// A description a caller merged in, which named no platform API.
27 ///
28 /// **Not** what deserialization produces: a restored relation carries no
29 /// observation at all, because the object's
30 /// [`Provenance`](crate::Provenance) already records that it came from a
31 /// file, and restating it here would duplicate a fact D-22 has just
32 /// separated. This variant is for the *mixed* case -- a caller adding
33 /// described relations to a topology that was discovered -- which is
34 /// exactly the case per-relation provenance exists to make visible.
35 Description,
36}
37
38/// One source's report of a relation, carrying that source's own label for it.
39///
40/// # Why the label lives here and not on the relation
41///
42/// [D-15](../DESIGN-NOTES.md) identifies a relation by `(kind, membership)`,
43/// because that is what the sources agree about. What they do **not** agree
44/// about is naming: measured on the development host, the two report the same
45/// eight-group core partition while labelling it `[0, 2, 4, ..., 14]` and
46/// `[0, 1, ..., 7]`. Neither numbering is wrong and neither is a claim about
47/// the machine, so a single `id` on the relation would have to pick one
48/// arbitrarily and discard the other.
49///
50/// Putting the label on the observation removes the choice: one relation, two
51/// observations, each keeping what its source called it.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub struct Observation {
55 /// Which API reported this relation.
56 pub source: Source,
57 /// What that source called it.
58 ///
59 /// A NUMA node number, a processor group number, a CPU-set `CoreIndex`, or
60 /// -- where the source numbers nothing -- the position it was reported in.
61 /// Meaningful only alongside [`Self::source`], never on its own.
62 pub label: u32,
63}
64
65impl Observation {
66 /// An observation by `source`, labelled `label`.
67 #[must_use]
68 pub fn new(source: Source, label: u32) -> Self {
69 Self { source, label }
70 }
71}
72
73/// A per-processor attribute that more than one source may describe.
74///
75/// [D-18](../DESIGN-NOTES.md)'s **second subject kind**. Relation unification
76/// matches on `(kind, membership)`, so two sources describing one core agree
77/// about *that* even when they disagree about a number hanging off it -- and
78/// that disagreement has nowhere to live in a relation, because both
79/// observations name the same relation.
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
83#[non_exhaustive]
84pub enum ProcessorAttribute {
85 /// The scheduler's efficiency class.
86 ///
87 /// Both Windows APIs report it: the relationship walk attaches it to a
88 /// core, and CPU Sets to each processor. On a hybrid part this is the value
89 /// that decides whether a processor is a performance or an efficiency core,
90 /// so a silent disagreement is a planning defect that shows up only in a
91 /// percentile.
92 EfficiencyClass,
93}
94
95/// One source's claim about one processor's attribute.
96///
97/// The `(subject, claim, source)` triple of [D-18](../DESIGN-NOTES.md), where
98/// the subject is `(processor, attribute)` rather than a relation identity.
99/// Held as a list and never reduced, for the same reason relations hold their
100/// observations as a set: collapsing them would destroy the disagreement, which
101/// is the only thing a second observer is for.
102#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104pub struct AttributeObservation {
105 /// The processor this is about.
106 pub processor: crate::domain::ProcessorId,
107 /// Which attribute.
108 pub attribute: ProcessorAttribute,
109 /// What this source said its value is.
110 pub value: u32,
111 /// Which source said it.
112 pub source: Source,
113}
114
115impl AttributeObservation {
116 /// An observation of `attribute` on `processor`.
117 #[must_use]
118 pub fn new(
119 processor: crate::domain::ProcessorId,
120 attribute: ProcessorAttribute,
121 value: u32,
122 source: Source,
123 ) -> Self {
124 Self {
125 processor,
126 attribute,
127 value,
128 source,
129 }
130 }
131
132 /// The subject this observation is about.
133 #[must_use]
134 pub fn subject(&self) -> (crate::domain::ProcessorId, ProcessorAttribute) {
135 (self.processor, self.attribute)
136 }
137}
138
139#[cfg(test)]
140mod tests;