diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f6f09956..7189a00b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Please see each crate's change log below: - [`templated_uri_macros`](./crates/templated_uri_macros/CHANGELOG.md) - [`templated_uri_macros_impl`](./crates/templated_uri_macros_impl/CHANGELOG.md) - [`thread_aware`](./crates/thread_aware/CHANGELOG.md) +- [`thread_aware_core`](./crates/thread_aware_core/CHANGELOG.md) - [`thread_aware_macros`](./crates/thread_aware_macros/CHANGELOG.md) - [`thread_aware_macros_impl`](./crates/thread_aware_macros_impl/CHANGELOG.md) - [`tick`](./crates/tick/CHANGELOG.md) diff --git a/Cargo.lock b/Cargo.lock index 694a37d85..998fd851f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -152,7 +152,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1499,7 +1499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3066,7 +3066,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3374,7 +3374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4264,7 +4264,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4320,7 +4320,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4676,7 +4676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4803,7 +4803,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4928,6 +4928,13 @@ dependencies = [ "thread_aware", ] +[[package]] +name = "thread_aware_core" +version = "0.1.0" +dependencies = [ + "static_assertions", +] + [[package]] name = "thread_aware_macros" version = "0.11.0" @@ -5655,7 +5662,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 33f02cb4a..7f50a1174 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -231,6 +231,7 @@ templated_uri_macros_impl = { path = "crates/templated_uri_macros_impl", default testing_aids = { path = "crates/testing_aids", default-features = false } thiserror = { version = "2.0.17", default-features = false } thread_aware = { path = "crates/thread_aware", default-features = false, version = "0.11.0" } +thread_aware_core = { path = "crates/thread_aware_core", default-features = false, version = "0.1.0" } thread_aware_macros = { path = "crates/thread_aware_macros", default-features = false, version = "0.11.0" } thread_aware_macros_impl = { path = "crates/thread_aware_macros_impl", default-features = false, version = "0.11.0" } thread_local = { version = "1.1.8", default-features = false } diff --git a/README.md b/README.md index 215727e71..6040fcf79 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ These are the primary crates built out of this repo: - [`seatbelt_http`](./crates/seatbelt_http/README.md) - HTTP-specific extensions for the seatbelt crate. - [`templated_uri`](./crates/templated_uri/README.md) - Standards-compliant URI handling with templating, safety validation, and data classification - [`thread_aware`](./crates/thread_aware/README.md) - Facilities to support thread-isolated state. +- [`thread_aware_core`](./crates/thread_aware_core/README.md) - Stable core traits and types for thread-aware state. - [`tick`](./crates/tick/README.md) - Provides primitives to interact with and manipulate machine time. - [`uniflight`](./crates/uniflight/README.md) - Coalesces duplicate async tasks into a single execution. diff --git a/crates/thread_aware_core/CHANGELOG.md b/crates/thread_aware_core/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/crates/thread_aware_core/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/crates/thread_aware_core/Cargo.toml b/crates/thread_aware_core/Cargo.toml new file mode 100644 index 000000000..7bcfc6194 --- /dev/null +++ b/crates/thread_aware_core/Cargo.toml @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[package] +name = "thread_aware_core" +description = "Stable core traits and types for thread-aware state." +version = "0.1.0" +readme = "README.md" +keywords = ["oxidizer", "thread", "aware"] +categories = ["data-structures", "no-std"] + +edition = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +include = { workspace = true } +repository = "https://github.com/microsoft/oxidizer/tree/main/crates/thread_aware_core" + +[package.metadata.docs.rs] +all-features = true + +[features] +default = ["std"] +std = [] + +[dev-dependencies] +static_assertions = { workspace = true } + +[[example]] +name = "relocation" +required-features = ["std"] + +# >>> anvil-managed: anvil-lints +[lints] +workspace = true +# <<< anvil-managed: anvil-lints diff --git a/crates/thread_aware_core/README.md b/crates/thread_aware_core/README.md new file mode 100644 index 000000000..154bcbaf0 --- /dev/null +++ b/crates/thread_aware_core/README.md @@ -0,0 +1,254 @@ +
+ Thread Aware Core Logo + +# Thread Aware Core + +[![crate.io](https://img.shields.io/crates/v/thread_aware_core.svg)](https://crates.io/crates/thread_aware_core) +[![docs.rs](https://docs.rs/thread_aware_core/badge.svg)](https://docs.rs/thread_aware_core) +[![MSRV](https://img.shields.io/crates/msrv/thread_aware_core)](https://crates.io/crates/thread_aware_core) +[![CI](https://github.com/microsoft/oxidizer/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/microsoft/oxidizer/actions/workflows/main.yml) +[![Coverage](https://codecov.io/gh/microsoft/oxidizer/graph/badge.svg?token=FCUG0EL5TI)](https://codecov.io/gh/microsoft/oxidizer) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/microsoft/oxidizer/blob/main/LICENSE) +This crate was developed as part of the Oxidizer project + +
+ +Support for values that adapt when a runtime moves them to another thread. + +This crate contains the small API shared by thread-aware libraries: + +* [`ThreadAware`][__link0] notifies a value that it has moved. +* [`Thread`][__link1] records where it now runs: which runtime, which OS thread, and which memory is + closest to it. + +[`Thread`][__link2] is a coordinate, not a handle: a runtime builds one to describe where a value +is running, and it owns no operating-system resource. It is unrelated to +[`std::thread::Thread`][__link3], which is a handle to a live OS thread. Naming both in one module +requires aliasing one of them. + +## The `thread_aware` family + +* **`thread_aware_core`** (this crate) — the vocabulary that two unrelated libraries must + agree on before either can relocate a value defined by the other. Deliberately small and + slow-moving, so naming [`ThreadAware`][__link4] or [`Thread`][__link5] in your own public API costs you + nothing later. +* **[`thread_aware`][__link6]** — the utilities that make relocation convenient: a + [`#[derive(ThreadAware)]`][__link7] macro, wrappers for foreign types, a per-core + [`Arc`][__link8], containers and registries. Free to evolve, and not meant to appear in a + public API. + +Depend on this crate directly when all you need is the trait. It adds nothing to your +dependency graph, and works without `std`: with default features turned off, [`Thread`][__link9] +loses its thread id component and keeps [`Owner`][__link10] and [`NumaNode`][__link11]. + +## Why relocation exists + +Thread-per-core and NUMA-aware runtimes are fast because each worker keeps to itself: it +uses memory close to its own thread, talks to its own I/O driver, and does not +synchronize with other workers. When a value moves to another worker, what used to be +close by is now in the wrong place: a cache line shared between threads, memory in a +distant region, a handle to another thread’s driver. + +[`ThreadAware`][__link12] lets that state repair itself. The runtime moves the value, then calls +[`relocate`][__link13] to report where it now lives. Relocation has two +sides, and most code sits on only one of them. + +## Library authors: implementing the trait + +**Library and application authors** implement [`ThreadAware`][__link14], usually through the +[`#[derive(ThreadAware)]`][__link15] macro. They never call +[`relocate`][__link16] and never construct a [`Thread`][__link17]; the runtime does +both and then invokes the implementation. It is a callback, like [`Drop::drop`][__link18]. + +The derive lives in [`thread_aware`][__link19], so a library that wants it depends on that crate. +Only the trait and [`Thread`][__link20] cross the public boundary, and both come from here, so the +dependency stays an implementation detail: + +```rust +// A build dependency, not part of what this library promises. +use thread_aware::ThreadAware; + +/// A codec whose scratch buffer should follow the memory it is used from. +#[derive(ThreadAware)] +pub struct Encoder { + scratch: Scratch, + dictionary: Dictionary, +} +``` + +The derive writes the forwarding implementation, calling `relocate` on `scratch` and +`dictionary` in turn. Because a composed type forwards to its fields, one call at the top +reaches everything below it. Callers of `Encoder` never name [`thread_aware`][__link21]. + +## Runtime authors: driving relocation + +**Runtime authors** construct a [`Thread`][__link22] per worker and call +[`relocate`][__link23] after moving a value, passing where it came from and +where it now runs. The example below plays the part of the runtime so the order is +visible. + +```rust +use std::thread; + +use thread_aware_core::{NumaNode, Owner, Thread, ThreadAware}; + +// What a library author writes. +struct Worker { + thread: Option, +} + +impl ThreadAware for Worker { + fn relocate(&mut self, _source: Option<&Thread>, destination: &Thread) { + self.thread = Some(destination.id()); + } +} + +// What the runtime does. +let here = thread::current().id(); +let there = thread::spawn(|| thread::current().id()).join().unwrap(); + +let owner = Owner::new(2); +let first = Thread::new(owner.clone(), here, NumaNode::new(0)); +let second = Thread::new(owner, there, NumaNode::new(1)); + +let mut worker = Worker { thread: None }; + +worker.relocate(None, &first); // first placement, no previous `Thread` +worker.relocate(Some(&first), &second); // moved to another thread + +assert_eq!(worker.thread, Some(there)); +``` + +## Performance, not correctness + +Relocation is an optimization, not a guarantee. A value must remain correct if the call +never comes, comes twice, or reports the same source and destination. Missing calls are +normal: a value can reach another thread through `std::thread::spawn`, a channel, or a +runtime that knows nothing about this trait. That may make things slower, but it must +never cause a panic, a deadlock, or a wrong answer. + +Nor is it a hot path. Expect roughly one relocation per object graph per job or request, +after which the value is used normally. Avoiding synchronization matters more than saving a +few cycles. + +## What the ids mean + +* **Thread id** — a `std::thread::ThreadId`, unique among the threads alive at once, so + state keyed on it is never shared by accident, not even between two runtimes in one + process. +* **[`NumaNode`][__link24]** — the memory closest to that thread. Unlike the thread id it is + *shared*: every thread near the same memory reports the same node, which is what suits + it to state shared within a region but not across the machine. That holds only while + every runtime numbers the regions identically. Nothing checks it, and runtimes that + disagree make shared state wrong rather than merely slow. +* **[`Owner`][__link25]** — the runtime a [`Thread`][__link26] belongs to. Every new owner is unique, so two + live runtimes never share one. It lets a value detect that it has crossed into a + different runtime and release anything the previous one owned. + +An implementation reads only the ids its state depends on. A per-thread cache or a handle +to a thread-local driver keys on the thread id; a buffer pool keys on [`NumaNode`][__link27] and +survives a move to another thread near the same memory; anything the runtime owns compares +[`Owner`][__link28]. + +The ids carry no meaning beyond identity: they need not start at zero or run +consecutively, and the [`Thread`][__link29]s in use cannot be enumerated. State keyed on any of them +belongs in a map rather than an array indexed by it. [`Owner::min_threads`][__link30] is the one +number on offer, and it is a floor to pre-size against, not a bound to index against. + +Without `std` there is no [`ThreadId`][__link31]: `Thread::new` and +`Thread::id` are absent and only [`Owner`][__link32] and [`NumaNode`][__link33] remain. A `no_std` library can +still implement [`ThreadAware`][__link34] and use whatever it is given; the runtime that drives +relocation requires `std` regardless. + +## Relation to `Send` + +[`ThreadAware`][__link35] requires [`Send`][__link36], and in that order: a value is sent to another thread +first, then told where it landed. [`Send`][__link37] is what makes the move safe, and +[`ThreadAware`][__link38] adds nothing to it. + +## Provided implementations + +Types with nothing tied to a thread receive an empty implementation: primitives and their +non-zero variants, the thread ids, `Duration`, strings, safe function pointers of up to +twelve parameters, and, with the `std` feature, paths. + +Containers forward the call to what they hold: [`Option`][__link39], [`Result`][__link40], arrays, slices, +`Vec`, `VecDeque`, `Box`, cells, tuples of up to twelve elements, and map values. + +References are not [`ThreadAware`][__link41]. Relocating through one would adapt something the value +only borrows, and whoever owns it is relocated on its own account. + +Map keys are left alone, since altering one could change its hash or ordering and corrupt +the map. Sets are not implemented at all for the same reason, so a `HashSet` or +`BTreeSet` field is not [`ThreadAware`][__link42]. + +`Cow` is omitted for now: relocating a borrowed one has to clone it into owned storage +first, which is a surprising amount of work to hide behind a hint. + +`Arc` is also omitted: whether a shared allocation should stay shared across threads or +be split per thread depends on what it holds. The per-core [`Arc`][__link43] in +[`thread_aware`][__link44] covers the case where splitting is correct. + +## Features + +* **`std`** *(default)* - Adds [`Thread::new`][__link45] and [`Thread::id`][__link46], which need + [`ThreadId`][__link47], and implements [`ThreadAware`][__link48] for standard library + types such as `HashMap`, `Path` and `PathBuf`. Turn it off for `no_std`, which needs + only `alloc` and pointer-width atomics. + + +
+ +This crate was developed as part of The Oxidizer Project. Browse this crate's source code. + + + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQb11VxC_uAPOQbtUn4Wx2-BfAbid3Nt1Y27Pobprn8Z6FjFy9hYvRhcoQbHZWlC1ydpTAb-CfpCfELSs4bQbTDXMAtkA8bpQ3Y3OBUQHFhZIGCcXRocmVhZF9hd2FyZV9jb3JlZTAuMS4w + [__link0]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware + [__link1]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread + [__link10]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Owner + [__link11]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=NumaNode + [__link12]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware + [__link13]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware::relocate + [__link14]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware + [__link15]: https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html + [__link16]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware::relocate + [__link17]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread + [__link18]: https://doc.rust-lang.org/stable/std/?search=ops::Drop::drop + [__link19]: https://docs.rs/thread_aware + [__link2]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread + [__link20]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread + [__link21]: https://docs.rs/thread_aware + [__link22]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread + [__link23]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware::relocate + [__link24]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=NumaNode + [__link25]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Owner + [__link26]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread + [__link27]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=NumaNode + [__link28]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Owner + [__link29]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread + [__link3]: https://doc.rust-lang.org/stable/std/?search=thread::Thread + [__link30]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Owner::min_threads + [__link31]: https://doc.rust-lang.org/stable/std/?search=thread::ThreadId + [__link32]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Owner + [__link33]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=NumaNode + [__link34]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware + [__link35]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware + [__link36]: https://doc.rust-lang.org/stable/std/marker/trait.Send.html + [__link37]: https://doc.rust-lang.org/stable/std/marker/trait.Send.html + [__link38]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware + [__link39]: https://doc.rust-lang.org/stable/std/option/enum.Option.html + [__link4]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware + [__link40]: https://doc.rust-lang.org/stable/std/result/struct.Result.html + [__link41]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware + [__link42]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware + [__link43]: https://docs.rs/thread_aware/latest/thread_aware/struct.Arc.html + [__link44]: https://docs.rs/thread_aware + [__link45]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread::new + [__link46]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread::id + [__link47]: https://doc.rust-lang.org/stable/std/?search=thread::ThreadId + [__link48]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware + [__link5]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread + [__link6]: https://docs.rs/thread_aware + [__link7]: https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html + [__link8]: https://docs.rs/thread_aware/latest/thread_aware/struct.Arc.html + [__link9]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread diff --git a/crates/thread_aware_core/docs/DESIGN.md b/crates/thread_aware_core/docs/DESIGN.md new file mode 100644 index 000000000..09ea58c06 --- /dev/null +++ b/crates/thread_aware_core/docs/DESIGN.md @@ -0,0 +1,175 @@ +# Thread Aware Core — Design + +## What this crate is + +`thread_aware_core` is the **stable vocabulary** for values that adapt when a +runtime moves them between threads. It exists to be depended on permanently: +crates are meant to name `ThreadAware` and `Thread` in their own public +signatures, and this crate's job is to make sure that never becomes a liability. + +It exposes one trait, one identifier and its two component ids, and nothing +else: + +- `ThreadAware` — the callback a value implements to be told it has moved. +- `Thread` — where a value runs, built from `Owner`, a `std::thread::ThreadId`, + and `NumaNode`. + +Everything that makes relocation *convenient* — registries, containers, +callbacks, derive macros, runtime integration — lives in the pre-1.0 +`thread_aware` crate. This crate carries only what two unrelated libraries must +agree on in order to interoperate. Implementations of `ThreadAware` for +`core`, `alloc` and `std` types ship here because they are part of that shared +vocabulary; implementations for third-party types deliberately do not. + +## Why a separate crate + +A vocabulary trait is only useful if everyone names the same one. If a +connection pool and a codec each define their own `ThreadAware`, a runtime that +relocates a value containing both has two unrelated traits to satisfy and no way +to treat them uniformly. The trait must come from a single crate they share. + +That makes this crate's compatibility a shared constraint rather than a private +matter. Two libraries interoperate only when their public APIs resolve to the +same `thread_aware_core` instance, so an incompatible release does not +inconvenience one crate — it splits the ecosystem until every participant has +upgraded. A crate in that position has to be *boring*: small, dependency-free, +and stable far longer than the code around it. The MSRV is part of the same +bargain, and is raised deliberately rather than incidentally. + +Splitting it out is what buys that. The volatile parts of `thread_aware` keep +iterating pre-1.0 while the few items that appear in public signatures stabilise +ahead of them. + +## Scope + +`Owner` describes a runtime; `NumaNode` describes hardware. Neither is +validated, and their guarantees differ in ways implementations depend on. A +thread id is meaningful only while that thread lives, an `Owner` only while its +runtime does, and a `NumaNode` is shared by every thread near the same memory — +including threads of a different runtime, but only while all of them number the +nodes identically. + +`Owner` values are issued by runtime and integration code, never by ordinary +data-structure authors. Every new owner is unique, so two runtimes alive at once +cannot collide however carelessly they are set up. An owner also reports the +smallest number of threads its runtime runs: a floor to pre-size against, which +the runtime may exceed, and which is zero for one that spawns on demand. + +Implementing `ThreadAware` also commits a type to `Send`, since that is a +supertrait. A type holding an `Rc` or another thread-bound handle cannot +participate at all, even with an empty `relocate`. + +## Principles + +**Performance, not correctness.** `relocate` is advisory. A value must be fully +correct if it is never called, called twice, or called with a `Thread` it has +never seen. This is the most important property in the design: it is what lets a +runtime call `relocate` opportunistically, lets an implementation give up and +stay slow rather than fail, and lets the method be infallible. A trait whose +correctness depended on being called would need error reporting, ordering +guarantees, and a way to refuse a move — none of which this crate has. + +**Say only what interoperability requires.** Every item here is a permanent +commitment, so the bar for adding one is whether two independent crates must +agree on it. A helper that merely makes implementation pleasant does not +qualify; it belongs in `thread_aware`, where it can still change. + +**Borrow from `std` rather than invent.** The thread coordinate is +`std::thread::ThreadId`, not an id of our own: a bespoke type would be one more +thing to convert to and from, and one more thing to keep stable forever. The +cost is that `Thread::new` and `Thread::id` need the `std` feature. With +default features off, a `no_std` crate can still implement `ThreadAware` and +read `Owner` and `NumaNode` from the `Thread` values it is handed; it just cannot +construct one, which only runtimes need to do. Such a build needs `alloc` and +pointer-width atomics, the latter because `Owner` identities come from a +process-wide counter. + +**No dependencies reach a consumer.** The manifest's only entry is a test-only +dev-dependency, so adopting this crate cannot introduce a version conflict or +pull anything unexpected into a build. + +**Describe, do not enforce.** Nothing checks that runtimes number NUMA nodes +alike or that implementations avoid blocking. These are documented +obligations; enforcing them would need runtime state and a coordination point, +and this crate has neither. + +## Evolving without breaking changes + +The crate has to be able to describe hardware it does not model yet. Machines +already subdivide a NUMA node into cache domains, and a `Thread` may one day need +to describe a value pinned to memory but not to a processor. Both mean +adding a coordinate, and the design makes that additive. + +**Ids are opaque, and take no `From`.** `NumaNode` exposes no accessor +returning its integer, and both ids are built through inherent constructors +rather than a `From` impl. That is a deliberate refusal, because a +`From` on a permanent vocabulary type is a one-shot commitment. While +one such impl exists it silently governs the width every unsuffixed literal +infers; adding a second either breaks those call sites — `from(1)` falls back to +`i32` and stops compiling — or, when the added impl happens to be `From`, +*silently re-resolves them to the new impl* with no error at all. Replacing the +impl instead breaks every caller passing a typed variable. None of these are +reported by `cargo semver-checks`, so adding one would reach a release +unchallenged. + +An inherent constructor avoids the trap entirely: `u32` already exceeds any node +count real hardware reaches, and if a wider or fallible form is ever needed it +arrives as a second, differently named constructor, which is purely additive. +The *stored* width remains private and may grow silently — it is not part of the +promise, and growing it is what would reserve values no existing constructor can +reach. + +**`Thread` fields are private.** Coordinates are read through accessors, so the +struct can gain a field without touching any existing reader, and the derived +`Clone`, `Debug`, `PartialEq`, `Eq` and `Hash` pick it up automatically. No +`#[non_exhaustive]` is needed: with no public fields there is nothing to +destructure or update. + +What a new field *can* silently remove is an auto trait — a coordinate that is +not `Send`, `Sync` or unwind-safe strips that property from `Thread` without any +signature changing. Static assertions pin the auto traits the public types are +expected to have, so this fails the build rather than reaching a release. + +**Every coordinate has a default variant.** This is the rule that makes the rest +work: a new coordinate must have a value meaning *unknown, or not pinned* — +something like a `NumaNode::UNPINNED` constant that no ordinary construction can +produce. Existing constructors keep their exact signatures and fill the new +coordinate with that default; callers that care about it opt in through a new +constructor. + +Reserving such a value costs one representation state that no constructor hands +out. Since `new` accepts the full `u32` range, a sentinel would come either from +widening the private storage past it or from making the reserving constructor +fallible, so that the reserved value can never be minted. A coordinate type +introduced later can simply reserve one from the start. + +Behaviour is preserved as well as compilation. Every pre-existing construction +path fills the new coordinate identically, so values built the old way remain +equal to exactly the values they were equal to before; the new coordinate can +only tell them apart once someone deliberately sets it. + +Adding a coordinate is therefore three additive steps: + +1. Add the private field and its accessor. +2. Add the sentinel constant to the coordinate's type. +3. Add a constructor that accepts it, leaving the existing one alone. + +**Trait additions stay defaulted and dyn-compatible.** `dyn ThreadAware` is part +of the stable surface, so a default body alone is not enough. Any method added +to `ThreadAware` must also avoid generic parameters, `impl Trait`, and `Self` by +value, or carry `where Self: Sized` so the vtable ignores it. Otherwise every +existing `impl` still compiles while every `Box` stops. An +assertion pins dyn-compatibility for the same reason the auto traits are pinned. + +### What this does not permit + +The escape hatch is one-directional. Removing a coordinate, changing the +signature of an existing constructor, adding a `From` impl, changing +what an existing id *means*, or adding a method that breaks dyn-compatibility +are all breaking, and no sentinel value makes them otherwise. Reserving the +additive path for genuinely new information is what keeps it available. + +## Related documents + +For the exact list of items inside the stable boundary, see +[STABILIZATION.md](./STABILIZATION.md). diff --git a/crates/thread_aware_core/docs/STABILIZATION.md b/crates/thread_aware_core/docs/STABILIZATION.md new file mode 100644 index 000000000..4f0eeec61 --- /dev/null +++ b/crates/thread_aware_core/docs/STABILIZATION.md @@ -0,0 +1,40 @@ +# Stabilization Notes + +These notes describe the stable boundary shared by the `thread_aware` crates. + +## Status + +`thread_aware_core` is a stand-alone crate. Nothing depends on it yet, and the +`thread_aware` crate still ships its own `Affinity`-based API unchanged. The +plan is to adopt the core crate later; until then the two evolve separately. + +## Stable boundary + +`thread_aware_core` 1.0 contains the API that downstream crates may expose: + +- `ThreadAware` +- `Thread`, and its component ids `Owner` and `NumaNode` + +A `Thread` says where a value runs: which runtime owns it (`Owner`), which +OS thread it is on, and which memory is closest to that thread (`NumaNode`). The +thread component is `std::thread::ThreadId` rather than an id of our own, so it +is not re-exported; callers take it from `std`. + +Nothing the crate depends on reaches a consumer build; its only manifest entry +is a test-only dev-dependency. The `std` feature is enabled by default and adds +implementations for standard-library types such as `HashMap`, `Path` and +`PathBuf`. Turn it off for `no_std`, where the crate needs only `alloc` and +pointer-width atomics; a +`Thread` then loses its thread id component and cannot be constructed, leaving `Owner` and +`NumaNode` readable so that a `no_std` library can still implement +`ThreadAware`. Implementations for types from external crates are intentionally +outside the stable boundary. + +## Unstable utilities + +Implementation helpers, containers, callbacks, registry APIs, derive support, +and integration helpers remain in the pre-1.0 `thread_aware` crate. Stable +downstream crates should not expose those types in their public APIs. + +This split allows the trait contract and its required `Thread` type to remain +stable without prematurely stabilizing the larger utility surface. diff --git a/crates/thread_aware_core/examples/relocation.rs b/crates/thread_aware_core/examples/relocation.rs new file mode 100644 index 000000000..04a8bb60c --- /dev/null +++ b/crates/thread_aware_core/examples/relocation.rs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Creating `Thread` values and relocating a value between them. +//! +//! Run with `cargo run -p thread_aware_core --example relocation`. + +#![allow(missing_docs, reason = "Example code")] +#![allow(clippy::print_stdout, reason = "Example code")] + +use std::thread; + +use thread_aware_core::{NumaNode, Owner, Thread, ThreadAware}; + +/// A sample value that remembers which thread it currently runs on. +struct Worker { + thread: Option, +} + +impl ThreadAware for Worker { + // `relocate` is limited to bounded local work, so this only records the destination. + // Reporting happens in `main`, outside the callback. + fn relocate(&mut self, _source: Option<&Thread>, destination: &Thread) { + self.thread = Some(destination.id()); + } +} + +fn main() { + // Manually build two `Thread` values, one per OS thread, on the same NUMA node. + let here = thread::current().id(); + let there = thread::spawn(|| thread::current().id()) + .join() + .expect("the spawned thread cannot panic"); + + let owner = Owner::new(2); + let first = Thread::new(owner.clone(), here, NumaNode::new(0)); + let second = Thread::new(owner, there, NumaNode::new(0)); + + // Relocate a sample object between them. + let mut worker = Worker { thread: None }; + + worker.relocate(None, &first); // initial placement; no previous `Thread` + println!("placed on thread {:?}", worker.thread); + + worker.relocate(Some(&first), &second); // migrate from the first `Thread` to the second + println!("relocated to thread {:?}", worker.thread); +} diff --git a/crates/thread_aware_core/favicon.ico b/crates/thread_aware_core/favicon.ico new file mode 100644 index 000000000..53e517713 --- /dev/null +++ b/crates/thread_aware_core/favicon.ico @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82222216219fb16db9695b0e01b605d092cdbd416e0c4c8095688b0bb07e70dd +size 200138 diff --git a/crates/thread_aware_core/logo.png b/crates/thread_aware_core/logo.png new file mode 100644 index 000000000..4058391e0 --- /dev/null +++ b/crates/thread_aware_core/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9d5770f820f40b5c656d676345569068ce7bfe747ddc46febd9b1a5eafd6103 +size 82978 diff --git a/crates/thread_aware_core/src/impls.rs b/crates/thread_aware_core/src/impls.rs new file mode 100644 index 000000000..4cc7a3170 --- /dev/null +++ b/crates/thread_aware_core/src/impls.rs @@ -0,0 +1,632 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use alloc::boxed::Box; +use alloc::collections::{BTreeMap, VecDeque}; +use alloc::string::String; +use alloc::vec::Vec; +use core::cell::{Cell, RefCell}; +use core::num::{ + NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, + NonZeroUsize, +}; +use core::time::Duration; +#[cfg(any(test, feature = "std"))] +use std::collections::HashMap; +#[cfg(any(test, feature = "std"))] +use std::path::{Path, PathBuf}; + +use crate::{NumaNode, Owner, Thread, ThreadAware}; + +/// Implements [`ThreadAware`] for a type that holds nothing bound to a thread, so a +/// move leaves it valid as-is and `relocate` has nothing to do. +macro_rules! impl_thread_aware { + ($t:ty) => { + impl ThreadAware for $t { + #[inline] + fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {} + } + }; +} + +impl_thread_aware!(bool); +impl_thread_aware!(u8); +impl_thread_aware!(u16); +impl_thread_aware!(u32); +impl_thread_aware!(u64); +impl_thread_aware!(u128); +impl_thread_aware!(i8); +impl_thread_aware!(i16); +impl_thread_aware!(i32); +impl_thread_aware!(i64); +impl_thread_aware!(i128); +impl_thread_aware!(usize); +impl_thread_aware!(isize); +impl_thread_aware!(f32); +impl_thread_aware!(f64); +impl_thread_aware!(char); + +impl_thread_aware!(NonZeroU8); +impl_thread_aware!(NonZeroU16); +impl_thread_aware!(NonZeroU32); +impl_thread_aware!(NonZeroU64); +impl_thread_aware!(NonZeroU128); +impl_thread_aware!(NonZeroUsize); +impl_thread_aware!(NonZeroI8); +impl_thread_aware!(NonZeroI16); +impl_thread_aware!(NonZeroI32); +impl_thread_aware!(NonZeroI64); +impl_thread_aware!(NonZeroI128); +impl_thread_aware!(NonZeroIsize); + +impl_thread_aware!(String); +#[cfg(any(test, feature = "std"))] +impl_thread_aware!(PathBuf); +impl_thread_aware!(Duration); +#[cfg(any(test, feature = "std"))] +impl_thread_aware!(Path); + +impl_thread_aware!(str); + +impl_thread_aware!(Owner); +impl_thread_aware!(NumaNode); +#[cfg(any(test, feature = "std"))] +impl_thread_aware!(std::thread::ThreadId); +impl_thread_aware!(Thread); + +// We need to implement `ThreadAware` for tuples ranging from 0 to 12 elements +macro_rules! impl_thread_aware_tuple { + ($head:ident, $($tail:ident,)*) => { + impl<$head, $($tail),*> ThreadAware for ($head, $($tail),*) + where + $head: ThreadAware, + $($tail: ThreadAware),* + { + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + #[expect(non_snake_case, reason = "Macro-generated code uses uppercase identifiers for tuple elements")] + let ($head, $($tail),*) = self; + $head.relocate(source, destination); + $( $tail.relocate(source, destination); )* + } + } + + // Recursively call the macro for the rest of the tuple + impl_thread_aware_tuple!($($tail,)*); + }; + + () => { + impl ThreadAware for () { + #[inline] + fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {} + } + }; +} + +impl_thread_aware_tuple!(A, B, C, D, E, F, G, H, I, J, K, L,); + +macro_rules! impl_thread_aware_fn { + ($head:ident, $($tail:ident,)*) => { + impl ThreadAware for fn($head, $($tail),*) -> R { + fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {} + } + + // Recursively call the macro for the rest of the function parameters + impl_thread_aware_fn!($($tail,)*); + }; + () => { + impl ThreadAware for fn() -> R { + fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {} + } + } +} + +impl_thread_aware_fn!(A, B, C, D, E, F, G, H, I, J, K, L,); + +impl ThreadAware for [T; N] +where + T: ThreadAware, +{ + #[inline] + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + self.as_mut_slice().relocate(source, destination); + } +} + +impl ThreadAware for [T] +where + T: ThreadAware, +{ + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + for value in self.iter_mut() { + value.relocate(source, destination); + } + } +} + +impl ThreadAware for Option +where + T: ThreadAware, +{ + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + if let Some(value) = self { + value.relocate(source, destination); + } + } +} + +impl ThreadAware for Result +where + T: ThreadAware, + E: ThreadAware, +{ + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + match self { + Ok(value) => value.relocate(source, destination), + Err(err) => err.relocate(source, destination), + } + } +} + +impl ThreadAware for Vec +where + T: ThreadAware, +{ + #[inline] + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + self.as_mut_slice().relocate(source, destination); + } +} + +impl ThreadAware for VecDeque +where + T: ThreadAware, +{ + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + for value in self.iter_mut() { + value.relocate(source, destination); + } + } +} + +impl ThreadAware for Box +where + T: ThreadAware + ?Sized, +{ + #[inline] + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + (**self).relocate(source, destination); + } +} + +impl ThreadAware for BTreeMap +where + K: Send, + V: ThreadAware, +{ + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + for value in self.values_mut() { + value.relocate(source, destination); + } + } +} + +/// Implements [`ThreadAware`] for a single-threaded interior-mutability wrapper. +/// +/// `relocate` already holds `&mut self`, so the inner value is reachable through +/// `get_mut` without a borrow flag or a `Cell` round-trip. +macro_rules! impl_thread_aware_cell { + ($t:ident) => { + impl ThreadAware for $t + where + T: ThreadAware, + { + #[inline] + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + self.get_mut().relocate(source, destination); + } + } + }; +} + +impl_thread_aware_cell!(Cell); +impl_thread_aware_cell!(RefCell); + +#[cfg(any(test, feature = "std"))] +impl ThreadAware for HashMap +where + K: Send, + V: ThreadAware, + S: Send, +{ + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { + for value in self.values_mut() { + value.relocate(source, destination); + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +#[cfg(test)] +mod tests { + use alloc::boxed::Box; + use alloc::collections::{BTreeMap, VecDeque}; + use alloc::string::{String, ToString}; + use alloc::vec; + use alloc::vec::Vec; + use core::cell::{Cell, RefCell}; + use core::num::{ + NonZero, NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, + NonZeroU128, NonZeroUsize, + }; + use std::collections::HashMap; + + use crate::{NumaNode, Owner, Thread, ThreadAware}; + + /// A type whose `relocate` visibly mutates state, so mutation tests catch + /// no-op replacements. + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + struct Tracker(bool); + + impl ThreadAware for Tracker { + fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) { + self.0 = true; + } + } + + fn sample_threads() -> [Thread; 2] { + let owner = Owner::new(2); + let thread = std::thread::current().id(); + [ + Thread::new(owner.clone(), thread, NumaNode::new(0)), + Thread::new(owner, thread, NumaNode::new(1)), + ] + } + + #[test] + fn all_nonzero_integer_types_are_thread_aware() { + fn assert_thread_aware() {} + + assert_thread_aware::(); + assert_thread_aware::(); + assert_thread_aware::(); + assert_thread_aware::(); + assert_thread_aware::(); + assert_thread_aware::(); + assert_thread_aware::(); + assert_thread_aware::(); + assert_thread_aware::(); + assert_thread_aware::(); + assert_thread_aware::(); + assert_thread_aware::(); + + assert_thread_aware::>(); + } + + #[test] + fn hashmap_relocates_values_and_tolerates_empty() { + let threads = sample_threads(); + let source = Some(&threads[0]); + let destination = &threads[1]; + + let mut value: HashMap = HashMap::new(); + value.insert(1, "one".to_string()); + value.insert(2, "two".to_string()); + + value.relocate(source, destination); + + assert_eq!(value.get(&1), Some(&"one".to_string())); + assert_eq!(value.get(&2), Some(&"two".to_string())); + + let mut empty_value: HashMap = HashMap::new(); + empty_value.relocate(source, destination); + assert!(empty_value.is_empty()); + } + + #[test] + fn tuples_of_every_supported_arity_relocate() { + let threads = sample_threads(); + let source = Some(&threads[0]); + let destination = &threads[1]; + + // Test empty tuple + let mut empty_tuple = (); + empty_tuple.relocate(source, destination); + + // Test single element tuple + let mut single = (42,); + single.relocate(source, destination); + assert_eq!(single, (42,)); + + // Test two element tuple + let mut two = (42, "hello".to_string()); + two.relocate(source, destination); + assert_eq!(two, (42, "hello".to_string())); + + // Test three element tuple with different types + let mut three = (1, "test".to_string(), 1.23); + three.relocate(source, destination); + assert_eq!(three, (1, "test".to_string(), 1.23)); + + // Test larger tuple (6 elements) + let mut six = (1, 2, 3, 4, 5, 6); + six.relocate(source, destination); + assert_eq!(six, (1, 2, 3, 4, 5, 6)); + + // Test tuple with nested Vec (complex type) + let mut nested = (vec![1, 2, 3], "data".to_string(), 100u64); + nested.relocate(source, destination); + assert_eq!(nested, (vec![1, 2, 3], "data".to_string(), 100u64)); + + // Test tuple with Option + let mut with_option = (Some(42), None::, "value".to_string()); + with_option.relocate(source, destination); + assert_eq!(with_option, (Some(42), None::, "value".to_string())); + + // Test large tuple (12 elements - maximum supported) + let mut twelve = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + twelve.relocate(source, destination); + assert_eq!(twelve, (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)); + } + + /// `impl_thread_aware_tuple!` recurses by peeling one element off the head, so the + /// 1-tuple is the last impl it generates before the `()` base case. That arm is + /// the one where a missing comma would silently change meaning: `let (A) = self` + /// is a parenthesized pattern binding the whole tuple, which sends the impl back + /// into itself instead of reaching the element. `Tracker` makes the visit + /// observable, so this pins the head, the tail and the 12-element maximum. + #[test] + fn every_tuple_element_is_relocated() { + let threads = sample_threads(); + let source = Some(&threads[0]); + let destination = &threads[1]; + + let mut one = (Tracker(false),); + one.relocate(source, destination); + assert_eq!(one, (Tracker(true),)); + + let mut two = (Tracker(false), Tracker(false)); + two.relocate(source, destination); + assert_eq!(two, (Tracker(true), Tracker(true))); + + let mut twelve = ( + Tracker(false), + Tracker(false), + Tracker(false), + Tracker(false), + Tracker(false), + Tracker(false), + Tracker(false), + Tracker(false), + Tracker(false), + Tracker(false), + Tracker(false), + Tracker(false), + ); + twelve.relocate(source, destination); + assert_eq!( + twelve, + ( + Tracker(true), + Tracker(true), + Tracker(true), + Tracker(true), + Tracker(true), + Tracker(true), + Tracker(true), + Tracker(true), + Tracker(true), + Tracker(true), + Tracker(true), + Tracker(true), + ) + ); + } + + #[test] + fn function_pointers_stay_callable_after_relocate() { + // Helper functions for testing + fn no_args() -> i32 { + 42 + } + + fn one_arg(x: i32) -> i32 { + x * 2 + } + + fn two_args(x: i32, y: i32) -> i32 { + x + y + } + + fn three_args(a: i32, b: i32, c: i32) -> i32 { + a + b + c + } + + fn many_args(arg0: i32, arg1: i32, arg2: i32, arg3: i32, arg4: i32, arg5: i32) -> i32 { + arg0 + arg1 + arg2 + arg3 + arg4 + arg5 + } + + // Test with different return types + fn returns_string() -> String { + "hello".to_string() + } + + fn returns_bool(x: i32) -> bool { + x > 0 + } + + let threads = sample_threads(); + let source = Some(&threads[0]); + let destination = &threads[1]; + + // Test fn() -> R + let mut fn_ptr_no_args: fn() -> i32 = no_args; + fn_ptr_no_args.relocate(source, destination); + assert_eq!(fn_ptr_no_args(), 42); + + // Test fn(A) -> R + let mut fn_ptr_one: fn(i32) -> i32 = one_arg; + fn_ptr_one.relocate(source, destination); + assert_eq!(fn_ptr_one(5), 10); + + // Test fn(A, B) -> R + let mut fn_ptr_two: fn(i32, i32) -> i32 = two_args; + fn_ptr_two.relocate(source, destination); + assert_eq!(fn_ptr_two(3, 7), 10); + + // Test fn(A, B, C) -> R + let mut fn_ptr_three: fn(i32, i32, i32) -> i32 = three_args; + fn_ptr_three.relocate(source, destination); + assert_eq!(fn_ptr_three(1, 2, 3), 6); + + // Test with many arguments + let mut fn_ptr_many: fn(i32, i32, i32, i32, i32, i32) -> i32 = many_args; + fn_ptr_many.relocate(source, destination); + assert_eq!(fn_ptr_many(1, 2, 3, 4, 5, 6), 21); + + let mut fn_string: fn() -> String = returns_string; + fn_string.relocate(source, destination); + assert_eq!(fn_string(), "hello".to_string()); + + let mut fn_bool: fn(i32) -> bool = returns_bool; + fn_bool.relocate(source, destination); + assert!(fn_bool(5)); + assert!(!fn_bool(-3)); + } + + #[test] + fn result_relocates_both_variants() { + let threads = sample_threads(); + let source = Some(&threads[0]); + let destination = &threads[1]; + + // Test Ok variant + let mut ok_value: Result = Ok("success".to_string()); + ok_value.relocate(source, destination); + assert_eq!(ok_value, Ok("success".to_string())); + + // Test Err variant + let mut err_value: Result = Err(42); + err_value.relocate(source, destination); + assert_eq!(err_value, Err(42)); + + // Test with complex types + let mut ok_vec: Result, String> = Ok(vec![1, 2, 3]); + ok_vec.relocate(source, destination); + assert_eq!(ok_vec, Ok(vec![1, 2, 3])); + + let mut err_string: Result, String> = Err("error".to_string()); + err_string.relocate(source, destination); + assert_eq!(err_string, Err("error".to_string())); + } + + #[test] + fn option_some_forwards_relocate() { + let threads = sample_threads(); + let mut val = Some(Tracker(false)); + val.relocate(Some(&threads[0]), &threads[1]); + assert_eq!(val, Some(Tracker(true))); + } + + #[test] + fn option_none_is_noop() { + let threads = sample_threads(); + let mut val: Option = None; + val.relocate(Some(&threads[0]), &threads[1]); + assert_eq!(val, None); + } + + #[test] + fn result_ok_forwards_relocate() { + let threads = sample_threads(); + let mut val: Result = Ok(Tracker(false)); + val.relocate(Some(&threads[0]), &threads[1]); + assert_eq!(val, Ok(Tracker(true))); + } + + #[test] + fn result_err_forwards_relocate() { + let threads = sample_threads(); + let mut val: Result = Err(Tracker(false)); + val.relocate(Some(&threads[0]), &threads[1]); + assert_eq!(val, Err(Tracker(true))); + } + + #[test] + fn vec_forwards_relocate_to_elements() { + let threads = sample_threads(); + let mut val = vec![Tracker(false), Tracker(false)]; + val.relocate(Some(&threads[0]), &threads[1]); + assert!(val.iter().all(|t| t.0), "all elements must be relocated"); + } + + #[test] + fn array_and_slice_forward_relocate_to_elements() { + let threads = sample_threads(); + let mut val = [Tracker(false), Tracker(false)]; + val.relocate(Some(&threads[0]), &threads[1]); + assert!(val.iter().all(|t| t.0), "all array elements must be relocated"); + + let slice: &mut [Tracker] = &mut val; + for value in slice.iter_mut() { + value.0 = false; + } + slice.relocate(Some(&threads[0]), &threads[1]); + assert!(slice.iter().all(|t| t.0), "all slice elements must be relocated"); + } + + #[test] + fn vec_deque_forwards_relocate_to_elements() { + let threads = sample_threads(); + let mut val = VecDeque::from([Tracker(false), Tracker(false)]); + val.relocate(Some(&threads[0]), &threads[1]); + assert!(val.iter().all(|t| t.0), "all elements must be relocated"); + } + + #[test] + fn btree_map_relocates_values_without_mutating_keys() { + let threads = sample_threads(); + let mut map = BTreeMap::new(); + map.insert(Tracker(false), Tracker(false)); + map.relocate(Some(&threads[0]), &threads[1]); + + let (key, value) = map.first_key_value().unwrap(); + assert!(!key.0, "key identity must remain stable"); + assert!(value.0, "value must be relocated"); + } + + #[test] + fn cells_forward_relocate_to_inner_value() { + let threads = sample_threads(); + + let mut cell = Cell::new(Tracker(false)); + cell.relocate(Some(&threads[0]), &threads[1]); + assert!(cell.into_inner().0); + + let mut ref_cell = RefCell::new(Tracker(false)); + ref_cell.relocate(Some(&threads[0]), &threads[1]); + assert!(ref_cell.into_inner().0); + } + + #[test] + fn box_forwards_relocate() { + let threads = sample_threads(); + let mut val: Box = Box::new(Tracker(false)); + val.relocate(Some(&threads[0]), &threads[1]); + assert!(val.0, "must forward relocate to the inner value"); + } + + #[test] + fn hashmap_relocates_values_without_mutating_keys() { + use core::hash::BuildHasherDefault; + use std::hash::DefaultHasher; + + let threads = sample_threads(); + let mut map: HashMap> = HashMap::default(); + map.insert(Tracker(false), Tracker(false)); + map.relocate(Some(&threads[0]), &threads[1]); + + let (key, value) = map.iter().next().unwrap(); + assert!(!key.0, "key identity must remain stable"); + assert!(value.0, "value must be relocated"); + } +} diff --git a/crates/thread_aware_core/src/lib.rs b/crates/thread_aware_core/src/lib.rs new file mode 100644 index 000000000..5b457be58 --- /dev/null +++ b/crates/thread_aware_core/src/lib.rs @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![no_std] +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc(html_logo_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/thread_aware_core/logo.png")] +#![doc( + html_favicon_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/thread_aware_core/favicon.ico" +)] + +//! Support for values that adapt when a runtime moves them to another thread. +//! +//! This crate contains the small API shared by thread-aware libraries: +//! +//! - [`ThreadAware`] notifies a value that it has moved. +//! - [`Thread`] records where it now runs: which runtime, which OS thread, and which memory is +//! closest to it. +//! +//! [`Thread`] is a coordinate, not a handle: a runtime builds one to describe where a value +//! is running, and it owns no operating-system resource. It is unrelated to +//! [`std::thread::Thread`], which is a handle to a live OS thread. Naming both in one module +//! requires aliasing one of them. +//! +//! # The `thread_aware` family +//! +//! - **`thread_aware_core`** (this crate) — the vocabulary that two unrelated libraries must +//! agree on before either can relocate a value defined by the other. Deliberately small and +//! slow-moving, so naming [`ThreadAware`] or [`Thread`] in your own public API costs you +//! nothing later. +//! - **[`thread_aware`]** — the utilities that make relocation convenient: a +//! [`#[derive(ThreadAware)]`][derive] macro, wrappers for foreign types, a per-core +//! [`Arc`][arc], containers and registries. Free to evolve, and not meant to appear in a +//! public API. +//! +//! [`thread_aware`]: https://docs.rs/thread_aware +//! [derive]: https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html +//! [arc]: https://docs.rs/thread_aware/latest/thread_aware/struct.Arc.html +//! +//! Depend on this crate directly when all you need is the trait. It adds nothing to your +//! dependency graph, and works without `std`: with default features turned off, [`Thread`] +//! loses its thread id component and keeps [`Owner`] and [`NumaNode`]. +//! +//! # Why relocation exists +//! +//! Thread-per-core and NUMA-aware runtimes are fast because each worker keeps to itself: it +//! uses memory close to its own thread, talks to its own I/O driver, and does not +//! synchronize with other workers. When a value moves to another worker, what used to be +//! close by is now in the wrong place: a cache line shared between threads, memory in a +//! distant region, a handle to another thread's driver. +//! +//! [`ThreadAware`] lets that state repair itself. The runtime moves the value, then calls +//! [`relocate`](ThreadAware::relocate) to report where it now lives. Relocation has two +//! sides, and most code sits on only one of them. +//! +//! # Library authors: implementing the trait +//! +//! **Library and application authors** implement [`ThreadAware`], usually through the +//! [`#[derive(ThreadAware)]`][derive] macro. They never call +//! [`relocate`](ThreadAware::relocate) and never construct a [`Thread`]; the runtime does +//! both and then invokes the implementation. It is a callback, like [`Drop::drop`]. +//! +//! The derive lives in [`thread_aware`], so a library that wants it depends on that crate. +//! Only the trait and [`Thread`] cross the public boundary, and both come from here, so the +//! dependency stays an implementation detail: +//! +//! ```ignore +//! // A build dependency, not part of what this library promises. +//! use thread_aware::ThreadAware; +//! +//! /// A codec whose scratch buffer should follow the memory it is used from. +//! #[derive(ThreadAware)] +//! pub struct Encoder { +//! scratch: Scratch, +//! dictionary: Dictionary, +//! } +//! ``` +//! +//! The derive writes the forwarding implementation, calling `relocate` on `scratch` and +//! `dictionary` in turn. Because a composed type forwards to its fields, one call at the top +//! reaches everything below it. Callers of `Encoder` never name [`thread_aware`]. +//! +//! # Runtime authors: driving relocation +//! +//! **Runtime authors** construct a [`Thread`] per worker and call +//! [`relocate`](ThreadAware::relocate) after moving a value, passing where it came from and +//! where it now runs. The example below plays the part of the runtime so the order is +//! visible. +//! +//! ``` +//! # fn main() { +//! # #[cfg(feature = "std")] { +//! use std::thread; +//! +//! use thread_aware_core::{NumaNode, Owner, Thread, ThreadAware}; +//! +//! // What a library author writes. +//! struct Worker { +//! thread: Option, +//! } +//! +//! impl ThreadAware for Worker { +//! fn relocate(&mut self, _source: Option<&Thread>, destination: &Thread) { +//! self.thread = Some(destination.id()); +//! } +//! } +//! +//! // What the runtime does. +//! let here = thread::current().id(); +//! let there = thread::spawn(|| thread::current().id()).join().unwrap(); +//! +//! let owner = Owner::new(2); +//! let first = Thread::new(owner.clone(), here, NumaNode::new(0)); +//! let second = Thread::new(owner, there, NumaNode::new(1)); +//! +//! let mut worker = Worker { thread: None }; +//! +//! worker.relocate(None, &first); // first placement, no previous `Thread` +//! worker.relocate(Some(&first), &second); // moved to another thread +//! +//! assert_eq!(worker.thread, Some(there)); +//! # } +//! # } +//! ``` +//! +//! # Performance, not correctness +//! +//! Relocation is an optimization, not a guarantee. A value must remain correct if the call +//! never comes, comes twice, or reports the same source and destination. Missing calls are +//! normal: a value can reach another thread through `std::thread::spawn`, a channel, or a +//! runtime that knows nothing about this trait. That may make things slower, but it must +//! never cause a panic, a deadlock, or a wrong answer. +//! +//! Nor is it a hot path. Expect roughly one relocation per object graph per job or request, +//! after which the value is used normally. Avoiding synchronization matters more than saving a +//! few cycles. +//! +//! # What the ids mean +//! +//! - **Thread id** — a `std::thread::ThreadId`, unique among the threads alive at once, so +//! state keyed on it is never shared by accident, not even between two runtimes in one +//! process. +//! - **[`NumaNode`]** — the memory closest to that thread. Unlike the thread id it is +//! *shared*: every thread near the same memory reports the same node, which is what suits +//! it to state shared within a region but not across the machine. That holds only while +//! every runtime numbers the regions identically. Nothing checks it, and runtimes that +//! disagree make shared state wrong rather than merely slow. +//! - **[`Owner`]** — the runtime a [`Thread`] belongs to. Every new owner is unique, so two +//! live runtimes never share one. It lets a value detect that it has crossed into a +//! different runtime and release anything the previous one owned. +//! +//! An implementation reads only the ids its state depends on. A per-thread cache or a handle +//! to a thread-local driver keys on the thread id; a buffer pool keys on [`NumaNode`] and +//! survives a move to another thread near the same memory; anything the runtime owns compares +//! [`Owner`]. +//! +//! The ids carry no meaning beyond identity: they need not start at zero or run +//! consecutively, and the [`Thread`]s in use cannot be enumerated. State keyed on any of them +//! belongs in a map rather than an array indexed by it. [`Owner::min_threads`] is the one +//! number on offer, and it is a floor to pre-size against, not a bound to index against. +//! +//! Without `std` there is no [`ThreadId`](std::thread::ThreadId): `Thread::new` and +//! `Thread::id` are absent and only [`Owner`] and [`NumaNode`] remain. A `no_std` library can +//! still implement [`ThreadAware`] and use whatever it is given; the runtime that drives +//! relocation requires `std` regardless. +//! +//! # Relation to `Send` +//! +//! [`ThreadAware`] requires [`Send`], and in that order: a value is sent to another thread +//! first, then told where it landed. [`Send`] is what makes the move safe, and +//! [`ThreadAware`] adds nothing to it. +//! +//! # Provided implementations +//! +//! Types with nothing tied to a thread receive an empty implementation: primitives and their +//! non-zero variants, the thread ids, `Duration`, strings, safe function pointers of up to +//! twelve parameters, and, with the `std` feature, paths. +//! +//! Containers forward the call to what they hold: [`Option`], [`Result`], arrays, slices, +//! `Vec`, `VecDeque`, `Box`, cells, tuples of up to twelve elements, and map values. +//! +//! References are not [`ThreadAware`]. Relocating through one would adapt something the value +//! only borrows, and whoever owns it is relocated on its own account. +//! +//! Map keys are left alone, since altering one could change its hash or ordering and corrupt +//! the map. Sets are not implemented at all for the same reason, so a `HashSet` or +//! `BTreeSet` field is not [`ThreadAware`]. +//! +//! `Cow` is omitted for now: relocating a borrowed one has to clone it into owned storage +//! first, which is a surprising amount of work to hide behind a hint. +//! +//! `Arc` is also omitted: whether a shared allocation should stay shared across threads or +//! be split per thread depends on what it holds. The per-core [`Arc`][arc] in +//! [`thread_aware`] covers the case where splitting is correct. +//! +//! # Features +//! +//! - **`std`** *(default)* - Adds [`Thread::new`] and [`Thread::id`], which need +//! [`ThreadId`](std::thread::ThreadId), and implements [`ThreadAware`] for standard library +//! types such as `HashMap`, `Path` and `PathBuf`. Turn it off for `no_std`, which needs +//! only `alloc` and pointer-width atomics. + +extern crate alloc; +#[cfg(any(test, feature = "std"))] +extern crate std; + +mod impls; +mod thread; +mod thread_aware; + +#[doc(inline)] +pub use thread::{NumaNode, Owner, Thread}; +#[doc(inline)] +pub use thread_aware::ThreadAware; diff --git a/crates/thread_aware_core/src/thread.rs b/crates/thread_aware_core/src/thread.rs new file mode 100644 index 000000000..3cc3ed388 --- /dev/null +++ b/crates/thread_aware_core/src/thread.rs @@ -0,0 +1,456 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! The [`Thread`] identifier and its component id types. +//! +//! A [`Thread`] records where a value runs: which runtime owns it, which OS thread it names, +//! and which memory is closest to that OS thread. See +//! [what the ids mean](crate#what-the-ids-mean) for the guarantees each id carries. + +use core::hash::{Hash, Hasher}; +use core::sync::atomic::{AtomicUsize, Ordering}; +#[cfg(any(test, feature = "std"))] +use std::thread::ThreadId; + +/// Hands the next identity to each [`Owner`] created in this process. +/// +/// Pointer-width so the counter does not wrap in practice; a wrap would hand out a live +/// identity twice. +static NEXT_OWNER: AtomicUsize = AtomicUsize::new(0); + +/// An identifier for the runtime that owns a [`Thread`]. +/// +/// Every new owner is unique, so two runtimes alive at the same time never share one. That +/// is what lets a value notice it has crossed from one runtime into another and release +/// anything the previous one owned. +/// +/// An owner also reports the smallest number of threads its runtime runs, which a value can +/// use to pre-size per-thread state before it has seen any of those threads. The runtime may +/// run more; one that spawns threads entirely on demand promises none and reports `0`. +/// +/// Two owners are the same runtime exactly when their identities match; the thread count +/// plays no part in equality or hashing. +/// +/// An owner names one live runtime, so it is `Clone` but not `Copy`, and +/// [`Thread::owner`] lends it rather than handing out duplicates. +/// +/// # Examples +/// +/// ``` +/// use thread_aware_core::Owner; +/// +/// let pool = Owner::new(4); +/// assert_eq!(pool.min_threads(), 4); +/// +/// let elastic = Owner::new(0); +/// assert_eq!(elastic.min_threads(), 0); +/// +/// // Every owner is distinct, however it was built. +/// assert_ne!(pool, Owner::new(4)); +/// assert_ne!(elastic, Owner::new(0)); +/// ``` +#[derive(Clone, Debug)] +pub struct Owner { + id: usize, + min_threads: usize, +} + +impl PartialEq for Owner { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Owner {} + +impl Hash for Owner { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl Owner { + /// Creates an owner for a runtime that runs at least `min_threads` threads. + /// + /// The owner is unique: no other owner in this process compares equal to it. + /// + /// # Examples + /// + /// ``` + /// use thread_aware_core::Owner; + /// + /// let owner = Owner::new(2); + /// + /// assert_eq!(owner.min_threads(), 2); + /// ``` + #[must_use] + pub fn new(min_threads: usize) -> Self { + Self { + id: NEXT_OWNER.fetch_add(1, Ordering::Relaxed), + min_threads, + } + } + + /// Returns the smallest number of threads the owning runtime runs. + /// + /// The runtime may run more, so this is a floor for pre-sizing rather than a bound to + /// index against. `0` means the runtime promises none and spawns on demand. + /// + /// # Examples + /// + /// ``` + /// use thread_aware_core::Owner; + /// + /// assert_eq!(Owner::new(8).min_threads(), 8); + /// assert_eq!(Owner::new(0).min_threads(), 0); + /// ``` + #[inline] + #[must_use] + pub const fn min_threads(&self) -> usize { + self.min_threads + } +} + +/// An identifier for the memory closest to a thread, usually a NUMA node. +/// +/// On a large machine, memory is divided into regions and a thread reaches its own region +/// fastest. Unlike the thread id, this identifier is shared: every thread near the same +/// memory reports the same `NumaNode`, which is what makes it suitable for state that is +/// shared within a region but not across the machine. Sharing between runtimes holds only +/// while they all number the regions identically; see +/// [what the ids mean](crate#what-the-ids-mean). +/// +/// Nodes carry no meaning beyond identity, and the width is wide enough that no real +/// machine can exhaust it. +/// +/// # Examples +/// +/// ``` +/// use thread_aware_core::NumaNode; +/// +/// let node = NumaNode::new(0); +/// +/// assert_eq!(node, NumaNode::new(0)); +/// assert_ne!(node, NumaNode::new(1)); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct NumaNode(u32); + +impl NumaNode { + /// Creates a node identifier from the number the platform reports. + /// + /// # Examples + /// + /// ``` + /// use thread_aware_core::NumaNode; + /// + /// assert_ne!(NumaNode::new(0), NumaNode::new(1)); + /// ``` + #[inline] + #[must_use] + pub const fn new(node: u32) -> Self { + Self(node) + } +} + +/// A record of where a value runs: its runtime, its OS thread, and its nearest memory. +/// +/// A runtime constructs these, usually once per worker at startup, and passes them to +/// [`ThreadAware::relocate`](crate::ThreadAware::relocate). An implementation reads whichever +/// part it depends on; [what the ids mean](crate#what-the-ids-mean) describes which to +/// choose. +/// +/// # Relation to `std::thread::Thread` +/// +/// This `Thread` is a coordinate, not a handle: it records where a value runs and owns no +/// operating-system resource. [`std::thread::Thread`] is unrelated, and naming both in one +/// module is `error[E0252]`; alias the standard handle if you need it. +/// +/// `Thread` is cheap to clone but deliberately not `Copy`, and is passed by reference to +/// [`relocate`](crate::ThreadAware::relocate), so cloning is rarely necessary. +/// +/// Two values are equal only if all their ids match. Code concerned only with memory +/// locality compares [`numa_node`](Self::numa_node) rather than whole `Thread`s, since +/// threads sharing a NUMA node still have different thread ids. +/// +/// # Without `std` +/// +/// The thread id is `std::thread::ThreadId`, so without the `std` feature a `Thread` cannot +/// be constructed and holds only [`owner`](Self::owner) and [`numa_node`](Self::numa_node). +/// Equality and hashing then compare those two alone. +/// +/// # Examples +/// +/// ``` +/// # fn main() { +/// # #[cfg(feature = "std")] { +/// use std::thread; +/// +/// use thread_aware_core::{NumaNode, Owner, Thread}; +/// +/// let here = thread::current().id(); +/// let owner = Owner::new(1); +/// let mine = Thread::new(owner.clone(), here, NumaNode::new(1)); +/// +/// assert_eq!(mine.owner(), &owner); +/// assert_eq!(mine.id(), here); +/// +/// // The same OS thread under a different runtime is a different `Thread`... +/// let elsewhere = Thread::new(Owner::new(1), here, NumaNode::new(1)); +/// assert_ne!(mine, elsewhere); +/// +/// // ...but the thread id and its nearest memory are unchanged. +/// assert_eq!(mine.numa_node(), elsewhere.numa_node()); +/// # } +/// # } +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Thread { + owner: Owner, + // `test` is part of the gate so the crate's own test build compiles this field without + // enumerating features (see docs/optional-deps-in-test-builds.md). The cost is that no + // unit test can observe the `no_std` shape: `cfg(test)` puts `id` back, so a test gated + // on `not(feature = "std")` still sees three fields and would assert the wrong thing. + // `tests/no_std_surface.rs` covers that shape from outside the test build instead. + #[cfg(any(test, feature = "std"))] + id: ThreadId, + numa_node: NumaNode, +} + +impl Thread { + /// Creates a `Thread` from an owner, a thread id, and a NUMA node. + /// + /// Runtimes call this as they set up their workers. Tests may call it directly to + /// construct values without starting a runtime. A thread id is obtained from + /// [`thread::current`](std::thread::current). + /// + /// Requires the `std` feature. + /// + /// # Examples + /// + /// ``` + /// use std::thread; + /// + /// use thread_aware_core::{NumaNode, Owner, Thread}; + /// + /// let owner = Owner::new(1); + /// let mine = Thread::new(owner.clone(), thread::current().id(), NumaNode::new(0)); + /// + /// assert_eq!(mine.owner(), &owner); + /// ``` + #[cfg(any(test, feature = "std"))] + #[inline] + #[must_use] + pub const fn new(owner: Owner, id: ThreadId, numa_node: NumaNode) -> Self { + Self { owner, id, numa_node } + } + + /// Returns the identifier of the runtime that owns this [`Thread`]. + /// + /// Comparing owners detects that a value has moved between runtimes. Such a move remains + /// sound, but resources owned by the previous runtime usually cannot follow. + /// + /// # Examples + /// + /// ``` + /// # fn main() { + /// # #[cfg(feature = "std")] { + /// use std::thread; + /// + /// use thread_aware_core::{NumaNode, Owner, Thread}; + /// + /// let owner = Owner::new(7); + /// let mine = Thread::new(owner.clone(), thread::current().id(), NumaNode::new(0)); + /// + /// assert_eq!(mine.owner(), &owner); + /// # } + /// # } + /// ``` + #[inline] + #[must_use] + pub const fn owner(&self) -> &Owner { + &self.owner + } + + /// Returns the [`ThreadId`](std::thread::ThreadId) of the OS thread this `Thread` names. + /// + /// Distinct live OS threads have distinct ids, so this partitions state by OS thread + /// without keys colliding. Whether that state is contended depends on the storage around + /// it. A [`ThreadId`](std::thread::ThreadId) has no defined relationship to whatever + /// identifier the platform assigns. + /// + /// Requires the `std` feature. + /// + /// # Examples + /// + /// ``` + /// use std::thread; + /// + /// use thread_aware_core::{NumaNode, Owner, Thread}; + /// + /// let here = thread::current().id(); + /// let mine = Thread::new(Owner::new(1), here, NumaNode::new(0)); + /// + /// assert_eq!(mine.id(), here); + /// ``` + #[cfg(any(test, feature = "std"))] + #[inline] + #[must_use] + pub const fn id(&self) -> ThreadId { + self.id + } + + /// Returns the identifier of the memory nearest to this [`Thread`]. + /// + /// This is the id to use when what matters is which memory is nearby rather than which + /// thread is running, since threads on the same node share that memory cheaply. + /// + /// # Examples + /// + /// ``` + /// # fn main() { + /// # #[cfg(feature = "std")] { + /// use std::thread; + /// + /// use thread_aware_core::{NumaNode, Owner, Thread}; + /// + /// let mine = Thread::new(Owner::new(1), thread::current().id(), NumaNode::new(3)); + /// + /// assert_eq!(mine.numa_node(), NumaNode::new(3)); + /// # } + /// # } + /// ``` + #[inline] + #[must_use] + pub const fn numa_node(&self) -> NumaNode { + self.numa_node + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +#[cfg(test)] +mod tests { + use core::panic::{RefUnwindSafe, UnwindSafe}; + use std::hash::DefaultHasher; + use std::thread; + + use static_assertions::{assert_impl_all, assert_not_impl_any}; + + use super::{Hash, Hasher, NumaNode, Owner, Thread}; + + assert_impl_all!(Owner: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe); + // An owner names one live runtime, so handing out silent duplicates is not something the + // type should make effortless; `Clone` keeps it deliberate. + assert_not_impl_any!(Owner: Copy); + assert_impl_all!(NumaNode: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe); + assert_impl_all!(Thread: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe); + + #[test] + fn exposes_components() { + let id = thread::current().id(); + let owner = Owner::new(3); + let mine = Thread::new(owner.clone(), id, NumaNode::new(2)); + + assert_eq!(mine.owner(), &owner); + assert_eq!(mine.owner().min_threads(), 3, "the owner survives intact, count included"); + assert_eq!(mine.id(), id); + assert_eq!(mine.numa_node(), NumaNode::new(2)); + } + + #[test] + fn different_owner_compares_unequal() { + let id = thread::current().id(); + let numa_node = NumaNode::new(0); + let first = Thread::new(Owner::new(1), id, numa_node); + let second = Thread::new(Owner::new(1), id, numa_node); + + assert_ne!(first, second); + } + + #[test] + fn different_numa_compares_unequal() { + let id = thread::current().id(); + // One owner, so the NUMA node is the only thing that differs. + let owner = Owner::new(1); + let first = Thread::new(owner.clone(), id, NumaNode::new(0)); + let second = Thread::new(owner, id, NumaNode::new(1)); + + assert_ne!(first, second); + } + + #[test] + fn different_thread_compares_unequal() { + let owner = Owner::new(0); + let numa_node = NumaNode::new(0); + let here = Thread::new(owner.clone(), thread::current().id(), numa_node); + let there = thread::spawn(move || Thread::new(owner, thread::current().id(), numa_node)) + .join() + .unwrap(); + + assert_ne!(here, there); + } + + #[test] + fn clone_preserves_components() { + let mine = Thread::new(Owner::new(4), thread::current().id(), NumaNode::new(9)); + let cloned = mine.clone(); + + assert_eq!(cloned, mine); + assert_eq!(cloned.owner().min_threads(), mine.owner().min_threads()); + assert_eq!(cloned.numa_node(), mine.numa_node()); + } + + #[test] + fn owner_identity_is_unique_per_construction() { + let first = Owner::new(4); + let second = Owner::new(4); + + assert_ne!(first, second, "each owner takes its own identity"); + assert_eq!(first, first, "an owner equals itself"); + assert_eq!(first, first.clone(), "a clone keeps the same identity"); + } + + #[test] + fn owner_equality_ignores_the_thread_count() { + // The public API cannot produce two owners sharing an identity, but `Eq` and `Hash` + // must still agree if it ever could, so build the pair directly. + let one = Owner { id: 7, min_threads: 1 }; + let other = Owner { id: 7, min_threads: 99 }; + + assert_eq!(one, other, "identity alone decides equality"); + assert_eq!(hash_of(&one), hash_of(&other), "equal owners must hash equally"); + } + + #[test] + fn owner_hashes_by_identity() { + let one = Owner::new(4); + let other = Owner::new(4); + + // Equal counts, so only the identity tells these apart. Hashing has to carry it, or + // every owner would land in one bucket. + assert_ne!(one, other); + assert_ne!(hash_of(&one), hash_of(&other), "distinct owners must hash apart"); + } + + #[test] + fn owner_reports_its_minimum() { + assert_eq!(Owner::new(8).min_threads(), 8); + assert_eq!(Owner::new(0).min_threads(), 0, "zero means the runtime promises none"); + } + + #[test] + fn owner_clone_preserves_the_thread_count() { + let owner = Owner::new(4); + let clone = owner.clone(); + + assert_eq!(clone, owner, "a clone keeps the identity"); + assert_eq!(clone.min_threads(), 4, "a clone keeps the count, not just the identity"); + } + + fn hash_of(owner: &Owner) -> u64 { + let mut hasher = DefaultHasher::new(); + owner.hash(&mut hasher); + hasher.finish() + } +} diff --git a/crates/thread_aware_core/src/thread_aware.rs b/crates/thread_aware_core/src/thread_aware.rs new file mode 100644 index 000000000..673a50559 --- /dev/null +++ b/crates/thread_aware_core/src/thread_aware.rs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! The [`ThreadAware`] trait. + +use crate::Thread; + +/// A type that adapts when it is moved to a different [`Thread`]. +/// +/// Implement this trait when part of a type depends on where it runs: memory near a +/// particular node, a handle to a thread-local driver, a shard index, a cached thread id. +/// [`relocate`](Self::relocate) brings that state back into line. Implementors do not call +/// it themselves; a runtime does, as described in +/// [driving relocation](crate#runtime-authors-driving-relocation). +/// +/// Implement or derive it for any type that may end up inside a value a runtime relocates, +/// including types that do nothing on relocation, since an empty implementation is what lets +/// an enclosing type derive the trait. Do not implement it merely because a type is +/// [`Send`], and never put anything correctness depends on inside +/// [`relocate`](Self::relocate); it may affect performance only. +/// +/// # Implementing +/// +/// Implementations usually take one of four shapes. +/// +/// 1. **Do nothing.** Nothing in the type depends on where it runs. An empty body is a +/// complete implementation, and is what the primitive types do. +/// +/// ``` +/// use thread_aware_core::{Thread, ThreadAware}; +/// +/// struct RequestId(u64); +/// +/// impl ThreadAware for RequestId { +/// fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {} +/// } +/// ``` +/// 2. **Record the destination.** Store the new thread id, [`NumaNode`](crate::NumaNode) or +/// [`Owner`](crate::Owner) for later use. +/// 3. **Replace a resource.** Compare the id it depends on: [`NumaNode`](crate::NumaNode) +/// for a buffer pool, the thread id for a driver handle, [`Owner`](crate::Owner) for +/// anything the runtime owns. If it changed, release the old resource and acquire one for +/// the new [`Thread`], moving out any real data it holds first. +/// 4. **Forward to fields.** A type composed of other types calls `relocate` on each field. +/// Prefer `#[derive(ThreadAware)]` to writing this by hand. +/// +/// To *detach* is to release a resource and leave the field empty, so that the value can +/// acquire a new one later or run without it. Such a field needs a type that can be empty, +/// such as `Option`. +/// +/// # Requirements +/// +/// [`relocate`](Self::relocate) cannot fail and has no way to report an error, so every +/// implementation must: +/// +/// * **Preserve real data.** Anything that exists only for speed may be rebuilt: caches, +/// pools, scratch buffers, handles. Nothing observable through the value may be lost or +/// altered. A cache still holding writes that have not been flushed is real data, and must +/// be moved rather than dropped. +/// +/// * **Remain correct.** When the ideal adaptation is unavailable, being slower is preferred +/// to being wrong, and keeping the old resource is acceptable. Whatever the value holds +/// must keep working even if this method is never called, so a driver handle must remain +/// usable from the new thread. Relocation brings it closer; it does not make it valid. +/// +/// * **Neither panic nor block.** This runs while the runtime is placing work, so it +/// performs no network or disk I/O, no waiting on another worker, and takes no contended +/// lock. Release anything that would block here and re-acquire it on first use. +/// +/// * **Tolerate repeated calls.** Relocating to the same [`Thread`], or with `source` equal to +/// `destination`, is harmless, and should also be cheap: compare the relevant ids and +/// return early when nothing has changed. +/// +/// * **Tolerate no call at all.** The value remains correct either way. +/// +/// * **Tolerate an unfamiliar [`Thread`].** A `destination` may name an OS thread the value has never +/// seen and carry an [`Owner`](crate::Owner) belonging to another runtime, and this must +/// remain sound. Release anything the previous runtime owned; state keyed on +/// [`NumaNode`](crate::NumaNode) may remain valid, subject to the caveat in +/// [what the ids mean](crate#what-the-ids-mean). Performance may suffer afterwards, though +/// not permanently: once back on a [`Thread`] it can serve, the value re-acquires what it +/// released. +/// +/// A [`Thread`] may be cloned and retained, but the thread id it holds is meaningful only +/// while that thread is alive, and an [`Owner`](crate::Owner) only while that runtime is. +/// +/// Runtimes carry their own requirements. They call [`relocate`](Self::relocate) only after +/// the value has actually moved, pass `None` when no previous [`Thread`] is known, build one +/// [`Owner`](crate::Owner) per runtime, and never rely on the call for correctness. Nothing +/// enforces any of this. +/// +/// # Examples +/// +/// A type that releases a scratch buffer when the nearest memory changes, so that the next +/// use allocates near the new node. The buffer holds nothing between calls, so discarding it +/// is safe, while the `name` field is real data and is left alone. +/// +/// ``` +/// use thread_aware_core::{NumaNode, Thread, ThreadAware}; +/// +/// struct Encoder { +/// name: String, +/// numa_node: Option, +/// /// Reused between calls purely to avoid re-allocating; empty outside a call. +/// scratch: Vec, +/// } +/// +/// impl ThreadAware for Encoder { +/// fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { +/// // Only pay for this when the nearest memory actually changed. +/// if source.map(Thread::numa_node) == Some(destination.numa_node()) { +/// return; +/// } +/// +/// // Record the node and drop the buffer allocated near the old one. `Vec` cannot +/// // choose a node itself, so placement comes from the allocator the application +/// // installs; releasing here is what gives it the chance to allocate near +/// // `numa_node` on the next use. +/// self.numa_node = Some(destination.numa_node()); +/// self.scratch = Vec::new(); +/// } +/// } +/// ``` +/// +/// A type that forwards the call to its fields: +/// +/// ``` +/// use thread_aware_core::{Thread, ThreadAware}; +/// +/// # struct Encoder; +/// # impl ThreadAware for Encoder { +/// # fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {} +/// # } +/// struct Session { +/// id: u64, +/// encoder: Encoder, +/// } +/// +/// impl ThreadAware for Session { +/// fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) { +/// self.id.relocate(source, destination); +/// self.encoder.relocate(source, destination); +/// } +/// } +/// ``` +pub trait ThreadAware: Send { + /// Adapts this value to the [`Thread`] it now occupies. + /// + /// Implementors provide this method but do not normally call it. A runtime calls it + /// after moving the value. + /// + /// `destination` is where the value runs from now on. `source` is where it ran before, + /// or `None` when that is unknown, which is normal for a first placement or for a value + /// arriving from outside the runtime. `None` means the implementation can assume nothing + /// about the previous [`Thread`]; it does not indicate an error. + /// + /// This method cannot fail, must not panic, and is safe to call more than once, + /// including with `source` equal to `destination`. See the + /// [requirements](Self#requirements) for the rest, and the trait-level + /// [examples](Self#examples) for implementations. + fn relocate(&mut self, source: Option<&Thread>, destination: &Thread); +} + +#[cfg(test)] +mod tests { + use static_assertions::assert_obj_safe; + + use super::ThreadAware; + + // `dyn ThreadAware` is part of the stable surface, so anything added to the trait later + // has to keep it dyn-compatible. + assert_obj_safe!(ThreadAware); +} diff --git a/crates/thread_aware_core/tests/no_std_surface.rs b/crates/thread_aware_core/tests/no_std_surface.rs new file mode 100644 index 000000000..a041e3f96 --- /dev/null +++ b/crates/thread_aware_core/tests/no_std_surface.rs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Exercises the public surface from outside the crate's own test build. +//! +//! Unit tests in `src/` compile the library with `cfg(test)`, which switches on every +//! `cfg(any(test, feature = "std"))` item whatever features are selected, so they always +//! see the three-field `Thread`. This harness links the library as an ordinary dependency, +//! so under `--no-default-features` the `no_std` shape is the one under test. + +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +use static_assertions::assert_impl_all; +use thread_aware_core::{NumaNode, Owner, Thread}; + +assert_impl_all!(Thread: Clone, Eq, Send, Sync); +assert_impl_all!(Owner: Clone, Eq, Send, Sync); +assert_impl_all!(NumaNode: Copy, Eq, Send, Sync); + +fn hash_of(value: &T) -> u64 +where + T: Hash, +{ + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() +} + +#[test] +fn owners_keep_their_identity_and_count() { + let first = Owner::new(4); + let second = Owner::new(4); + + assert_ne!(first, second, "each owner takes its own identity"); + assert_eq!(first, first, "an owner equals itself"); + assert_ne!(hash_of(&first), hash_of(&second), "distinct owners must hash apart"); + assert_eq!(first.min_threads(), 4); +} + +#[test] +fn numa_nodes_compare_and_hash_by_value() { + assert_eq!(NumaNode::new(3), NumaNode::new(3)); + assert_ne!(NumaNode::new(3), NumaNode::new(4)); + assert_eq!( + hash_of(&NumaNode::new(3)), + hash_of(&NumaNode::new(3)), + "equal nodes must hash equally" + ); +} + +/// Without `std` the thread id is gone, so equality and hashing cover the owner and the +/// NUMA node alone. `Thread` cannot be constructed here, which is exactly why that +/// narrowing is unobservable in a real program, so pin the shape by size instead: anything +/// wider than the two remaining fields means the id came back. +#[cfg(not(feature = "std"))] +#[test] +fn without_std_a_thread_carries_no_thread_id() { + let align = align_of::(); + let expected = (size_of::() + size_of::()).div_ceil(align) * align; + + assert_eq!(size_of::(), expected, "a no_std `Thread` is its owner and its NUMA node"); +} + +#[cfg(feature = "std")] +#[test] +fn with_std_every_component_takes_part_in_equality() { + use std::thread; + + let id = thread::current().id(); + let owner = Owner::new(1); + let numa_node = NumaNode::new(0); + let mine = Thread::new(owner.clone(), id, numa_node); + + assert_eq!(mine, Thread::new(owner.clone(), id, numa_node)); + assert_eq!(hash_of(&mine), hash_of(&Thread::new(owner.clone(), id, numa_node))); + + assert_ne!(mine, Thread::new(Owner::new(1), id, numa_node), "the owner counts"); + assert_ne!(mine, Thread::new(owner.clone(), id, NumaNode::new(1)), "the NUMA node counts"); + + let elsewhere = thread::spawn(move || Thread::new(owner, thread::current().id(), numa_node)) + .join() + .unwrap(); + + assert_ne!(mine, elsewhere, "the thread id counts"); +}