diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 67a7b173a0f70..7b5cf83889bfa 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -270,83 +270,84 @@ var LibraryPThread = { PThread.tlsInitFunctions.forEach((f) => f()); }, // Loads the WebAssembly module into the given Worker. - // onFinishedLoading: A callback function that will be called once all of - // the workers have been initialized and are - // ready to host pthreads. - loadWasmModuleToWorker: (worker) => new Promise((onFinishedLoading) => { - worker.onmessage = (e) => { - var d = e['data']; - var cmd = d.cmd; -#if PTHREADS_DEBUG - dbg(`main thread: received message '${cmd}' from worker. ${d}`); -#endif - - // If this message is intended to a recipient that is not the main - // thread, forward it to the target thread. - if (d.targetThread && d.targetThread != _pthread_self()) { - var targetWorker = PThread.pthreads[d.targetThread]; - if (targetWorker) { - targetWorker.postMessage(d, d.transferList); - } else { - err(`worker sent message (${cmd}) to pthread (${d.targetThread}) that no longer exists`); + // @returns: A promise the resolves once the worker has loaded the wasm module + // and is ready to run a pthread. + loadWasmModuleToWorker: (worker) => { + worker.loaded = new Promise((onFinishedLoading) => { + worker.onmessage = (e) => { + var d = e['data']; + var cmd = d.cmd; + #if PTHREADS_DEBUG + dbg(`main thread: received message '${cmd}' from worker. ${d}`); + #endif + + // If this message is intended to a recipient that is not the main + // thread, forward it to the target thread. + if (d.targetThread && d.targetThread != _pthread_self()) { + var targetWorker = PThread.pthreads[d.targetThread]; + if (targetWorker) { + targetWorker.postMessage(d, d.transferList); + } else { + err(`worker sent message (${cmd}) to pthread (${d.targetThread}) that no longer exists`); + } + return; } - return; - } - if (d === 'setimmediate') { - // Worker wants to postMessage() to itself to implement setImmediate() - // emulation. - worker.postMessage(d); - return; - } + if (d === 'setimmediate') { + // Worker wants to postMessage() to itself to implement setImmediate() + // emulation. + worker.postMessage(d); + return; + } - switch (cmd) { - case {{{ CMD_CHECK_MAILBOX }}}: - checkMailbox(); - break; - case {{{ CMD_SPAWN_THREAD }}}: - spawnThread(d); - break; - case {{{ CMD_CLEANUP_THREAD }}}: - // cleanupThread needs to be run via callUserCallback since it calls - // back into user code to free thread data. Without this it's possible - // the unwind or ExitStatus exception could escape here. - callUserCallback(() => cleanupThread(d.thread)); - break; + switch (cmd) { + case {{{ CMD_CHECK_MAILBOX }}}: + checkMailbox(); + break; + case {{{ CMD_SPAWN_THREAD }}}: + spawnThread(d); + break; + case {{{ CMD_CLEANUP_THREAD }}}: + // cleanupThread needs to be run via callUserCallback since it calls + // back into user code to free thread data. Without this it's possible + // the unwind or ExitStatus exception could escape here. + callUserCallback(() => cleanupThread(d.thread)); + break; #if MAIN_MODULE - case {{{ CMD_MARK_AS_FINISHED }}}: - markAsFinished(d.thread); - break; + case {{{ CMD_MARK_AS_FINISHED }}}: + markAsFinished(d.thread); + break; #endif - case {{{ CMD_LOADED }}}: + case {{{ CMD_LOADED }}}: #if ENVIRONMENT_MAY_BE_NODE - if (ENVIRONMENT_IS_NODE && !worker.strongref) { - // Once worker is loaded & idle, mark it as weakly referenced, - // so that mere existence of a Worker in the pool does not prevent - // Node.js from exiting the app. - worker.unref(); - } -#endif - onFinishedLoading(worker); - break; + if (ENVIRONMENT_IS_NODE && !worker.strongref) { + // Once worker is loaded & idle, mark it as weakly referenced, + // so that mere existence of a Worker in the pool does not prevent + // Node.js from exiting the app. + worker.unref(); + } +#endif + onFinishedLoading(); + break; #if ENVIRONMENT_MAY_BE_NODE - case {{{ CMD_UNCAUGHT_EXN }}}: - // Message handler for Node.js specific out-of-order behavior: - // https://github.com/nodejs/node/issues/59617 - // A pthread sent an uncaught exception event. Re-raise it on the main thread. - worker.onerror(d.error); - break; -#endif - case {{{ CMD_CALL_HANDLER }}}: - Module[d.handler](...d.args); - break; - default: - // The received message looks like something that should be handled by this message - // handler, (since there is a e.data.cmd field present), but is not one of the - // recognized commands: - if (cmd) err(`worker sent an unknown command ${cmd}`); - } - }; + case {{{ CMD_UNCAUGHT_EXN }}}: + // Message handler for Node.js specific out-of-order behavior: + // https://github.com/nodejs/node/issues/59617 + // A pthread sent an uncaught exception event. Re-raise it on the main thread. + worker.onerror(d.error); + break; +#endif + case {{{ CMD_CALL_HANDLER }}}: + Module[d.handler](...d.args); + break; + default: + // The received message looks like something that should be handled by this message + // handler, (since there is a e.data.cmd field present), but is not one of the + // recognized commands: + if (cmd) err(`worker sent an unknown command ${cmd}`); + } + }; + }); worker.onerror = (e) => { var message = 'worker sent an error!'; @@ -443,7 +444,9 @@ var LibraryPThread = { 'workerID': worker.workerID, #endif }); - }), + + return worker.loaded; + }, #if PTHREAD_POOL_SIZE async loadWasmModuleToAllWorkers() { @@ -730,7 +733,11 @@ var LibraryPThread = { #endif // Ask the worker to start executing its pthread entry point function. worker.postMessage(msg, threadParams.transferList); +#if ASYNCIFY + return worker.loaded; +#else return 0; +#endif }, _emscripten_init_main_thread_js: (tb) => { @@ -775,6 +782,11 @@ var LibraryPThread = { // allocations from __pthread_create_js we could also remove this. __pthread_create_js__noleakcheck: true, #endif + // Pthread creation is async when possible. This allows us to return to the + // event loop and wait for the Worker to be created. + // This is needed in browsers where synchronous worker creation is still not + // possible: + __pthread_create_js__async: 'auto', __pthread_create_js__deps: ['$spawnThread', '$pthreadCreateProxied', 'emscripten_has_threading_support', #if OFFSCREENCANVAS_SUPPORT diff --git a/src/postamble.js b/src/postamble.js index 37a06dc690940..4336c32e25011 100644 --- a/src/postamble.js +++ b/src/postamble.js @@ -78,7 +78,7 @@ var mainArgs = undefined; var ret = entryFunction(argc, {{{ to64('argv') }}}); #endif // STANDALONE_WASM -#if ASYNCIFY == 2 && !PROXY_TO_PTHREAD +#if ASYNCIFY == 2 // The current spec of JSPI returns a promise only if the function suspends // and a plain value otherwise. This will likely change: // https://github.com/WebAssembly/js-promise-integration/issues/11 diff --git a/test/decorators.py b/test/decorators.py index 78e14857c41cd..9a54c8a937120 100644 --- a/test/decorators.py +++ b/test/decorators.py @@ -603,6 +603,47 @@ def metafunc(self, mode, *args, **kwargs): return metafunc +def with_asyncify_and_jspi(func): + assert callable(func) + + @wraps(func) + def metafunc(self, jspi, *args, **kwargs): + if self.get_setting('WASM_ESM_INTEGRATION'): + self.skipTest('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY') + if jspi: + self.set_setting('JSPI') + self.require_jspi() + else: + self.set_setting('ASYNCIFY') + return func(self, *args, **kwargs) + + parameterize(metafunc, {'': (False,), + 'jspi': (True,)}) + return metafunc + + +def also_with_asyncify_and_jspi(func): + assert callable(func) + + @wraps(func) + def metafunc(self, asyncify, *args, **kwargs): + if asyncify and self.get_setting('WASM_ESM_INTEGRATION'): + self.skipTest('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY') + if asyncify == 2: + self.set_setting('JSPI') + self.require_jspi() + elif asyncify == 1: + self.set_setting('ASYNCIFY') + else: + assert asyncify == 0 + return func(self, *args, **kwargs) + + parameterize(metafunc, {'': (0,), + 'asyncify': (1,), + 'jspi': (2,)}) + return metafunc + + def parameterize(func, parameters): """Add additional parameterization to a test function. diff --git a/test/test_browser.py b/test/test_browser.py index c3fe8de49d17a..3337ee3a04fcc 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -48,6 +48,7 @@ ) from decorators import ( also_with_asan, + also_with_asyncify_and_jspi, also_with_fetch_streaming, also_with_minimal_runtime, also_with_pthreads, @@ -66,6 +67,7 @@ skip_if, skip_if_simple, with_all_sjlj, + with_asyncify_and_jspi, ) from tools import ports, shared, utils @@ -3666,6 +3668,10 @@ def test_pthread_pool_size_strict(self): expected='abort:Assertion failed: thrd_create(&t4, thread_main, NULL) == thrd_success', cflags=['-g2', '-pthread', '-sPTHREAD_POOL_SIZE=3', '-sPTHREAD_POOL_SIZE_STRICT=2']) + @with_asyncify_and_jspi + def test_pthread_asyncify(self): + self.btest_exit('pthread/test_pthread_printf.c', cflags=['-pthread']) + def test_pthread_in_pthread_pool_size_strict(self): # Check that it fails when there's a pthread creating another pthread. self.btest_exit('pthread/test_pthread_create_pthread.c', cflags=['-g2', '-pthread', '-sPTHREAD_POOL_SIZE=2', '-sPTHREAD_POOL_SIZE_STRICT=2']) @@ -3681,8 +3687,11 @@ def test_pthread_atomics(self, args): self.btest_exit('pthread/test_pthread_atomics.c', cflags=['-O3', '-pthread', '-sPTHREAD_POOL_SIZE=8', '-g1'] + args) # Test 64-bit atomics. + @also_with_asyncify_and_jspi def test_pthread_64bit_atomics(self): - self.btest_exit('pthread/test_pthread_64bit_atomics.c', cflags=['-O3', '-pthread', '-sPTHREAD_POOL_SIZE=8']) + if not self.get_setting('JSPI') and not self.get_setting('ASYNCIFY'): + self.set_setting('PTHREAD_POOL_SIZE', 8) + self.btest_exit('pthread/test_pthread_64bit_atomics.c', cflags=['-O3', '-pthread']) # Test 64-bit C++11 atomics. @also_with_pthreads diff --git a/test/test_core.py b/test/test_core.py index 1c437d8149c01..47a8e68db0db7 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -40,6 +40,7 @@ ) from decorators import ( all_engines, + also_with_asyncify_and_jspi, also_with_minimal_runtime, also_with_modularize, also_with_nodefs, @@ -76,6 +77,7 @@ with_all_eh_sjlj, with_all_fs, with_all_sjlj, + with_asyncify_and_jspi, with_env_modify, ) @@ -255,47 +257,6 @@ def decorated(self, dylink_reversed, *args, **kwargs): only_wasm2js = skip_if('only_wasm2js', lambda t: not t.is_wasm2js()) -def with_asyncify_and_jspi(func): - assert callable(func) - - @wraps(func) - def metafunc(self, jspi, *args, **kwargs): - if self.get_setting('WASM_ESM_INTEGRATION'): - self.skipTest('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY') - if jspi: - self.set_setting('JSPI') - self.require_jspi() - else: - self.set_setting('ASYNCIFY') - return func(self, *args, **kwargs) - - parameterize(metafunc, {'': (False,), - 'jspi': (True,)}) - return metafunc - - -def also_with_asyncify_and_jspi(func): - assert callable(func) - - @wraps(func) - def metafunc(self, asyncify, *args, **kwargs): - if asyncify and self.get_setting('WASM_ESM_INTEGRATION'): - self.skipTest('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY') - if asyncify == 2: - self.set_setting('JSPI') - self.require_jspi() - elif asyncify == 1: - self.set_setting('ASYNCIFY') - else: - assert asyncify == 0 - return func(self, *args, **kwargs) - - parameterize(metafunc, {'': (0,), - 'asyncify': (1,), - 'jspi': (2,)}) - return metafunc - - def also_with_wasm_workers(func): assert callable(func) diff --git a/tools/link.py b/tools/link.py index 3d3d838da3733..c525cb6d3e038 100644 --- a/tools/link.py +++ b/tools/link.py @@ -64,6 +64,7 @@ DEFAULT_ASYNCIFY_EXPORTS = [ 'main', '__main_argc_argv', + '_emscripten_proxy_main', ] VALID_ENVIRONMENTS = {'web', 'webview', 'worker', 'node', 'shell', 'worklet'} @@ -1694,6 +1695,14 @@ def limit_incoming_module_api(): settings.REQUIRED_EXPORTS += ['setThrew'] if settings.ASYNCIFY: + # Warn against using PTHREAD_POOL_SIZE with ASYNCIFY, since there should be no need for it. + if 'PTHREAD_POOL_SIZE' in user_settings: + diagnostics.warning('emcc', 'PTHREAD_POOL_SIZE should not be needed under ASYNCIFY') + # PTHREAD_POOL_SIZE_STRICT is completely ignored since the warning/error it controls + # does not make sense with ASYNCIFY + if 'PTHREAD_POOL_SIZE_STRICT' in user_settings: + diagnostics.warning('unused-command-line-argument', 'PTHREAD_POOL_SIZE_STRICT is ignored under ASYNCIFY') + settings.PTHREAD_POOL_SIZE_STRICT = 0 if not settings.ASYNCIFY_IGNORE_INDIRECT: # if we are not ignoring indirect calls, then we must treat invoke_* as if # they are indirect calls, since that is what they do - we can't see their