Skip to main content

windows_topology_sys/
relation.rs

1// Copyright (c) 2026 Mike Grier
2//! Faithful, owned records for each processor relationship Windows reports.
3//!
4//! "Faithful" means each record says what Win32 said, with a [`ProcessorSet`]
5//! in place of a raw mask and no interpretation layered on top of it (D-2 in
6//! `DESIGN-NOTES.md`).
7
8use std::io;
9
10use crate::processor_set::ProcessorSet;
11use crate::walk::{self, GroupAffinity, Record};
12
13/// The Win32 `LTP_PC_SMT` flag on `PROCESSOR_RELATIONSHIP::Flags`.
14///
15/// `windows-sys` does not export this constant, so it is named here rather
16/// than written as a bare literal at the call site.
17const LTP_PC_SMT: u8 = 0x1;
18
19fn to_processor_set(masks: &[GroupAffinity]) -> ProcessorSet {
20    let mut set = ProcessorSet::empty();
21    for m in masks {
22        set = set.union(&ProcessorSet::from_group_mask(m.group, m.mask));
23    }
24    set
25}
26
27/// A processor core: one or more logical processors sharing a physical core
28/// as SMT siblings.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct CoreRelation {
31    /// Whether this core has more than one logical processor (SMT /
32    /// hyperthreading).
33    pub simultaneous_multithreading: bool,
34    /// The scheduler's efficiency class for this core. Windows documents only
35    /// that a higher value means intrinsically greater performance and less
36    /// power efficiency than a lower value on a hybrid part; it does not fix
37    /// an absolute scale.
38    pub efficiency_class: u8,
39    /// The logical processors that make up this core.
40    pub processors: ProcessorSet,
41}
42
43/// A physical processor package (socket).
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct PackageRelation {
46    /// The logical processors in this package.
47    pub processors: ProcessorSet,
48}
49
50/// What a [`CacheRelation`] holds.
51///
52/// With the `serde` feature, a well-known variant serializes as its lowercase
53/// name (`"unified"`, `"instruction"`, `"data"`, `"trace"`); `Other` serializes
54/// as `{"other": <raw value>}`, so an unrecognised `PROCESSOR_CACHE_TYPE`
55/// still round-trips rather than losing its value.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
59#[non_exhaustive]
60pub enum CacheKind {
61    /// A unified instruction-and-data cache.
62    Unified,
63    /// An instruction-only cache.
64    Instruction,
65    /// A data-only cache.
66    Data,
67    /// A trace cache.
68    Trace,
69    /// A cache type this crate does not have a name for; carries Windows's
70    /// raw `PROCESSOR_CACHE_TYPE` value rather than losing it.
71    Other(i32),
72}
73
74/// A cache level shared by a set of logical processors.
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct CacheRelation {
77    /// Cache level (1, 2, 3, ...).
78    pub level: u8,
79    /// Associativity Windows reported, or `0xFF` for fully associative.
80    pub associativity: u8,
81    /// Cache line size in bytes.
82    pub line_size: u16,
83    /// Total cache size in bytes.
84    pub cache_size: u32,
85    /// What the cache holds.
86    pub cache_type: CacheKind,
87    /// The logical processors that share this cache.
88    pub processors: ProcessorSet,
89}
90
91/// A NUMA node, as Windows reports it: a set of logical processors.
92///
93/// Windows does not report a memory-only NUMA node through this API -- every
94/// node it returns here has at least one processor. This crate's own
95/// `Domain` type, built on top of these records, does not assume that; see
96/// `windows-topology-sys`'s D-5 for why.
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct NumaNodeRelation {
99    /// The NUMA node number.
100    pub node_number: u32,
101    /// The logical processors in this node.
102    pub processors: ProcessorSet,
103}
104
105/// One processor group Windows has created.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct GroupRelation {
108    /// The group number. Windows documents `GroupInfo[i]` as describing group
109    /// `i`, which is where this comes from.
110    pub group: u16,
111    /// The maximum number of processors this group could ever hold.
112    pub maximum_processor_count: u8,
113    /// The number of processors currently active in this group.
114    pub active_processor_count: u8,
115    /// The processors currently active in this group.
116    pub active_processors: ProcessorSet,
117}
118
119/// Every relationship Windows reports, decoded and owned.
120#[derive(Clone, Debug, Default, PartialEq, Eq)]
121pub struct Relations {
122    /// Every processor core.
123    pub cores: Vec<CoreRelation>,
124    /// Every processor package.
125    pub packages: Vec<PackageRelation>,
126    /// Every processor die, on systems that report the distinction from
127    /// package. Empty on a system that does not.
128    pub dies: Vec<PackageRelation>,
129    /// Every processor module, on systems that report it. Empty on a system
130    /// that does not.
131    pub modules: Vec<PackageRelation>,
132    /// Every cache level.
133    pub caches: Vec<CacheRelation>,
134    /// Every NUMA node.
135    pub numa_nodes: Vec<NumaNodeRelation>,
136    /// Every processor group.
137    pub groups: Vec<GroupRelation>,
138    /// Records the walk could not decode. Empty on a healthy machine; see
139    /// [`crate::MachineMemoryTopology::enumeration_anomalies`].
140    pub anomalies: Vec<crate::EnumerationAnomaly>,
141}
142
143fn core_from(body: walk::ProcessorBody) -> CoreRelation {
144    CoreRelation {
145        simultaneous_multithreading: body.flags & LTP_PC_SMT != 0,
146        efficiency_class: body.efficiency_class,
147        processors: to_processor_set(&body.group_masks),
148    }
149}
150
151fn package_from(body: walk::ProcessorBody) -> PackageRelation {
152    PackageRelation {
153        processors: to_processor_set(&body.group_masks),
154    }
155}
156
157fn cache_from(body: walk::CacheBody) -> CacheRelation {
158    CacheRelation {
159        level: body.level,
160        associativity: body.associativity,
161        line_size: body.line_size,
162        cache_size: body.cache_size,
163        cache_type: walk::cache_kind_from_raw(body.cache_type),
164        processors: to_processor_set(&body.group_masks),
165    }
166}
167
168fn numa_from(body: walk::NumaNodeBody) -> NumaNodeRelation {
169    NumaNodeRelation {
170        node_number: body.node_number,
171        processors: to_processor_set(&body.group_masks),
172    }
173}
174
175fn groups_from(body: walk::GroupBody) -> Vec<GroupRelation> {
176    body.group_info
177        .into_iter()
178        .enumerate()
179        .map(|(index, info)| GroupRelation {
180            group: index as u16,
181            maximum_processor_count: info.maximum_processor_count,
182            active_processor_count: info.active_processor_count,
183            active_processors: ProcessorSet::from_group_mask(
184                index as u16,
185                info.active_processor_mask,
186            ),
187        })
188        .collect()
189}
190
191/// Enumerate every processor, package, cache, NUMA node, and group relation
192/// the system reports.
193///
194/// # Errors
195///
196/// Returns any error from the underlying `GetLogicalProcessorInformationEx`
197/// call.
198pub fn discover() -> io::Result<Relations> {
199    let mut relations = Relations::default();
200    let (walk_records, walk_anomalies) = walk::enumerate()?;
201    for record in walk_records {
202        match record {
203            Record::ProcessorCore(body) => relations.cores.push(core_from(body)),
204            Record::ProcessorPackage(body) => relations.packages.push(package_from(body)),
205            Record::ProcessorDie(body) => relations.dies.push(package_from(body)),
206            Record::ProcessorModule(body) => relations.modules.push(package_from(body)),
207            Record::Cache(body) => relations.caches.push(cache_from(body)),
208            Record::NumaNode(body) => relations.numa_nodes.push(numa_from(body)),
209            Record::Group(body) => relations.groups.extend(groups_from(body)),
210            Record::Unknown(_) => {}
211        }
212    }
213    relations.anomalies = walk_anomalies;
214    Ok(relations)
215}
216
217#[cfg(test)]
218mod tests;