1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//! `wasm32-wasip1-threads` only: every thread's view of the shared memory size. Not a public
//! API; napi's allocator wrappers and napi-async-runtime's scheduler hook share it.
//!
//! # The V8 bug
//!
//! All threads of a `wasm32-wasip1-threads` addon share one wasm memory, and wasi-libc's
//! dlmalloc grows it (`memory.grow` in `sbrk`) from whichever thread runs out of heap. V8
//! updates the size on that thread only; every other thread keeps its old size until it
//! handles V8's grow interrupt, which a thread inside one long wasm activation does late. On
//! such a stale thread `memory.fill`, `memory.copy`, Liftoff atomics and atomic wait/notify are
//! bounds-checked against the old size, so they trap on pages another thread grew; on hosts
//! without V8's wasm trap handler (for example Node's `--disable-wasm-trap-handler`), plain
//! loads and stores do too. `memory.grow(0)` on the stale thread makes V8 reload the size and
//! returns it; `memory.size` returns the stale size and reloads nothing. V8 fixed it in
//! v8/v8@34241014663390c72e08c123faef6fedf395be8e. The evidence and the rejected alternatives
//! are in rolldown's `internal-docs/wasi-shared-memory-grow/design.md`.
//!
//! # The state
//!
//! - `LOCAL_PAGES` (thread-local): the size this thread's bounds were last refreshed to. It is
//! only set from the result of `memory.grow(0)` run on this thread, and memory never shrinks,
//! so it never exceeds the size V8 checks on this thread.
//! - `MAX_SEEN_PAGES`: the largest `LOCAL_PAGES` any thread has stored.
//!
//! napi's allocator wrappers call [`grow_zero`] under their lock, so a thread that grows the
//! memory publishes the new size before any other thread allocates. napi-async-runtime calls
//! [`refresh_if_behind`] at every scheduler handoff, because a task can allocate on one thread
//! and then fill or copy into that memory on another without allocating there.
//!
//! # Why in napi-sys
//!
//! Both users must read and write the same statics: with two copies, the hook would read a
//! `MAX_SEEN_PAGES` the allocator never writes, and silently never refresh. napi cannot depend
//! on napi-async-runtime (its default `napi` feature depends on napi, a cycle), and hosts build
//! napi-async-runtime without napi, so the state lives in napi-sys: napi already depends on it,
//! and napi-async-runtime can without napi. It defines no `#[no_mangle]` item and imports
//! nothing, so napi-free test binaries still link.
use ;
/// Wasm page size in bytes.
const PAGE: u64 = 65536;
/// Index of [`handoff_refreshes`] in napi's `napi_wasm_heap_sync_stat` export; 0-4 are napi's
/// allocator counters.
pub const HANDOFF_REFRESHES_STAT: u32 = 5;
/// Keeps a value alone in its cache line.
;
/// The largest `LOCAL_PAGES` any thread has stored.
///
/// Every scheduler handoff reads it, and napi's allocator lock is taken all the time, so the
/// two must not share a cache line. When they shared one (as in rolldown's first copy of this
/// code), the handoff check cost 0.89 ns instead of 0.24 ns (3.7x) while another thread took
/// the lock in a loop, and that thread ran 12% slower (node 24.21, arm64 with 128-byte lines).
/// 128 bytes also covers x86, which prefetches 64-byte lines in pairs.
static MAX_SEEN_PAGES: = Padded;
/// Refreshes run by [`refresh_if_behind`]. Only written on its cold path.
static HANDOFF_REFRESHES: AtomicU32 = new;
thread_local!
/// Whether a thread whose bounds were refreshed to `local_pages` must refresh before it
/// touches a block that ends at byte `end` (exclusive).
///
/// `end` is `u64` because a block may end exactly at 4 GiB, and the pages are multiplied as
/// `u64` because 65536 pages * 64 KiB overflows the 32-bit `usize` of wasm32.
pub const
// The largest wasm32 memory. Checked when this module compiles, so also on wasm32, where the
// same math in `usize` would overflow.
const _: = assert!;
/// Run `memory.grow(0)` on this thread: V8 reloads this thread's size and returns it. Records
/// it in `LOCAL_PAGES` and publishes it to `MAX_SEEN_PAGES`, then returns it.
/// Refresh when another thread has seen a larger memory than this thread.
///
/// Runs outside napi's allocator lock. Hot path: one atomic load and one thread-local load.
/// `LOCAL_PAGES` of this thread.
/// `MAX_SEEN_PAGES`, with `Acquire` to pair with the publish in [`grow_zero`].
/// How many refreshes [`refresh_if_behind`] has run, on every thread.