Skip to content

Commit 1fe2c44

Browse files
committed
Move emscripten_atomic_wait_async to atomic.h and add core testing
This API was orininally added for wasm workers but is not wasm worker specific. Followup to #20381.
1 parent 4b33521 commit 1fe2c44

22 files changed

+399
-303
lines changed

emcc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2503,7 +2503,7 @@ def phase_linker_setup(options, state, newargs):
25032503
# set location of Wasm Worker bootstrap JS file
25042504
if settings.WASM_WORKERS == 1:
25052505
settings.WASM_WORKER_FILE = unsuffixed(os.path.basename(target)) + '.ww.js'
2506-
settings.JS_LIBRARIES.append((0, shared.path_from_root('src', 'library_wasm_worker.js')))
2506+
settings.JS_LIBRARIES.append((0, 'library_wasm_worker.js'))
25072507

25082508
# Set min browser versions based on certain settings such as WASM_BIGINT,
25092509
# PTHREADS, AUDIO_WORKLET

src/library_atomic.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* @license
3+
* Copyright 2023 The Emscripten Authors
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
assert(SHARED_MEMORY);
8+
9+
addToLibrary({
10+
// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync:
11+
// https://www.chromestatus.com/feature/6243382101803008
12+
// However its implementation is faulty:
13+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
14+
// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it:
15+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1467846
16+
// And at the time of writing, no other browser has it either.
17+
#if MIN_EDGE_VERSION < 91 || MIN_CHROME_VERSION < 91 || MIN_SAFARI_VERSION != TARGET_NOT_SUPPORTED || MIN_FIREFOX_VERSION != TARGET_NOT_SUPPORTED || ENVIRONMENT_MAY_BE_NODE
18+
// Partially polyfill Atomics.waitAsync() if not available in the browser.
19+
// Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is
20+
// broken until Chrome 91, see:
21+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
22+
// https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md
23+
// This polyfill performs polling with setTimeout() to observe a change in the
24+
// target memory location.
25+
$polyfillWaitAsync__postset: `if (!Atomics.waitAsync || (typeof navigator !== 'undefined' && navigator.userAgent && jstoi_q((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91)) {
26+
let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/];
27+
function __Atomics_pollWaitAsyncAddresses() {
28+
let now = performance.now();
29+
let l = __Atomics_waitAsyncAddresses.length;
30+
for (let i = 0; i < l; ++i) {
31+
let a = __Atomics_waitAsyncAddresses[i];
32+
let expired = (now > a[3]);
33+
let awoken = (Atomics.load(a[0], a[1]) != a[2]);
34+
if (expired || awoken) {
35+
__Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l];
36+
__Atomics_waitAsyncAddresses.length = l;
37+
a[4](awoken ? 'ok': 'timed-out');
38+
}
39+
}
40+
if (l) {
41+
// If we still have addresses to wait, loop the timeout handler to continue polling.
42+
setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
43+
}
44+
}
45+
#if ASSERTIONS && WASM_WORKERS
46+
if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.');
47+
#endif
48+
/**
49+
* @param {number=} maxWaitMilliseconds
50+
*/
51+
Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
52+
let val = Atomics.load(i32a, index);
53+
if (val != value) return { async: false, value: 'not-equal' };
54+
if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };
55+
maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);
56+
let promiseResolve;
57+
let promise = new Promise((resolve) => { promiseResolve = resolve; });
58+
if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
59+
__Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);
60+
return { async: true, value: promise };
61+
};
62+
}`,
63+
#endif
64+
65+
$polyfillWaitAsync__internal: true,
66+
$polyfillWaitAsync__deps: ['$jstoi_q'],
67+
$polyfillWaitAsync: () => {
68+
// nop, used for its postset to ensure waitAsync() polyfill is included
69+
// exaclyt once.
70+
// Any function using Atomics.waitAsync should depend on this.
71+
},
72+
73+
$atomicWaitStates__internal: true,
74+
$atomicWaitStates: ['ok', 'not-equal', 'timed-out'],
75+
$liveAtomicWaitAsyncs: {},
76+
$liveAtomicWaitAsyncs__internal: true,
77+
$liveAtomicWaitAsyncCounter: 0,
78+
$liveAtomicWaitAsyncCounter__internal: true,
79+
80+
emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$polyfillWaitAsync', '$callUserCallback'],
81+
emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => {
82+
let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
83+
if (!wait.async) return atomicWaitStates.indexOf(wait.value);
84+
// Increment waitAsync generation counter, account for wraparound in case
85+
// application does huge amounts of waitAsyncs per second (not sure if
86+
// possible?)
87+
// Valid counterrange: 0...2^31-1
88+
let counter = liveAtomicWaitAsyncCounter;
89+
liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0);
90+
liveAtomicWaitAsyncs[counter] = addr;
91+
{{{ runtimeKeepalivePush() }}}
92+
wait.value.then((value) => {
93+
if (liveAtomicWaitAsyncs[counter]) {
94+
{{{ runtimeKeepalivePop() }}}
95+
delete liveAtomicWaitAsyncs[counter];
96+
callUserCallback(() => {{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData));
97+
}
98+
});
99+
return -counter;
100+
},
101+
102+
emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'],
103+
emscripten_atomic_cancel_wait_async: (waitToken) => {
104+
#if ASSERTIONS
105+
if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) {
106+
warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_NOT_EQUAL (1) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()');
107+
} else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) {
108+
warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_TIMED_OUT (2) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()');
109+
} else if (waitToken > 0) {
110+
warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`);
111+
}
112+
#endif
113+
var address = liveAtomicWaitAsyncs[waitToken];
114+
if (address) {
115+
// Notify the waitAsync waiters on the memory location, so that JavaScript
116+
// garbage collection can occur.
117+
// See https://github.com/WebAssembly/threads/issues/176
118+
// This has the unfortunate effect of causing spurious wakeup of all other
119+
// waiters at the address (which causes a small performance loss).
120+
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
121+
delete liveAtomicWaitAsyncs[waitToken];
122+
{{{ runtimeKeepalivePop() }}}
123+
return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
124+
}
125+
// This waitToken does not exist.
126+
return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}};
127+
},
128+
129+
emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'],
130+
emscripten_atomic_cancel_all_wait_asyncs: () => {
131+
let waitAsyncs = Object.values(liveAtomicWaitAsyncs);
132+
waitAsyncs.forEach((address) => {
133+
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
134+
});
135+
liveAtomicWaitAsyncs = {};
136+
return waitAsyncs.length;
137+
},
138+
139+
emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'],
140+
emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => {
141+
let numCancelled = 0;
142+
Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => {
143+
if (liveAtomicWaitAsyncs[waitToken] == address) {
144+
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
145+
delete liveAtomicWaitAsyncs[waitToken];
146+
numCancelled++;
147+
}
148+
});
149+
return numCancelled;
150+
},
151+
});

src/library_wasm_worker.js

Lines changed: 8 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright 2023 The Emscripten Authors
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
17
{{{
28
global.captureModuleArg = () => MODULARIZE ? '' : 'self.Module=d;';
39
global.instantiateModule = () => MODULARIZE ? `${EXPORT_NAME}(d);` : '';
@@ -230,137 +236,6 @@ if (ENVIRONMENT_IS_WASM_WORKER) {
230236
_wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': readEmAsmArgs(sigPtr, varargs) });
231237
},
232238

233-
$atomicWaitStates: "['ok', 'not-equal', 'timed-out']",
234-
235-
// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync (https://www.chromestatus.com/feature/6243382101803008)
236-
// However its implementation is faulty: https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
237-
// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it, https://bugzilla.mozilla.org/show_bug.cgi?id=1467846
238-
// And at the time of writing, no other browser has it either.
239-
#if MIN_EDGE_VERSION < 91 || MIN_CHROME_VERSION < 91 || MIN_SAFARI_VERSION != TARGET_NOT_SUPPORTED || MIN_FIREFOX_VERSION != TARGET_NOT_SUPPORTED || ENVIRONMENT_MAY_BE_NODE
240-
// Partially polyfill Atomics.waitAsync() if not available in the browser.
241-
// Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is
242-
// broken until Chrome 91, see
243-
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
244-
// https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md
245-
// This polyfill performs polling with setTimeout() to observe a change in the
246-
// target memory location.
247-
emscripten_atomic_wait_async__postset: `if (!Atomics.waitAsync || (typeof navigator !== 'undefined' && navigator.userAgent && jstoi_q((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91)) {
248-
let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/];
249-
function __Atomics_pollWaitAsyncAddresses() {
250-
let now = performance.now();
251-
let l = __Atomics_waitAsyncAddresses.length;
252-
for (let i = 0; i < l; ++i) {
253-
let a = __Atomics_waitAsyncAddresses[i];
254-
let expired = (now > a[3]);
255-
let awoken = (Atomics.load(a[0], a[1]) != a[2]);
256-
if (expired || awoken) {
257-
__Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l];
258-
__Atomics_waitAsyncAddresses.length = l;
259-
a[4](awoken ? 'ok': 'timed-out');
260-
}
261-
}
262-
if (l) {
263-
// If we still have addresses to wait, loop the timeout handler to continue polling.
264-
setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
265-
}
266-
}
267-
#if ASSERTIONS
268-
if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.');
269-
#endif
270-
Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
271-
let val = Atomics.load(i32a, index);
272-
if (val != value) return { async: false, value: 'not-equal' };
273-
if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };
274-
maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);
275-
let promiseResolve;
276-
let promise = new Promise((resolve) => { promiseResolve = resolve; });
277-
if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
278-
__Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);
279-
return { async: true, value: promise };
280-
};
281-
}`,
282-
283-
// These dependencies are artificial, issued so that we still get the
284-
// waitAsync polyfill emitted if code only calls
285-
// emscripten_lock/semaphore_async_acquire() but not
286-
// emscripten_atomic_wait_async() directly.
287-
emscripten_lock_async_acquire__deps: ['emscripten_atomic_wait_async'],
288-
emscripten_semaphore_async_acquire__deps: ['emscripten_atomic_wait_async'],
289-
290-
#endif
291-
292-
$liveAtomicWaitAsyncs: '{}',
293-
$liveAtomicWaitAsyncCounter: '0',
294-
295-
emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$jstoi_q'],
296-
emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => {
297-
let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
298-
if (!wait.async) return atomicWaitStates.indexOf(wait.value);
299-
// Increment waitAsync generation counter, account for wraparound in case
300-
// application does huge amounts of waitAsyncs per second (not sure if
301-
// possible?)
302-
// Valid counterrange: 0...2^31-1
303-
let counter = liveAtomicWaitAsyncCounter;
304-
liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0);
305-
liveAtomicWaitAsyncs[counter] = addr;
306-
wait.value.then((value) => {
307-
if (liveAtomicWaitAsyncs[counter]) {
308-
delete liveAtomicWaitAsyncs[counter];
309-
{{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData);
310-
}
311-
});
312-
return -counter;
313-
},
314-
315-
emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'],
316-
emscripten_atomic_cancel_wait_async: (waitToken) => {
317-
#if ASSERTIONS
318-
if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) {
319-
warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_NOT_EQUAL (1) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()');
320-
} else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) {
321-
warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_TIMED_OUT (2) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()');
322-
} else if (waitToken > 0) {
323-
warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`);
324-
}
325-
#endif
326-
var address = liveAtomicWaitAsyncs[waitToken];
327-
if (address) {
328-
// Notify the waitAsync waiters on the memory location, so that JavaScript
329-
// garbage collection can occur.
330-
// See https://github.com/WebAssembly/threads/issues/176
331-
// This has the unfortunate effect of causing spurious wakeup of all other
332-
// waiters at the address (which causes a small performance loss).
333-
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
334-
delete liveAtomicWaitAsyncs[waitToken];
335-
return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
336-
}
337-
// This waitToken does not exist.
338-
return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}};
339-
},
340-
341-
emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'],
342-
emscripten_atomic_cancel_all_wait_asyncs: () => {
343-
let waitAsyncs = Object.values(liveAtomicWaitAsyncs);
344-
waitAsyncs.forEach((address) => {
345-
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
346-
});
347-
liveAtomicWaitAsyncs = {};
348-
return waitAsyncs.length;
349-
},
350-
351-
emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'],
352-
emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => {
353-
let numCancelled = 0;
354-
Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => {
355-
if (liveAtomicWaitAsyncs[waitToken] == address) {
356-
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
357-
delete liveAtomicWaitAsyncs[waitToken];
358-
numCancelled++;
359-
}
360-
});
361-
return numCancelled;
362-
},
363-
364239
emscripten_navigator_hardware_concurrency: () => {
365240
#if ENVIRONMENT_MAY_BE_NODE
366241
if (ENVIRONMENT_IS_NODE) return require('os').cpus().length;
@@ -372,6 +247,7 @@ Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
372247
return Atomics.isLockFree(width);
373248
},
374249

250+
emscripten_lock_async_acquire__deps: ['$polyfillWaitAsync'],
375251
emscripten_lock_async_acquire: (lock, asyncWaitFinished, userData, maxWaitMilliseconds) => {
376252
let dispatch = (val, ret) => {
377253
setTimeout(() => {
@@ -393,6 +269,7 @@ Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
393269
tryAcquireLock();
394270
},
395271

272+
emscripten_semaphore_async_acquire__deps: ['$polyfillWaitAsync'],
396273
emscripten_semaphore_async_acquire: (sem, num, asyncWaitFinished, userData, maxWaitMilliseconds) => {
397274
let dispatch = (idx, ret) => {
398275
setTimeout(() => {

src/modules.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ global.LibraryManager = {
153153
libraries.push('library_lz4.js');
154154
}
155155

156+
if (SHARED_MEMORY) {
157+
libraries.push('library_atomic.js');
158+
}
159+
156160
if (MAX_WEBGL_VERSION >= 2) {
157161
// library_webgl2.js must be included only after library_webgl.js, so if we are
158162
// about to include library_webgl2.js, first squeeze in library_webgl.js.

0 commit comments

Comments
 (0)