Skip to content

Commit 57091df

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 a3ab41a commit 57091df

16 files changed

+367
-297
lines changed

src/library_atomic.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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
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+
Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
49+
let val = Atomics.load(i32a, index);
50+
if (val != value) return { async: false, value: 'not-equal' };
51+
if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };
52+
maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);
53+
let promiseResolve;
54+
let promise = new Promise((resolve) => { promiseResolve = resolve; });
55+
if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
56+
__Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);
57+
return { async: true, value: promise };
58+
};
59+
}`,
60+
#endif
61+
62+
$polyfillWaitAsync__internal: true,
63+
$polyfillWaitAsync: () => {
64+
// nop, used for its postset to ensure waitAsync() polyfill is included
65+
// exaclyt once.
66+
// Any function using Atomics.waitAsync should depend on this.
67+
},
68+
69+
$atomicWaitStates__internal: true,
70+
$atomicWaitStates: ['ok', 'not-equal', 'timed-out'],
71+
$liveAtomicWaitAsyncs: {},
72+
$liveAtomicWaitAsyncs__internal: true,
73+
$liveAtomicWaitAsyncCounter: 0,
74+
$liveAtomicWaitAsyncCounter__internal: true,
75+
76+
emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$jstoi_q', '$polyfillWaitAsync'],
77+
emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => {
78+
let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
79+
if (!wait.async) return atomicWaitStates.indexOf(wait.value);
80+
// Increment waitAsync generation counter, account for wraparound in case
81+
// application does huge amounts of waitAsyncs per second (not sure if
82+
// possible?)
83+
// Valid counterrange: 0...2^31-1
84+
let counter = liveAtomicWaitAsyncCounter;
85+
liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0);
86+
liveAtomicWaitAsyncs[counter] = addr;
87+
wait.value.then((value) => {
88+
if (liveAtomicWaitAsyncs[counter]) {
89+
delete liveAtomicWaitAsyncs[counter];
90+
{{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData);
91+
}
92+
});
93+
return -counter;
94+
},
95+
96+
emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'],
97+
emscripten_atomic_cancel_wait_async: (waitToken) => {
98+
#if ASSERTIONS
99+
if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) {
100+
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()');
101+
} else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) {
102+
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()');
103+
} else if (waitToken > 0) {
104+
warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`);
105+
}
106+
#endif
107+
var address = liveAtomicWaitAsyncs[waitToken];
108+
if (address) {
109+
// Notify the waitAsync waiters on the memory location, so that JavaScript
110+
// garbage collection can occur.
111+
// See https://github.com/WebAssembly/threads/issues/176
112+
// This has the unfortunate effect of causing spurious wakeup of all other
113+
// waiters at the address (which causes a small performance loss).
114+
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
115+
delete liveAtomicWaitAsyncs[waitToken];
116+
return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
117+
}
118+
// This waitToken does not exist.
119+
return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}};
120+
},
121+
122+
emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'],
123+
emscripten_atomic_cancel_all_wait_asyncs: () => {
124+
let waitAsyncs = Object.values(liveAtomicWaitAsyncs);
125+
waitAsyncs.forEach((address) => {
126+
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
127+
});
128+
liveAtomicWaitAsyncs = {};
129+
return waitAsyncs.length;
130+
},
131+
132+
emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'],
133+
emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => {
134+
let numCancelled = 0;
135+
Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => {
136+
if (liveAtomicWaitAsyncs[waitToken] == address) {
137+
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
138+
delete liveAtomicWaitAsyncs[waitToken];
139+
numCancelled++;
140+
}
141+
});
142+
return numCancelled;
143+
},
144+
});

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)