Skip to main content

windows_topology_sys/
granularity.rs

1// Copyright (c) 2026 Mike Grier
2//! The granularity order: what a set of processors shares, ordered by
3//! observed set inclusion.
4//!
5//! This is the model `M2+.2`, `M2+.3` and `M2+.4` call for, and the three are
6//! one type because they cannot be separated: an order derived from inclusion
7//! has to say what its top is and what it does when two elements do not nest.
8//!
9//! # Why inclusion, and not the level number
10//!
11//! [`DomainKind::Cache`](crate::DomainKind::Cache) carries a firmware `level`,
12//! and ordering by it is the obvious thing to do. This crate does not, because
13//! a level number is **asserted** by firmware while a membership is
14//! **observed**, and asserted structure is what has bitten this crate before:
15//! the ARM64 host that reports no L3 at all, and the consumer that swept a
16//! hard-coded `1..=4`. Inclusion is checkable against the very sets Windows
17//! reported, so it cannot disagree with them.
18//!
19//! It is also strictly more general. Inclusion orders a cache against a *core*
20//! and against a *NUMA node*, which share no numbering with it and could not
21//! otherwise be compared at all.
22
23use crate::domain::{Domain, DomainKind, ProcessorId};
24use crate::processor_set::ProcessorSet;
25use crate::topology::MachineMemoryTopology;
26
27/// One element of the granularity order: something a set of processors can
28/// be observed to share.
29///
30/// Ordered by inclusion of the processors covered, never by any level number
31/// (see the module documentation).
32#[derive(Clone, Copy, Debug, PartialEq)]
33pub enum Granularity<'a> {
34    /// One relation the platform reported.
35    Relation(&'a Domain),
36    /// **The machine** -- the order's top, and the reason a query over it is
37    /// total.
38    ///
39    /// Two processors in one machine always share *something*: one address
40    /// space, one scheduler, one memory system. Without an explicit top, a
41    /// query about a pair that no reported relation covers would answer
42    /// "nothing", and every caller would write the same empty-case branch for
43    /// a cross-node pair.
44    ///
45    /// It is deliberately **not** a [`Domain`] in
46    /// [`MachineMemoryTopology::domains`]: putting it there would claim the
47    /// platform observed a relation it never reported.
48    Machine,
49}
50
51impl<'a> Granularity<'a> {
52    /// The relation this names, or `None` for [`Granularity::Machine`].
53    #[must_use]
54    pub fn relation(self) -> Option<&'a Domain> {
55        match self {
56            Self::Relation(domain) => Some(domain),
57            Self::Machine => None,
58        }
59    }
60
61    /// Whether this is the order's top.
62    #[must_use]
63    pub fn is_machine(self) -> bool {
64        matches!(self, Self::Machine)
65    }
66
67    /// The processors this granularity covers.
68    ///
69    /// Takes the topology because [`Granularity::Machine`] has no membership
70    /// of its own -- it is every processor the topology knows, which only the
71    /// topology can supply.
72    #[must_use]
73    pub fn processors(self, topology: &MachineMemoryTopology) -> ProcessorSet {
74        match self {
75            Self::Relation(domain) => domain.processors.clone(),
76            Self::Machine => topology.machine_processors(),
77        }
78    }
79}
80
81impl MachineMemoryTopology {
82    /// Every processor this topology knows, online or not.
83    ///
84    /// The membership of [`Granularity::Machine`]. Offline processors are
85    /// included on purpose: a slot that exists is still part of the machine,
86    /// and excluding it would make a query naming it answer "nothing shared"
87    /// rather than "the machine", which is the empty-case branch the top
88    /// exists to remove.
89    #[must_use]
90    pub fn machine_processors(&self) -> ProcessorSet {
91        let mut set = ProcessorSet::empty();
92        for processor in &self.processors {
93            set.insert(processor.id.group, processor.id.number);
94        }
95        set
96    }
97
98    /// The **minimal** granularities that cover every processor in `of` --
99    /// the tightest things they all share.
100    ///
101    /// # The answer is a set, not one element
102    ///
103    /// Inclusion is a *partial* order, so two granularities can be
104    /// incomparable: neither contains the other, and both are therefore
105    /// minimal. It is almost always one element, but not by construction, and
106    /// a caller that takes the first must say that is what it is doing.
107    ///
108    /// Equal memberships are the ordinary case of this. Measured on the
109    /// development host, L1 arrives as a data cache *and* an instruction cache
110    /// over the very same processors -- two relations, distinct in kind and
111    /// attributes, tied in the order. Both are returned, because picking one
112    /// would be arbitrary.
113    ///
114    /// # When the answer is the machine
115    ///
116    /// [`Granularity::Machine`] is returned exactly when **no** reported
117    /// relation covers `of`. It is the fallback that makes this query total
118    /// rather than an element that competes with observed relations, so it
119    /// never appears alongside one.
120    ///
121    /// # When the answer is empty
122    ///
123    /// Totality holds over processors this topology knows. A set naming a
124    /// processor it does not know is answered with an empty result rather than
125    /// with the machine, because claiming the machine contains a processor it
126    /// has never heard of would be an invention.
127    #[must_use]
128    pub fn minimal_shared(&self, of: &ProcessorSet) -> Vec<Granularity<'_>> {
129        if !of.is_subset(&self.machine_processors()) {
130            return Vec::new();
131        }
132
133        let covering: Vec<&Domain> = self
134            .domains
135            .iter()
136            .filter(|domain| of.is_subset(&domain.processors))
137            .collect();
138
139        if covering.is_empty() {
140            return vec![Granularity::Machine];
141        }
142
143        covering
144            .iter()
145            .filter(|candidate| {
146                // Minimal means nothing else covering `of` sits strictly
147                // inside it. Strict is what keeps a tie -- two relations over
148                // identical sets do not exclude each other, so both survive.
149                !covering.iter().any(|other| {
150                    other.processors.is_subset(&candidate.processors)
151                        && other.processors != candidate.processors
152                })
153            })
154            .map(|domain| Granularity::Relation(domain))
155            .collect()
156    }
157
158    /// What a set of processors shares, and whether the answer is complete.
159    ///
160    /// The pairwise proximity query, generalised to any set because nothing in
161    /// it is specific to two: `proximity(&[a, b])` is the pair case, and the
162    /// same call sizes an MPSC fan-in over a whole block.
163    ///
164    /// # It is a helper over the order, not the primary surface
165    ///
166    /// The body is [`Self::minimal_shared`] plus the coverage check below. That
167    /// is deliberate: the *collection* is what a caller reaching for structure
168    /// should use, because rebuilding the grouping from repeated pairwise calls
169    /// takes union-find over `O(n^2)` questions -- and that reconstruction is
170    /// exactly what `SH-16.9` records three consumers doing, two of them
171    /// differently. Providing the helper here means there is one implementation
172    /// of the grouping rather than one per caller.
173    ///
174    /// # Errors
175    ///
176    /// None -- the query is total over processors this topology knows, and
177    /// answers [`Granularity::Machine`] rather than "nothing" for a pair no
178    /// relation covers. A processor it does not know yields an empty
179    /// [`Proximity::shared`], because claiming the machine contains a processor
180    /// it has never heard of would be an invention.
181    #[must_use]
182    pub fn proximity(&self, processors: &[ProcessorId]) -> Proximity<'_> {
183        // **A query may not abort over its own argument.** `ProcessorId`'s
184        // fields are public and `number` is a `u8`, so any value up to 255 is
185        // constructible, while a `ProcessorSet` holds one bit per processor in
186        // a `usize` -- and `ProcessorSet::insert` asserts on anything wider.
187        // Passing such an id here used to panic, which is a poor answer to a
188        // question about a machine that simply does not have that processor.
189        //
190        // The threshold is target-dependent, which is what makes it sharp:
191        // `usize::BITS` is 64 on x86-64 and 32 on `i686-pc-windows-msvc`, a
192        // target [D-18](../DESIGN-NOTES.md#d-18) deliberately keeps supported.
193        // A `number` of 40 is an ordinary query on one and an abort on the
194        // other. Raised in the PR #56 review.
195        //
196        // Reported rather than skipped: dropping the id would answer a question
197        // about fewer processors than were asked about, and "these share
198        // nothing" would become indistinguishable from "one of these cannot
199        // exist here". That is the conflation `Observed` exists to remove,
200        // arriving by a different door.
201        let mut set = ProcessorSet::empty();
202        let mut representable = Vec::new();
203        let mut unrepresentable = Vec::new();
204        for id in processors {
205            if ProcessorSet::can_represent(id.number) {
206                set.insert(id.group, id.number);
207                representable.push(*id);
208            } else {
209                unrepresentable.push(*id);
210            }
211        }
212        Proximity {
213            shared: self.minimal_shared(&set),
214            // Over the representable ids only. `ProcessorSet::contains`
215            // answers `false` for an out-of-range number, which is
216            // indistinguishable from "the platform never placed this
217            // processor in a relation of this kind" -- so an unrepresentable
218            // id would report a coverage gap at *every* reported kind at
219            // once, on a machine that described all of its processors fully.
220            //
221            // That would restate a defect in the query as a gap in the
222            // platform's reporting, when the defect is already named in
223            // `unrepresentable`. Raised in the PR #61 review.
224            finer_unobserved: representable
225                .iter()
226                .any(|id| !self.kinds_covering(*id).is_empty()),
227            unrepresentable,
228        }
229    }
230
231    /// The relation kinds this machine reports that `processor` appears in
232    /// **no** instance of.
233    ///
234    /// Evidence of a gap in what the platform said, not of a fact about the
235    /// machine. A kind is only counted when some *other* processor is covered
236    /// by it, so a machine that reports no caches at all does not make every
237    /// answer an upper bound -- that is a complete description of a machine
238    /// without caches, and treating it as incomplete would be the same
239    /// conflation [D-13](../DESIGN-NOTES.md) exists to remove.
240    fn kinds_covering(&self, processor: ProcessorId) -> Vec<&'static str> {
241        let mut missing = Vec::new();
242        for kind in Self::REPORTED_KINDS {
243            let mut kind_exists = false;
244            let mut covers = false;
245            for domain in &self.domains {
246                if Self::kind_name(&domain.kind) != kind {
247                    continue;
248                }
249                kind_exists = true;
250                if domain
251                    .processors
252                    .contains(processor.group, processor.number)
253                {
254                    covers = true;
255                    break;
256                }
257            }
258            if kind_exists && !covers {
259                missing.push(kind);
260            }
261        }
262        missing
263    }
264
265    /// The kinds a coverage gap is meaningful for.
266    ///
267    /// Deliberately not every [`DomainKind`]: `Memory` is excluded because a
268    /// processor in no memory domain is `M4+.3`'s *unplaced* case, which has
269    /// its own answer and is not evidence that something finer went unreported.
270    const REPORTED_KINDS: [&'static str; 5] = ["cache", "core", "module", "die", "package"];
271
272    fn kind_name(kind: &DomainKind) -> &'static str {
273        match kind {
274            DomainKind::Group => "group",
275            DomainKind::Package => "package",
276            DomainKind::Die => "die",
277            DomainKind::Module => "module",
278            DomainKind::Core { .. } => "core",
279            DomainKind::Cache { .. } => "cache",
280            DomainKind::Memory { .. } => "memory",
281            DomainKind::Other { .. } => "other",
282        }
283    }
284
285    /// Whether `finer` sits strictly inside `coarser` in the granularity
286    /// order.
287    ///
288    /// The order's comparison, exposed so a caller can position an answer
289    /// against another without re-deriving what "tighter" means -- the
290    /// re-derivation this model exists to stop.
291    #[must_use]
292    pub fn is_finer_than(&self, finer: Granularity<'_>, coarser: Granularity<'_>) -> bool {
293        let finer = finer.processors(self);
294        let coarser = coarser.processors(self);
295        finer.is_subset(&coarser) && finer != coarser
296    }
297}
298
299/// What a set of processors shares, and how far the answer can be trusted.
300///
301/// The answer to [`MachineMemoryTopology::proximity`], and the reason it is a
302/// struct rather than a bare list: a caller needs to know not only what was
303/// found but whether something finer might exist and simply was not reported.
304#[derive(Clone, Debug, PartialEq)]
305pub struct Proximity<'a> {
306    /// The tightest granularities all the processors share.
307    ///
308    /// A **set**, because inclusion is a partial order and two granularities
309    /// can be incomparable -- almost always one element, but not by
310    /// construction (M2+.4). Exactly [`Granularity::Machine`] when no reported
311    /// relation covers them all, which is what makes the query total (M2+.3).
312    pub shared: Vec<Granularity<'a>>,
313    /// Whether a **finer** granularity may exist that was never reported.
314    ///
315    /// When true, [`Self::shared`] is an **upper bound** rather than the
316    /// answer: the machine describes some kind of relation that one of these
317    /// processors appears in *no* instance of, so the platform has said nothing
318    /// about whether they share it.
319    ///
320    /// This is the third of `EP-D-2`'s requirements, and the one a naive design
321    /// drops. A caller told "the tightest shared thing is L3" when in truth L2
322    /// was never reported for this processor would choose a slower channel than
323    /// the machine can support and never learn why -- and under this crate's own
324    /// bar it cannot go and measure to find out.
325    ///
326    pub finer_unobserved: bool,
327    /// Processors the query named that this platform cannot express, and which
328    /// therefore took no part in [`Self::shared`].
329    ///
330    /// Empty in every ordinary case. Non-empty means a caller passed a
331    /// `ProcessorId` whose `number` is at least `usize::BITS` -- constructible,
332    /// because the field is public and it is a `u8`, and target-dependent,
333    /// because that ceiling is 64 on x86-64 and 32 on `i686-pc-windows-msvc`.
334    ///
335    /// Carried rather than skipped so "these processors share nothing" stays
336    /// distinguishable from "one of these could not exist on this machine".
337    /// A caller that ignores this field gets the first reading of the second
338    /// situation, which is exactly the conflation this crate's `Observed`
339    /// exists to prevent elsewhere.
340    pub unrepresentable: Vec<ProcessorId>,
341}
342
343impl<'a> Proximity<'a> {
344    /// The single shared granularity, when there is exactly one.
345    ///
346    /// `None` when the answer is a tie, so a caller that cannot handle one is
347    /// forced to say so rather than silently taking the first.
348    #[must_use]
349    pub fn only(&self) -> Option<Granularity<'a>> {
350        match self.shared.as_slice() {
351            [single] => Some(*single),
352            _ => None,
353        }
354    }
355}
356
357#[cfg(test)]
358mod tests;