Skip to main content

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;