Skip to content

Commit

Permalink
Move emscripten_atomic_wait_async to atomic.h and add core testing
Browse files Browse the repository at this point in the history
This API was orininally added for wasm workers but is not wasm worker
specific.

Followup to #20381.
  • Loading branch information
sbc100 committed Oct 23, 2023
1 parent 6ba6be6 commit 06f2e36
Show file tree
Hide file tree
Showing 22 changed files with 397 additions and 302 deletions.
151 changes: 151 additions & 0 deletions src/library_atomic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @license
* Copyright 2023 The Emscripten Authors
* SPDX-License-Identifier: MIT
*/

assert(SHARED_MEMORY);

addToLibrary({
// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync:
// https://www.chromestatus.com/feature/6243382101803008
// However its implementation is faulty:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1467846
// And at the time of writing, no other browser has it either.
#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
// Partially polyfill Atomics.waitAsync() if not available in the browser.
// Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is
// broken until Chrome 91, see:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
// https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md
// This polyfill performs polling with setTimeout() to observe a change in the
// target memory location.
$polyfillWaitAsync__postset: `if (!Atomics.waitAsync || (typeof navigator !== 'undefined' && navigator.userAgent && jstoi_q((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91)) {
let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/];
function __Atomics_pollWaitAsyncAddresses() {
let now = performance.now();
let l = __Atomics_waitAsyncAddresses.length;
for (let i = 0; i < l; ++i) {
let a = __Atomics_waitAsyncAddresses[i];
let expired = (now > a[3]);
let awoken = (Atomics.load(a[0], a[1]) != a[2]);
if (expired || awoken) {
__Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l];
__Atomics_waitAsyncAddresses.length = l;
a[4](awoken ? 'ok': 'timed-out');
}
}
if (l) {
// If we still have addresses to wait, loop the timeout handler to continue polling.
setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
}
}
#if ASSERTIONS && WASM_WORKERS
if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.');
#endif
/**
* @param {number=} maxWaitMilliseconds
*/
Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
let val = Atomics.load(i32a, index);
if (val != value) return { async: false, value: 'not-equal' };
if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };
maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);
let promiseResolve;
let promise = new Promise((resolve) => { promiseResolve = resolve; });
if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
__Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);
return { async: true, value: promise };
};
}`,
$polyfillWaitAsync__deps: ['$jstoi_q'],
#endif

$polyfillWaitAsync__internal: true,
$polyfillWaitAsync: () => {
// nop, used for its postset to ensure `Atomics.waitAsync()` polyfill is
// included exactly once and only included when needed.
// Any function using Atomics.waitAsync should depend on this.
},

$atomicWaitStates__internal: true,
$atomicWaitStates: ['ok', 'not-equal', 'timed-out'],
$liveAtomicWaitAsyncs: {},
$liveAtomicWaitAsyncs__internal: true,
$liveAtomicWaitAsyncCounter: 0,
$liveAtomicWaitAsyncCounter__internal: true,

emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$polyfillWaitAsync', '$callUserCallback'],
emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => {
let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
if (!wait.async) return atomicWaitStates.indexOf(wait.value);
// Increment waitAsync generation counter, account for wraparound in case
// application does huge amounts of waitAsyncs per second (not sure if
// possible?)
// Valid counterrange: 0...2^31-1
let counter = liveAtomicWaitAsyncCounter;
liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0);
liveAtomicWaitAsyncs[counter] = addr;
{{{ runtimeKeepalivePush() }}}
wait.value.then((value) => {
if (liveAtomicWaitAsyncs[counter]) {
{{{ runtimeKeepalivePop() }}}
delete liveAtomicWaitAsyncs[counter];
callUserCallback(() => {{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData));
}
});
return -counter;
},

emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_wait_async: (waitToken) => {
#if ASSERTIONS
if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) {
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()');
} else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) {
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()');
} else if (waitToken > 0) {
warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`);
}
#endif
var address = liveAtomicWaitAsyncs[waitToken];
if (address) {
// Notify the waitAsync waiters on the memory location, so that JavaScript
// garbage collection can occur.
// See https://github.com/WebAssembly/threads/issues/176
// This has the unfortunate effect of causing spurious wakeup of all other
// waiters at the address (which causes a small performance loss).
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
delete liveAtomicWaitAsyncs[waitToken];
{{{ runtimeKeepalivePop() }}}
return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
}
// This waitToken does not exist.
return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}};
},

emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_all_wait_asyncs: () => {
let waitAsyncs = Object.values(liveAtomicWaitAsyncs);
waitAsyncs.forEach((address) => {
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
});
liveAtomicWaitAsyncs = {};
return waitAsyncs.length;
},

emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => {
let numCancelled = 0;
Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => {
if (liveAtomicWaitAsyncs[waitToken] == address) {
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
delete liveAtomicWaitAsyncs[waitToken];
numCancelled++;
}
});
return numCancelled;
},
});
137 changes: 6 additions & 131 deletions src/library_wasm_worker.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* @license
* Copyright 2023 The Emscripten Authors
* SPDX-License-Identifier: MIT
*/

{{{
global.captureModuleArg = () => MODULARIZE ? '' : 'self.Module=d;';
global.instantiateModule = () => MODULARIZE ? `${EXPORT_NAME}(d);` : '';
Expand Down Expand Up @@ -241,137 +247,6 @@ if (ENVIRONMENT_IS_WASM_WORKER) {
_wasmWorkers[id].postMessage({'_wsc': funcPtr, 'x': readEmAsmArgs(sigPtr, varargs) });
},

$atomicWaitStates: "['ok', 'not-equal', 'timed-out']",

// Chrome 87 (and hence Edge 87) shipped Atomics.waitAsync (https://www.chromestatus.com/feature/6243382101803008)
// However its implementation is faulty: https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it, https://bugzilla.mozilla.org/show_bug.cgi?id=1467846
// And at the time of writing, no other browser has it either.
#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
// Partially polyfill Atomics.waitAsync() if not available in the browser.
// Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is
// broken until Chrome 91, see
// https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
// https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md
// This polyfill performs polling with setTimeout() to observe a change in the
// target memory location.
$polyfillWaitAsync__deps: ['$jstoi_q'],
$polyfillWaitAsync__postset: `if (!Atomics.waitAsync || (typeof navigator !== 'undefined' && navigator.userAgent && jstoi_q((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91)) {
let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/];
function __Atomics_pollWaitAsyncAddresses() {
let now = performance.now();
let l = __Atomics_waitAsyncAddresses.length;
for (let i = 0; i < l; ++i) {
let a = __Atomics_waitAsyncAddresses[i];
let expired = (now > a[3]);
let awoken = (Atomics.load(a[0], a[1]) != a[2]);
if (expired || awoken) {
__Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l];
__Atomics_waitAsyncAddresses.length = l;
a[4](awoken ? 'ok': 'timed-out');
}
}
if (l) {
// If we still have addresses to wait, loop the timeout handler to continue polling.
setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
}
}
#if ASSERTIONS
if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.');
#endif
Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
let val = Atomics.load(i32a, index);
if (val != value) return { async: false, value: 'not-equal' };
if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };
maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);
let promiseResolve;
let promise = new Promise((resolve) => { promiseResolve = resolve; });
if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
__Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);
return { async: true, value: promise };
};
}`,
#endif

$polyfillWaitAsync__internal: true,
$polyfillWaitAsync: () => {
// nop, used for its postset to ensure `Atomics.waitAsync()` polyfill is
// included exactly once and only included when needed.
// Any function using Atomics.waitAsync should depend on this.
},

$liveAtomicWaitAsyncs: {},
$liveAtomicWaitAsyncCounter: 0,

emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$polyfillWaitAsync'],
emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => {
let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
if (!wait.async) return atomicWaitStates.indexOf(wait.value);
// Increment waitAsync generation counter, account for wraparound in case
// application does huge amounts of waitAsyncs per second (not sure if
// possible?)
// Valid counterrange: 0...2^31-1
let counter = liveAtomicWaitAsyncCounter;
liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0);
liveAtomicWaitAsyncs[counter] = addr;
wait.value.then((value) => {
if (liveAtomicWaitAsyncs[counter]) {
delete liveAtomicWaitAsyncs[counter];
{{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData);
}
});
return -counter;
},

emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_wait_async: (waitToken) => {
#if ASSERTIONS
if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) {
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()');
} else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) {
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()');
} else if (waitToken > 0) {
warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`);
}
#endif
var address = liveAtomicWaitAsyncs[waitToken];
if (address) {
// Notify the waitAsync waiters on the memory location, so that JavaScript
// garbage collection can occur.
// See https://github.com/WebAssembly/threads/issues/176
// This has the unfortunate effect of causing spurious wakeup of all other
// waiters at the address (which causes a small performance loss).
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
delete liveAtomicWaitAsyncs[waitToken];
return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
}
// This waitToken does not exist.
return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}};
},

emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_all_wait_asyncs: () => {
let waitAsyncs = Object.values(liveAtomicWaitAsyncs);
waitAsyncs.forEach((address) => {
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
});
liveAtomicWaitAsyncs = {};
return waitAsyncs.length;
},

emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'],
emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => {
let numCancelled = 0;
Object.keys(liveAtomicWaitAsyncs).forEach((waitToken) => {
if (liveAtomicWaitAsyncs[waitToken] == address) {
Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
delete liveAtomicWaitAsyncs[waitToken];
numCancelled++;
}
});
return numCancelled;
},

emscripten_navigator_hardware_concurrency: () => {
#if ENVIRONMENT_MAY_BE_NODE
if (ENVIRONMENT_IS_NODE) return require('os').cpus().length;
Expand Down
4 changes: 4 additions & 0 deletions src/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ global.LibraryManager = {
libraries.push('library_lz4.js');
}

if (SHARED_MEMORY) {
libraries.push('library_atomic.js');
}

if (MAX_WEBGL_VERSION >= 2) {
// library_webgl2.js must be included only after library_webgl.js, so if we are
// about to include library_webgl2.js, first squeeze in library_webgl.js.
Expand Down
67 changes: 67 additions & 0 deletions system/include/emscripten/atomic.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

#include <inttypes.h>

#include <emscripten/em_types.h>

#ifdef __cplusplus
extern "C" {
#endif
Expand Down Expand Up @@ -223,6 +225,71 @@ _EM_INLINE int64_t emscripten_atomic_notify(void *addr __attribute__((nonnull)),
return __builtin_wasm_memory_atomic_notify((int*)addr, count);
}

#define EMSCRIPTEN_WAIT_ASYNC_INFINITY __builtin_inf()

// Represents a pending 'Atomics.waitAsync' wait operation.
#define ATOMICS_WAIT_TOKEN_T int32_t

#define EMSCRIPTEN_IS_VALID_WAIT_TOKEN(token) ((token) <= 0)

// Issues the JavaScript 'Atomics.waitAsync' instruction:
// performs an asynchronous wait operation on the main thread. If the given
// 'addr' contains 'value', issues a deferred wait that will invoke the
// specified callback function 'asyncWaitFinished' once that address has been
// notified by another thread.
// NOTE: Unlike functions emscripten_atomic_wait_u32() and
// emscripten_atomic_wait_u64() which take in the wait timeout parameter as int64
// nanosecond units, this function takes in the wait timeout parameter as double
// millisecond units. See https://github.com/WebAssembly/threads/issues/175 for
// more information.
// Pass in maxWaitMilliseconds == EMSCRIPTEN_WAIT_ASYNC_INFINITY
// (==__builtin_inf()) to wait infinitely long.
// Returns one of:
// - ATOMICS_WAIT_NOT_EQUAL if the waitAsync operation could not be registered
// since the memory value did not contain the value 'value'.
// - ATOMICS_WAIT_TIMED_OUT if the waitAsync operation timeout parameter was <= 0.
// - Any other value: denotes a 'wait token' that can be passed to function
// emscripten_atomic_cancel_wait_async() to unregister an asynchronous wait.
// You can use the macro EMSCRIPTEN_IS_VALID_WAIT_TOKEN(retval) to check if
// this function returned a valid wait token.
ATOMICS_WAIT_TOKEN_T emscripten_atomic_wait_async(void *addr __attribute__((nonnull)),
uint32_t value,
void (*asyncWaitFinished)(int32_t *addr, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void *userData) __attribute__((nonnull)),
void *userData,
double maxWaitMilliseconds);

// Unregisters a pending Atomics.waitAsync operation that was established via a
// call to emscripten_atomic_wait_async() in the calling thread. Pass in the
// wait token handle that was received as the return value from the wait
// function. Returns EMSCRIPTEN_RESULT_SUCCESS if the cancellation was
// successful, or EMSCRIPTEN_RESULT_INVALID_PARAM if the asynchronous wait has
// already resolved prior and the callback has already been called.
// NOTE: Because of needing to work around issue
// https://github.com/WebAssembly/threads/issues/176, calling this function has
// an effect of introducing spurious wakeups to any other threads waiting on the
// same address that the async wait denoted by the token does. This means that
// in order to safely use this function, the mechanisms used in any wait code on
// that address must be written to be spurious wakeup safe. (this is the case
// for all the synchronization primitives declared in this header, but if you
// are rolling out your own, you need to be aware of this). If
// https://github.com/tc39/proposal-cancellation/issues/29 is resolved, then the
// spurious wakeups can be avoided.
EMSCRIPTEN_RESULT emscripten_atomic_cancel_wait_async(ATOMICS_WAIT_TOKEN_T waitToken);

// Cancels all pending async waits in the calling thread. Because of
// https://github.com/WebAssembly/threads/issues/176, if you are using
// asynchronous waits in your application, and need to be able to let GC reclaim
// Wasm heap memory when deinitializing an application, you *must* call this
// function to help the GC unpin all necessary memory. Otherwise, you can wrap
// the Wasm content in an iframe and unload the iframe to let GC occur.
// (navigating away from the page or closing that tab will also naturally
// reclaim the memory)
int emscripten_atomic_cancel_all_wait_asyncs(void);

// Cancels all pending async waits in the calling thread to the given memory
// address. Returns the number of async waits canceled.
int emscripten_atomic_cancel_all_wait_asyncs_at_address(void *addr __attribute__((nonnull)));

#undef _EM_INLINE

#ifdef __cplusplus
Expand Down
Loading

0 comments on commit 06f2e36

Please sign in to comment.