feat(stabilization)!: thread_aware 1.0 - #643
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a761577-6422-4fd1-a0f7-88807d7f6134
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a761577-6422-4fd1-a0f7-88807d7f6134
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a761577-6422-4fd1-a0f7-88807d7f6134
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #643 +/- ##
=========================================
Coverage 100.0% 100.0%
=========================================
Files 543 550 +7
Lines 60394 61487 +1093
=========================================
+ Hits 60394 61487 +1093
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
✅ Version increments look sufficient
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Make the stable core crate dependency- and feature-free, and remove external-type implementations and their benchmarks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Keep the core crate dependency-free while providing Path, PathBuf, and HashMap implementations behind an opt-in std feature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
|
|
||
| #[doc(inline)] | ||
| pub use crate::core::ThreadAware; | ||
| pub use thread_aware_core::ThreadAware; |
There was a problem hiding this comment.
the _private module should not exist, thread_aware just re-exports the types from thread_aware_core directly
There was a problem hiding this comment.
🤖 Agreed - I’ll remove the private wrapper and re-export the core types directly.
AI response generated by rocket
There was a problem hiding this comment.
🤖 Fixed: removed thread_aware::__private; the trait is now re-exported directly from thread_aware_core at the crate root.
AI response generated by rocket
There was a problem hiding this comment.
imho, this could me moved to thread_aware_core, check if we can hide the constructor on Affinity again if this is moved
There was a problem hiding this comment.
🤖 Agreed - I’ll move this into thread_aware_core and check whether Affinity can hide its constructor again.
AI response generated by rocket
There was a problem hiding this comment.
🤖 Fixed: moved pinned_affinities into thread_aware_core and made thread_aware::affinity a direct re-export. Affinity::new still needs public visibility because thread_aware::registry constructs arbitrary hardware mappings from a sibling crate; hiding it would require moving the registry or adding another public construction backdoor.
AI response generated by rocket
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
| /// create per-core state: they ignore relocation and behave identically | ||
| /// regardless of which core they run on. | ||
| #[derive(Clone, ThreadAware)] | ||
| #[derive(Clone, thread_aware::ThreadAware)] |
There was a problem hiding this comment.
| #[derive(Clone, thread_aware::ThreadAware)] | |
| #[derive(Clone, ThreadAware)] |
(and rever similar changes)
There was a problem hiding this comment.
🤖 Agreed - I’ll restore the derive here and revert the similar manual implementations introduced by this PR.
AI response generated by rocket
There was a problem hiding this comment.
🤖 Fixed: restored unqualified ThreadAware derives and the existing thread_aware re-export imports across downstream crates. The redundant direct thread_aware_core dependencies were removed as well.
AI response generated by rocket
| "thread_aware::core::ThreadAware", | ||
| "thread_aware::affinity::MemoryAffinity", | ||
| "thread_aware::affinity::PinnedAffinity", | ||
| "thread_aware_core::ThreadAware", |
There was a problem hiding this comment.
how about thread_aware_core::Affinity
There was a problem hiding this comment.
🤖 I don’t think thread_aware_core::Affinity should be added here. The external-type check reports only thread_aware_core::ThreadAware for tick’s public API; adding Affinity would create an unused approval rather than cover an exposed type.
AI response generated by rocket
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
| /// Panics if either count is zero or an index is outside its corresponding | ||
| /// count. | ||
| #[must_use] | ||
| pub const fn new(processor_index: u16, memory_region_index: u16, processor_count: u16, memory_region_count: u16) -> Self { |
There was a problem hiding this comment.
Are we sure that these are the four properties we can stabilize? What if we need to further enrich Affinity in the future? Should we have a builder here perhaps?
There was a problem hiding this comment.
🤖 The private fields let us add derived or optional state later without breaking struct construction, and we can add new constructors or a builder additively. The main commitment here is the four-argument Affinity::new contract plus its Copy/equality semantics. I’d keep the current constructor rather than stabilize a builder preemptively, but leave this thread open for the API-design decision.
AI response generated by rocket
There was a problem hiding this comment.
AI got little bit trigger happy, I have disabled automatic responses for humans. Let's discuss this (as humans :D)
There was a problem hiding this comment.
There is also inconsistency in what types are accepted in the construct and how these are exposed through properties (u16 vs usize).
Also another alternative if we are not sure about the constructor:
Leverage _private module and expose a pub-internal new_affinity API that could be consumed in thread_aware.
Full sem-ver guarantees but it's not visible to users anymore.
There was a problem hiding this comment.
Did we agree that _private modules are acceptable? I thought they would generally break with cargo doing updates.
I see two options:
- Make affinities more opaque - don't expose the indices and counts at all, and just have the pinned_affinities function that produces a bunch of opaque affinities.
- Agree that this list of affinity properties makes sense.
I'd lean towards the former in the first iteration, but we'll have to figure out the list of affinity properties at some point.
There was a problem hiding this comment.
Did we agree that _private modules are acceptable? I thought they would generally break with cargo doing updates.
There is nothing special about _private module. Full SemVer guarantees need to be preserved between updates. It is just that we have lower API quality gate for items in _private module. Used as a last resort.
Make affinities more opaque
Making Affinity opaque makes sense to me. Let's see if this is feasible.
AI: Can you checked whether it is safe to make Affinity type opaque?
There was a problem hiding this comment.
[AI Agent]
I checked all workspace consumers. Making construction opaque is safe and straightforward: Affinity::new is only needed by thread_aware_core::pinned_affinities, tests, and ThreadRegistry::affinities; the registry can instead derive per-region counts and consume pinned_affinities, so new can become private.
Making the properties fully opaque is feasible, but not a surgical visibility change. ThreadRegistry::pin_to, PerCore, PerNuma, Storage, and the anyspawn example currently consume the index/count accessors. Rust has no friend-crate visibility, so moving those accessors to a #[doc(hidden)]/_private public API would still leave a SemVer surface. A genuinely opaque design should remove the accessors and refactor consumers to operate on opaque identity/grouping instead—for example, registry-owned affinity-to-processor lookup plus storage keyed/grouped by affinity (potentially with an opaque memory-region identity). That also gives us a place to reject affinities not produced for the registry, rather than trusting a caller-provided processor index.
Recommendation: make Affinity::new private now and expose only pinned_affinities; treat removal of all four accessors as a small coordinated redesign rather than hiding them behind a private-looking public module. No code changes made pending agreement on the grouping API needed by PerNuma.
There was a problem hiding this comment.
Recommendation: make Affinity::new private now and expose only pinned_affinities;
Proceed with this change
There was a problem hiding this comment.
🤖 Fixed in d48b4e88: Affinity::new is now private, and ThreadRegistry constructs its values exclusively through pinned_affinities. Registry processor ordering is grouped by dense NUMA region so affinity indices continue to map correctly when pinning.
AI response generated by rocket
There was a problem hiding this comment.
Also hide the public accessors
| [package] | ||
| name = "thread_aware_core" | ||
| description = "Stable core traits and types for thread-aware state." | ||
| version = "1.0.0" |
There was a problem hiding this comment.
Are we sure we can go straight to 1.0? Should we be a bit more conservative and give ourselves a week or two of 0.1?
There was a problem hiding this comment.
🤖 A short 0.1 soak would reduce release risk, but it would also postpone the stable boundary this PR is explicitly creating. My recommendation is to keep 1.0 only if we consider the trait contract and Affinity semantics settled after this review; otherwise, publish 0.1 first and promote after the soak. I’m leaving this open for the release decision.
AI response generated by rocket
There was a problem hiding this comment.
I would say we can go with alpha, i.e. 1.0.0-alpha.1 (or something in that sense)
There was a problem hiding this comment.
I remember pre-releases causing a bunch of challenges for interoptopus. I'd probably rather just go with 0.1 in the first few weeks.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
| /// runtime that created the value. An `Affinity` is a logical identifier; it does | ||
| /// not pin the current thread or change operating-system scheduling. | ||
| #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] | ||
| pub struct Affinity { |
There was a problem hiding this comment.
review this type and fields it uses and ensure this model aligns well with how NUMA nodes are used/modelled in real processors. Try to find any gaps or potential blockers/breaking changes we would have to make if we stick with this model.
use multi models to research this area thoroughly
There was a problem hiding this comment.
🤖 Understood - I’m starting a multi-model review focused on whether the Affinity representation matches real NUMA and processor topologies, and on API choices that could force future breaking changes.
AI response generated by rocket
There was a problem hiding this comment.
🤖 Multi-model review complete (Opus, Sonnet, and GPT). The reviewers independently converged on these risks:
High-confidence correctness issue
thread_aware/src/registry.rs:98-140usesnuma_nodes.len()asmemory_region_count, but that vector is indexed by the raw OS memory-region ID and may contain holes. For sparse IDs such as{2, 7}, affinities get dense region indices{0, 1}but report a count of8. Track the distinct dense-region count separately, or use a raw-ID-to-dense-index map.
High API and correctness risks
Affinity::newpermits callers to fabricate topology metadata, whileThreadRegistry::pin_totrusts onlyprocessor_index. An affinity from another registry can therefore pin the wrong physical processor or causecurrent_affinity()to report topology the registry never created. Construction should be opaque/fallible, orpin_tomust validate registry/topology identity.processor_indexis a dense ordinal over the registry's selected logical processors, not a physical core index or OS processor ID. Two registries can produce equal affinities for different hardware, while the same hardware can compare unequal when topology counts differ. Equality/hash semantics need an explicit topology identity or a clearly enforced registry-local contract.- The public four-
u16constructor freezes argument order, widths, and count semantics. Theu16inputs versususizeaccessors are also inconsistent. Hiding raw construction behind a documented factory/builder and accepting domain-appropriate typed values would preserve more room before stabilization.
Model scope to decide explicitly
- Real systems also expose socket/package, logical processor versus physical core/SMT sibling, Windows processor group, sparse OS IDs, and heterogeneous-core efficiency class. Private fields and additive accessors can be added later without breaking callers, but changing constructor, equality/hash, or the meaning of
processor_indexcannot. The type should explicitly be either a registry-local locality coordinate or a fuller hardware-topology identity.
Lower-priority fixes
- Use checked addition in
pinned_affinitiesbefore narrowing the processor count. - The registry's
< u16::MAXassertion rejects a count thatu16can represent; use<=if the narrow representation remains.
Recommendation before stabilizing: fix sparse NUMA counting, make construction opaque or topology-validated, define equality/identity semantics, rename/document the logical-processor ordinal precisely, and use the proposed alpha release to validate that contract.
No code changes were made from this review.
AI response generated by rocket
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
|
| /// runtime that created the value. An `Affinity` is a logical identifier; it does | ||
| /// not pin the current thread or change operating-system scheduling. | ||
| #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] | ||
| pub struct Affinity { |
There was a problem hiding this comment.
Let's split the type definitions out into separate files, please - I went looking for affinity.rs and was confused about where the code was. lib.rs and mod.rs should be reserved for structural content to avoid surprises.
| memory_region_count: u16, | ||
| } | ||
|
|
||
| impl Affinity { |
There was a problem hiding this comment.
We run into some API design issues here that are perhaps already embodied in ThreadAware and make it cumbersome to stabilize. Perhaps we need to actually do a proper API evolution and cleanup of ThreadAware APIs themselves before we stabilize anything?
I personally would like to see a design document that lays out the design tenets of ThreadAware and how it is meant to function - it has sort of accumulated stuff and transformed 4 different times over the years and I do not really have a feel for it, it has always been something I stumble through. Perhaps a design document + discussion would be fruitful on this topic.
Anyway, looking at some specifics here.
For one, calling this type Affinity is misleading - the name suggests it implies some data is actually affine to the Affinity but that's not really the case. If you have a thread_aware::Arc<T, PerProcess> then whatever value Affinity has is meaningless. And if you have a thread_aware::Arc<T, PerNuma> then half of it (the core number) is meaningless. The actual decision of "what am I affine to" is made by the thread-aware type itself. Affinity therefore does not signify affinity. This implies, at minimum, that the thing is misnamed. I suggest something like ThreadContext or just Context here - these are descriptors of a specific thread in context of which a relocation is happening.
For another, as a robot in a neighboring thread already noted, the coordinate space of these dimensions is unspecified. Processor index in what range of values? Memory region index in what range of values? Nothing defines that and there is no universal definition we can assume. Any index/count we provide must have a defined coordinate space - what are we counting/indexing exactly? How do we map to sparse "true" coordinate spaces (e.g. a machine processors 1 and 399 only)? pinned_affinities() de-facto partially defines the coordinate space as a dense one without ever actually saying it out loud, creating a lot of confusion here. Today, consumers of pinned_affinities() and Affinities just have to magically align with the undocumented assumptions in this function. Let's document this whole topic.
| /// not pin the current thread or change operating-system scheduling. | ||
| #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] | ||
| pub struct Affinity { | ||
| processor_index: u16, |
There was a problem hiding this comment.
These fields all require values. How can we represent relocation to/from threads that are not pinned to a processor or memory region?
| processor_index: u16, | ||
| memory_region_index: u16, | ||
| processor_count: u16, | ||
| memory_region_count: u16, |
There was a problem hiding this comment.
It is not obvious to me why these are u16 if they are only ever accessed as usize
| /// Panics if there are more than `u16::MAX` processors or memory regions. | ||
| #[must_use] | ||
| #[expect(clippy::needless_range_loop, reason = "clearer in this case")] | ||
| pub fn pinned_affinities(counts: &[usize]) -> alloc::vec::Vec<Affinity> { |
There was a problem hiding this comment.
The probability of needing it is somewhat low but we should have a non-breaking migration path to a universe where we add additional coordinates to threads, such as "cache domains" (e.g. to subdivide a memory region into multiple sub-regions - these already exist today, physically, but we are somewhat behind the curve in modeling hardware realities so we have not used/needed such concepts today).
This assumes that callers who are creating Affinity objects without knowledge of these extended concepts should have a way to continue creating them. A single free function with a fixed behavior feels slightly strange, as we would have to add a second function alongside to provide support for such concepts. A builder seems more generalizable.
| /// remain correct when relocation is not reported, is reported more than once, or | ||
| /// uses equal source and destination affinities. | ||
| /// | ||
| /// # Example |
There was a problem hiding this comment.
This example does not really seem to show off a meaningful implementation - it is an API dummy with no useful behavior. A more useful example would showcase how the function is used to achieve some realistic goal (i..e. specializing behavior for some purpose). We just made some claims about performance and contention-avoidance mechanisms above but only showcase how to set the value of a field.
|
|
||
| /// Marks state that can adapt after being transferred between affinities. | ||
| /// | ||
| /// Implementations commonly recreate affinity-local resources, select a |
There was a problem hiding this comment.
Not sure what this paragraph means.
| pub trait ThreadAware: Send { | ||
| /// Adapts this value in place for the destination affinity. | ||
| /// | ||
| /// `source` is `None` when the value's previous affinity is unknown. Callers |
There was a problem hiding this comment.
It also seems worth mentioning whether implementations are expected to tolerate relocations across unrelated affinities (e.g. relocation 1 to "processor 5 of 10" and relocation 2 to "processor 19 of 2000"). I make no comment on what the expectation should be, merely that it is a question that implementors will have, so we should answer here.
| pub struct Affinity { | ||
| processor_index: u16, | ||
| memory_region_index: u16, | ||
| processor_count: u16, |
There was a problem hiding this comment.
IMO counts should be NonZero<T> to avoid completely unnecessary zero-checking path.
Explore an opaque, extensible redesign of the affinity descriptor: a Dimension-keyed LocalityId identity built through Affinity::builder, replacing the frozen four-number processor/memory-region shape. Unset dimensions are absent, and LocalityId is an opaque Copy+Eq+Hash key created from a u16 index or a static name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
…conflict) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
Summary
thread_aware_core1.0.0 with the stableThreadAwaretrait,Affinitytype, andpinned_affinitiestopology helper.no_std; an opt-instdfeature adds standard-library implementations forHashMap,Path, andPathBuf.thread_aware, preservingthread_aware::ThreadAwareandthread_aware::affinity::{Affinity, pinned_affinities}for downstream users.Validation
thread_aware_coreandthread_awaretests and doctests pass with all applicable features.thread_aware_corepackages and verifies successfully with zero dependencies.