Skip to main content

wasmer_types/
store_id.rs

1use core::fmt::Display;
2use std::{
3    cell::Cell,
4    num::NonZeroUsize,
5    sync::atomic::{AtomicUsize, Ordering},
6};
7
8/// Unique ID to identify a context.
9///
10/// Every handle to an object managed by a context also contains the ID of the
11/// context. This is used to check that a handle is always used with the
12/// correct context.
13#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
14pub struct StoreId(NonZeroUsize);
15
16#[cfg(feature = "artifact-size")]
17impl loupe::MemoryUsage for StoreId {
18    fn size_of_val(&self, _visited: &mut dyn loupe::MemoryUsageTracker) -> usize {
19        std::mem::size_of_val(self)
20    }
21}
22
23impl StoreId {
24    /// Returns the raw [`NonZeroUsize`] value of this [`StoreId`].
25    pub fn as_raw(&self) -> NonZeroUsize {
26        self.0
27    }
28}
29
30/// Number of IDs each thread reserves from the global counter at a time.
31///
32/// `Default::default` used to hit a single global `AtomicUsize` on every
33/// call, so multiple `Store::new` callers on different cores would
34/// ping-pong the same cache line. With chunked allocation, each thread
35/// reserves `CHUNK_SIZE` consecutive IDs in one atomic step and then
36/// hands them out from a thread-local cursor with zero cross-thread
37/// traffic until the chunk is exhausted.
38///
39/// 256 keeps the global atomic out of the picture for any normal
40/// workload while leaving the total ID space unchanged: the global
41/// counter still grows by the same total amount, just in batches.
42const CHUNK_SIZE: usize = 256;
43
44/// Global pointer to the first ID of the next available chunk.
45///
46/// Starts at 1 so the first ID handed out is non-zero (the wrapper is
47/// `NonZeroUsize`).
48static NEXT_CHUNK_START: AtomicUsize = AtomicUsize::new(1);
49
50/// Per-thread chunk cursor. `next` is the ID we hand out on the next
51/// `Default::default` call; `end` is the exclusive upper bound of the
52/// currently-held chunk. `next == end` (initially `0 == 0`) signals
53/// that the thread must reserve a fresh chunk from the global counter.
54#[derive(Clone, Copy)]
55struct ChunkCursor {
56    next: usize,
57    end: usize,
58}
59
60impl ChunkCursor {
61    const EMPTY: Self = Self { next: 0, end: 0 };
62}
63
64thread_local! {
65    /// Per-thread cursor inside the currently-held chunk.
66    static LOCAL_CURSOR: Cell<ChunkCursor> = const { Cell::new(ChunkCursor::EMPTY) };
67}
68
69impl Default for StoreId {
70    // Allocates a unique ID for a new context.
71    fn default() -> Self {
72        // No overflow checking is needed here: the global counter is
73        // `AtomicUsize`. On 64-bit hosts, exhausting it at one
74        // `Store::new` per nanosecond on every core would still take
75        // centuries.
76        let raw = LOCAL_CURSOR.with(|cell| {
77            let mut cursor = cell.get();
78            if cursor.next == cursor.end {
79                cursor.next = NEXT_CHUNK_START.fetch_add(CHUNK_SIZE, Ordering::Relaxed);
80                cursor.end = cursor.next + CHUNK_SIZE;
81            }
82            let id = cursor.next;
83            cursor.next += 1;
84            cell.set(cursor);
85            id
86        });
87        Self(NonZeroUsize::new(raw).expect("chunked allocator never returns 0"))
88    }
89}
90
91impl Display for StoreId {
92    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
93        let val: usize = self.0.into();
94        if val == usize::MAX {
95            write!(f, "unknown")
96        } else {
97            write!(f, "{}", self.0)
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use std::collections::HashSet;
106    use std::sync::Mutex;
107    use std::thread;
108
109    /// Many sequential `default()` calls from one thread produce unique
110    /// IDs across at least three chunk boundaries (catches off-by-one
111    /// errors at the chunk seam).
112    #[test]
113    fn ids_unique_within_a_thread_across_chunk_boundaries() {
114        let n = CHUNK_SIZE * 3 + 17;
115        let mut seen = HashSet::with_capacity(n);
116        for _ in 0..n {
117            let id = StoreId::default();
118            assert!(
119                seen.insert(id.as_raw()),
120                "duplicate ID handed out within a single thread",
121            );
122        }
123    }
124
125    /// Two threads each pulling many chunks must never see overlap. This
126    /// is the property the global `fetch_add` is preserving: the chunks
127    /// are disjoint windows of the integer space.
128    #[test]
129    fn ids_unique_across_threads_under_load() {
130        const THREADS: usize = 16;
131        const PER_THREAD: usize = CHUNK_SIZE * 8;
132        let collected: Mutex<HashSet<NonZeroUsize>> =
133            Mutex::new(HashSet::with_capacity(THREADS * PER_THREAD));
134        thread::scope(|s| {
135            for _ in 0..THREADS {
136                s.spawn(|| {
137                    let mut local = Vec::with_capacity(PER_THREAD);
138                    for _ in 0..PER_THREAD {
139                        local.push(StoreId::default().as_raw());
140                    }
141                    let mut guard = collected.lock().unwrap();
142                    for id in local {
143                        assert!(
144                            guard.insert(id),
145                            "duplicate ID handed out across threads: {id}",
146                        );
147                    }
148                });
149            }
150        });
151        let total = collected.into_inner().unwrap().len();
152        assert_eq!(
153            total,
154            THREADS * PER_THREAD,
155            "expected every produced ID to be unique",
156        );
157    }
158
159    /// `StoreId` is `NonZeroUsize`-backed, so the allocator must never
160    /// hand out zero. The chunk start is initialised to 1 and chunk
161    /// reservations only ever increase it, so a zero would indicate a
162    /// regression in chunk bookkeeping.
163    #[test]
164    fn allocator_never_returns_zero() {
165        for _ in 0..10_000 {
166            let id = StoreId::default();
167            assert_ne!(id.as_raw().get(), 0);
168        }
169    }
170}