diff --git a/.circleci/config.yml b/.circleci/config.yml index e45d3e8c5bb07..30b2f6efd28ae 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1093,6 +1093,7 @@ jobs: core0.test_pthread_join_and_asyncify core0.test_async_ccall_promise_jspi* core0.test_cubescript_jspi + core0.test_pthread_wait_suspending* core0.test_poll_blocking_asyncify_jspi wasm64.test_pthread_join_and_asyncify " diff --git a/src/lib/libatomic.js b/src/lib/libatomic.js index b7366d226b6f2..cfe0cdea52527 100644 --- a/src/lib/libatomic.js +++ b/src/lib/libatomic.js @@ -74,6 +74,15 @@ addToLibrary({ // Any function using Atomics.waitAsync should depend on this. }, +#if ASYNCIFY + emscripten_atomic_wait_suspending__async: 'auto', + emscripten_atomic_wait_suspending__deps: ['$polyfillWaitAsync', '$atomicWaitStates'], + emscripten_atomic_wait_suspending: async (addr, val, maxWaitMilliseconds) => { + var wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds); + return atomicWaitStates.indexOf(await wait.value) + }, +#endif + $atomicWaitStates__internal: true, $atomicWaitStates: ['ok', 'not-equal', 'timed-out'], $liveAtomicWaitAsyncs: {}, diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index 27e5607cd322b..3d17121526a05 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -592,6 +592,7 @@ sigs = { emscripten_atomic_cancel_all_wait_asyncs_at_address__sig: 'ip', emscripten_atomic_cancel_wait_async__sig: 'ii', emscripten_atomic_wait_async__sig: 'ipippd', + emscripten_atomic_wait_suspending__sig: 'ipid', emscripten_atomics_is_lock_free__sig: 'ii', emscripten_audio_context_quantum_size__sig: 'ii', emscripten_audio_context_sample_rate__sig: 'ii', diff --git a/system/include/emscripten/atomic.h b/system/include/emscripten/atomic.h index 34bb7f609a583..a9b52119deffa 100644 --- a/system/include/emscripten/atomic.h +++ b/system/include/emscripten/atomic.h @@ -301,6 +301,13 @@ int emscripten_atomic_cancel_all_wait_asyncs(void); // address. Returns the number of async waits canceled. int emscripten_atomic_cancel_all_wait_asyncs_at_address(void * _Nonnull addr); +// Like emscripten_atomic_wait_async, but suspends the current Wasm executation +// using JSPI/ASYNCIFY. +// This function is not available unless linking with -sJSPI or -sASYNCIFY. +ATOMICS_WAIT_TOKEN_T emscripten_atomic_wait_suspending(volatile void * _Nonnull addr, + uint32_t value, + double maxWaitMilliseconds); + // Returns the value of the expression "Atomics.isLockFree(byteWidth)": true if // the given memory access width can be accessed atomically, and false // otherwise. Generally will return true on 1, 2 and 4 byte accesses. On 8 byte diff --git a/test/atomic/test_wait_suspending.c b/test/atomic/test_wait_suspending.c new file mode 100644 index 0000000000000..2e60670dc8283 --- /dev/null +++ b/test/atomic/test_wait_suspending.c @@ -0,0 +1,95 @@ +#include +#include +#include +#include + +// Test emscripten_atomic_wait_suspending() function. + +_Atomic int addr = 1; + +void run_test() { + emscripten_out("worker running"); +#if __EMSCRIPTEN_WASM_WORKERS__ + emscripten_wasm_worker_sleep(1000 * 1000000ull); // Sleep one second. +#else + emscripten_thread_sleep(1000); // Sleep one second. +#endif + emscripten_out("worker: addr = 1234"); + addr = 1234; + emscripten_out("worker: notify async waiting main thread"); + emscripten_atomic_notify((int32_t*)&addr, 1); +} + + +// This test run in both wasm workers and pthreads mode +#ifdef __EMSCRIPTEN_WASM_WORKERS__ + +void worker_main() { + run_test(); +} + +#else + +pthread_t t; + +bool done = false; + +void keepalive(void* user_data) { + if (!done) { + emscripten_set_timeout(keepalive, 100, NULL); + } +} + +void* thread_main(void* arg) { + run_test(); + return 0; +} + +#endif + +int main() { + emscripten_out("main: creating worker"); + +#ifdef __EMSCRIPTEN_WASM_WORKERS__ + emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(4096); + emscripten_wasm_worker_post_function_v(worker, worker_main); +#else + pthread_create(&t, NULL, thread_main, NULL); + // This is bit of hack to keep the node process alive. Without this, + // the whole node process will exit when we suspend them main thread below. + emscripten_set_timeout(keepalive, 100, NULL); +#endif + + ATOMICS_WAIT_TOKEN_T ret; + + emscripten_out("Async waiting on address with unexpected value should return 'not-equal'"); + ret = emscripten_atomic_wait_suspending((int32_t*)&addr, 2, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(ret == ATOMICS_WAIT_NOT_EQUAL); + + emscripten_out("Waiting on address with unexpected value should return 'not-equal' also if timeout==0"); + ret = emscripten_atomic_wait_suspending((int32_t*)&addr, 2, 0); + assert(ret == ATOMICS_WAIT_NOT_EQUAL); + + emscripten_out("Waiting for 0 milliseconds should return 'timed-out'"); + ret = emscripten_atomic_wait_suspending((int32_t*)&addr, 1, 0); + assert(ret == ATOMICS_WAIT_TIMED_OUT); + + emscripten_out("Waiting for >0 milliseconds should also timeout"); + ret = emscripten_atomic_wait_suspending((int32_t*)&addr, 1, 10); + assert(ret == ATOMICS_WAIT_TIMED_OUT); + + emscripten_out("Waiting for infinitely long should return once we have been notified"); + ret = emscripten_atomic_wait_suspending((int32_t*)&addr, 1, EMSCRIPTEN_WAIT_ASYNC_INFINITY); + assert(ret == ATOMICS_WAIT_OK); + assert(addr == 1234); + +#ifdef __EMSCRIPTEN_WASM_WORKERS__ + emscripten_terminate_all_wasm_workers(); +#else + done = true; + pthread_join(t, NULL); +#endif + + emscripten_out("test finished"); + return 0; +} diff --git a/test/atomic/test_wait_suspending.out b/test/atomic/test_wait_suspending.out new file mode 100644 index 0000000000000..2adb5954f35d2 --- /dev/null +++ b/test/atomic/test_wait_suspending.out @@ -0,0 +1,10 @@ +main: creating worker +Async waiting on address with unexpected value should return 'not-equal' +Waiting on address with unexpected value should return 'not-equal' also if timeout==0 +Waiting for 0 milliseconds should return 'timed-out' +Waiting for >0 milliseconds should also timeout +Waiting for infinitely long should return once we have been notified +worker running +worker: addr = 1234 +worker: notify async waiting main thread +test finished diff --git a/test/test_core.py b/test/test_core.py index 2b9f2c78a0cdd..653accd5aeb68 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -2751,6 +2751,18 @@ def test_pthread_wait_async(self): self.set_setting('PROXY_TO_PTHREAD') self.do_run_in_out_file_test('atomic/test_wait_async.c') + # Include @requires_node_25 explictly here so that this test will be disabled + # by EMTEST_SKIP_NODE_25. Without this, the `requires_pthreads` and `requires_jspi` can + # end with conflicting requirements because we often run with both v8 (which satisfies + # the `requires_jspi` part have node 22 (which satisfies the `requires_pthreads` part). + # FIXME: This should not be needed. + @requires_node_25 + @requires_pthreads + @with_asyncify_and_jspi + @also_with_wasm_workers + def test_pthread_wait_suspending(self): + self.do_run_in_out_file_test('atomic/test_wait_suspending.c') + @requires_pthreads @also_with_minimal_runtime def test_pthread_run_on_main_thread(self):