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;