Skip to main content

MachineMemoryTopology

Struct MachineMemoryTopology 

Source
pub struct MachineMemoryTopology {
    pub processors: Vec<Processor>,
    pub domains: Vec<Domain>,
    pub cpu_sets: Option<Vec<CpuSet>>,
    pub processor_attributes: Vec<AttributeObservation>,
    pub provenance: Provenance,
    pub enumeration_anomalies: Vec<EnumerationAnomaly>,
    pub coherence: Coherence,
}
Expand description

A processor, cache, and memory topology: a set of processors and the domains that relate them.

Built either by MachineMemoryTopology::discover from the running system, by hand, or (with the serde feature) by deserializing a fed-in description.

The JSON shape this produces and accepts is explicitly not covered by this crate’s semver contract – see Domain’s documentation and D-8 in DESIGN-NOTES.md.

Fields§

§processors: Vec<Processor>

Every logical processor, including one for each inactive slot up to a group’s maximum processor count.

As the relationship walk reported them. A processor that only the CPU-set enumeration saw is not here, and that is a real omission rather than an oversight: this list has one source, and inventing an entry for a processor the walk never described would put a Processor here with no core, no class and no domain membership – a fabricated record of exactly the kind Observed exists to prevent.

The disagreement is not lost, it is reported: Coherence::Disagreed names such processors in its cpu_sets_only field. A caller that needs them can read them there, and one that ignores coherence sees the walk’s view, which is what this field has always been.

Reaching that state takes a disagreement surviving every pass of Self::discover’s retry, so it is not the ordinary case – but the ordinary case is not what a topology crate is for. See D-26.

§domains: Vec<Domain>

Every domain.

§cpu_sets: Option<Vec<CpuSet>>

What GetSystemCpuSetInformation reported, as its own observation. /// Windows describes processors through two APIs, and this is the second one. It is not a more convenient spelling of Self::domains: it carries facts the relationship walk has no equivalent for – whether a processor is parked, whether it is allocated to this process, the scheduler’s own last-level-cache grouping, a scheduling class and an allocation tag.

It also duplicates some facts, deliberately and without reconciliation. CoreIndex, NumaNodeIndex and EfficiencyClass also appear, derived differently, in domains and Self::processors. The two paths can disagree – under a hypervisor, or where one is stale – and merging them here would silently pick a winner and destroy the disagreement, which is the only thing a second observer is for. Which of them a consumer should believe, and what to do when they differ, is a decision that has not been taken.

None means not observed, which is not the same as observed-and- empty: a hand-built or deserialized topology has not asked the running system, and a consumer must be able to tell that from a machine that genuinely reported nothing.

§processor_attributes: Vec<AttributeObservation>

What each source said about each processor’s attributes.

D-18’s second subject kind, and the one relation unification cannot reach: two sources describing the same core agree on (kind, membership) even when they disagree about a number hanging off it, so that disagreement has nowhere to live among the relation’s own observations.

Never reduced, for the same reason relations keep both observations: collapsing them would destroy the disagreement, which is the only thing a second observer is for. Use Self::attribute_conflicts to find the subjects the sources did not agree on.

Empty for a hand-built or deserialized topology – nobody asked, which is Observed::NotObserved’s meaning carried up to a collection.

§provenance: Provenance

Where this content came from.

Defaults to Provenance::Synthetic, so a topology built by hand or by Default is tainted unless its author says otherwise. Only Self::discover produces Provenance::Measured, and deserialization can never produce it – see Provenance for why the default points this way.

§enumeration_anomalies: Vec<EnumerationAnomaly>

Records the enumeration could not decode, if any.

Empty on every healthy machine, and empty for a hand-built or deserialized topology, which asked nothing. A non-empty list means a buffer Windows returned did not describe what it claimed to – a defective hypervisor, a driver corrupting memory – and it carries the byte offset and the reason so that is diagnosable from the data rather than from a debugger.

Per D-24 this is recorded rather than thrown: a malformed record is not evidence that this crate reached an inconsistent state, so it must not panic, and dropping it silently would leave a consumer unable to tell a truncated enumeration from a small machine. Whatever decoded before the anomaly is still present in the fields above and is still correct.

Written out, never read back in, for exactly the reason Self::coherence is not: an anomaly is a fact about an enumeration, and a deserialized topology performed none. A description is welcome to carry these so a human reading a dump can see how the run that produced it went, but a file cannot establish that a buffer walk hit a malformed record any more than it can establish that the walk observed anything at all (D-12).

This was serde(default) until 2026-09-04, which let a document assert anomalies into a topology that had asked nothing – contradicting the first paragraph of this very comment, which has always said the list is empty for a deserialized topology.

§coherence: Coherence

Whether the two Win32 sources described the same machine when this was collected.

Stated rather than implied, per D-16: a reader cannot infer from the data’s shape how far its parts may be correlated with each other, and that is a different question from whether any single part is accurate.

Written out, never read back in. A description is welcome to carry this so a human reading a dump can see how the run that produced it went, but a file cannot establish that two enumerations agreed – exactly as it cannot establish that the relationship walk observed anything (D-12). So deserialization always yields Coherence::NotCollected, which is the true answer for a topology nobody collected, and a description claiming Agreed gains nothing by saying so.

Implementations§

Source§

impl MachineMemoryTopology

Source

pub fn machine_processors(&self) -> ProcessorSet

Every processor this topology knows, online or not.

The membership of Granularity::Machine. Offline processors are included on purpose: a slot that exists is still part of the machine, and excluding it would make a query naming it answer “nothing shared” rather than “the machine”, which is the empty-case branch the top exists to remove.

Source

pub fn minimal_shared(&self, of: &ProcessorSet) -> Vec<Granularity<'_>>

The minimal granularities that cover every processor in of – the tightest things they all share.

§The answer is a set, not one element

Inclusion is a partial order, so two granularities can be incomparable: neither contains the other, and both are therefore minimal. It is almost always one element, but not by construction, and a caller that takes the first must say that is what it is doing.

Equal memberships are the ordinary case of this. Measured on the development host, L1 arrives as a data cache and an instruction cache over the very same processors – two relations, distinct in kind and attributes, tied in the order. Both are returned, because picking one would be arbitrary.

§When the answer is the machine

Granularity::Machine is returned exactly when no reported relation covers of. It is the fallback that makes this query total rather than an element that competes with observed relations, so it never appears alongside one.

§When the answer is empty

Totality holds over processors this topology knows. A set naming a processor it does not know is answered with an empty result rather than with the machine, because claiming the machine contains a processor it has never heard of would be an invention.

Source

pub fn proximity(&self, processors: &[ProcessorId]) -> Proximity<'_>

What a set of processors shares, and whether the answer is complete.

The pairwise proximity query, generalised to any set because nothing in it is specific to two: proximity(&[a, b]) is the pair case, and the same call sizes an MPSC fan-in over a whole block.

§It is a helper over the order, not the primary surface

The body is Self::minimal_shared plus the coverage check below. That is deliberate: the collection is what a caller reaching for structure should use, because rebuilding the grouping from repeated pairwise calls takes union-find over O(n^2) questions – and that reconstruction is exactly what SH-16.9 records three consumers doing, two of them differently. Providing the helper here means there is one implementation of the grouping rather than one per caller.

§Errors

None – the query is total over processors this topology knows, and answers Granularity::Machine rather than “nothing” for a pair no relation covers. A processor it does not know yields an empty Proximity::shared, because claiming the machine contains a processor it has never heard of would be an invention.

Source

pub fn is_finer_than( &self, finer: Granularity<'_>, coarser: Granularity<'_>, ) -> bool

Whether finer sits strictly inside coarser in the granularity order.

The order’s comparison, exposed so a caller can position an answer against another without re-deriving what “tighter” means – the re-derivation this model exists to stop.

Source§

impl MachineMemoryTopology

Source

pub fn discover() -> Result<Self>

Discover the running system’s topology.

§Errors

Returns any error from the underlying GetLogicalProcessorInformationEx or GetSystemCpuSetInformation calls.

Source

pub fn attribute_conflicts(&self) -> Vec<(ProcessorId, ProcessorAttribute)>

The (processor, attribute) subjects the sources did not agree on.

Empty is the ordinary answer. A non-empty one is the attribute conflict D-17 says to expect in the field and to record rather than refuse over – firmware tables are populated incrementally, and the places to meet this are hardware nobody here has.

Reported rather than resolved: picking a winner would destroy the disagreement, and on a hybrid part the choice decides whether a processor is treated as a performance or an efficiency core.

Source

pub fn processor(&self, id: ProcessorId) -> Option<&Processor>

The processor named by id, if this topology has one.

Source

pub fn groups(&self) -> impl Iterator<Item = &Domain>

Every domain of kind DomainKind::Group.

Source

pub fn packages(&self) -> impl Iterator<Item = &Domain>

Every domain of kind DomainKind::Package.

Source

pub fn cores(&self) -> impl Iterator<Item = &Domain>

Every domain of kind DomainKind::Core.

Source

pub fn caches(&self) -> impl Iterator<Item = &Domain>

Every cache domain, at any level.

Source

pub fn caches_at_level(&self, level: u8) -> impl Iterator<Item = &Domain>

Every cache domain at exactly level.

Source

pub fn cache_levels(&self) -> Vec<u8> ⓘ

Every cache level this machine reports, ascending, without repeats.

Derived from what the topology actually contains rather than from a fixed ceiling. DomainKind::Cache’s level is a u8, so a caller that sweeps a hard-coded 1..=4 silently reports a partitioning L5 as absent – a wrong answer that looks like a confident one.

Source

pub fn cache_partitions_at_level(&self, level: u8) -> Vec<&Domain>

The distinct processor partitions the caches at level form.

§Why this is not Self::caches_at_level

Windows reports one relationship per cache, not per partition, and a level is routinely reported more than once over the very same processors. Measured on the eight-core development host rather than reasoned about: L1 arrives as eight data domains plus eight instruction domains covering exactly the same eight processor pairs.

Counting relationships therefore claims sixteen L1 partitions where the machine has eight. Two consequences, both silent: a partition count that is a whole multiple too large, and – on a machine whose L1i and L1d are its only two cache domains – a level reported as partitioning when it divides nothing at all.

Deduplication is by processor set, which is the thing a caller partitioning work actually cares about; the first domain covering each distinct set is kept, so the returned ids are stable for a topology.

Distinct is not disjoint. Two sets that overlap without being equal both survive this, so the result is a set of domains rather than a proven partition. Self::outermost_partitioning_cache is where that stronger property is required and checked.

A domain covering no processors is dropped, because it partitions nothing and every consumer of this list wants pieces of the machine. It would otherwise survive both filters here and in Self::outermost_partitioning_cache: it is not equal to any non-empty set, so deduplication keeps it, and it is disjoint from everything vacuously, so the pairwise check passes it. A level with one real cache plus one empty domain would then count two partitions and be reported as dividing a machine it does not divide. Contrast Self::memory_domains, which deliberately keeps a processor-less domain because a memory domain with no CPUs is real hardware (D-5); a cache over no processors is not.

Source

pub fn outermost_partitioning_cache(&self) -> Option<(u8, Vec<&Domain>)>

The outermost cache level that actually divides this machine, together with the distinct partitions it forms.

A level whose caches all cover the same processors partitions nothing – a fully shared L3 is one domain spanning everything – so it is never a candidate however far out it sits. None means no reported level divides the machine, which is a real answer and not a failure: a single-core host is one partition by every measure.

This is deliberately not “level 3”. A shipping ARM64 laptop measured during the 2026-08-30 session reports no L3 at all, with two L2 domains of six processors forming the real cluster boundary.

§A level must be a partition, not merely a set of domains

Self::cache_partitions_at_level deduplicates by equal processor set, which is what the measured case needs (L1i and L1d cover identical sets). That alone does not make the result a partition: two distinct sets can still overlap. Real hardware does not do this, but a MachineMemoryTopology is deliberately constructible by hand and by deserialization (see Provenance), so this method cannot assume hardware produced it – and a caller splitting work across overlapping “partitions” double-counts the processors in the intersection and overwrites their domain assignment, silently.

So a level qualifies only when its distinct sets are pairwise disjoint. Full coverage of the online processors is deliberately not required: a processor with no cache reported at a level is a gap in what the firmware said, not evidence that the domains which were reported overlap, and rejecting the level would discard a true boundary over it.

§“Outermost” is decided by inclusion, not by the level number

Level groups the relations into candidate partitions – that is reading what the source said. It does not order them, because “a higher number is coarser” is asserted structure of the kind M2+.2 forbids, and a machine that reports L2 over six processors and no L3 at all is the counterexample this crate has actually measured.

A candidate is coarser than another when every block of the other is contained in one of its blocks, which is checkable against the very memberships Windows reported. On ordinary hardware the two agree; where they disagree, the firmware numbering is the one that is wrong.

Defined here, in the crate that owns the topology, so that every consumer asks the same question rather than restating the rule and drifting from it.

Source

pub fn shard_set(&self) -> Vec<ProcessorFacts<'_>>

Everything this crate knows about one processor, without sentinels.

The shard-set surface M4+.2 calls for: what a caller needs to decide whether a processor may host work, gathered so the answer is asked for once rather than assembled differently by each consumer.

Every field says which absence it means (D-13), and none uses a sentinel. That is the point rather than a nicety: Processor::capacity spells “offline”, “in no core”, and “efficiency class zero” as the same 0, and the third is every processor on every non-hybrid machine – so the stand-in collides with the overwhelmingly common real value and no careful caller can tell them apart.

Source

pub fn memory_domain_of(&self, processor: ProcessorId) -> Observed<&Domain>

Which memory domain processor allocates from.

Observed::Known with the domain, or Observed::NotObserved when no memory domain names it – the unplaced case, which is deliberately not collapsed into “node 0”.

§Why the unplaced case gets its own answer

An unknown cache domain costs an optimisation. An unknown memory domain has no honest fallback at all: the pool has to be allocated somewhere, and guessing means quietly allocating remote memory for the life of the process. windows-placement-probe already encodes this asymmetry – it refuses a missing NUMA node while tolerating a missing cache domain – and this method is where that distinction becomes the model’s rather than each consumer’s.

Observed::Absent is never returned: a memory domain covering no processors is a real shape (D-5), but “this processor belongs to no node” is a gap in what the firmware said, not a positive statement that it has no memory.

Source

pub fn unplaced_processors(&self) -> Vec<ProcessorId>

Every processor no memory domain names.

The set a caller must decide about before allocating anything, and empty on a machine whose firmware covered every processor. Offered so the question is asked once rather than rediscovered per allocation site.

Source

pub fn memory_domains(&self) -> impl Iterator<Item = &Domain>

Every memory domain, including one with no processors (D-5).

Trait Implementations§

Source§

impl Clone for MachineMemoryTopology

Source§

fn clone(&self) -> MachineMemoryTopology

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for MachineMemoryTopology

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for MachineMemoryTopology

Source§

fn default() -> MachineMemoryTopology

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for MachineMemoryTopology

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl PartialEq for MachineMemoryTopology

Source§

fn eq(&self, other: &MachineMemoryTopology) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for MachineMemoryTopology

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for MachineMemoryTopology

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.