diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index eabdc9e503a63..14a258e7d404a 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -259,74 +259,74 @@ 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 (cmd === 'checkMailbox') { - checkMailbox(); - } else if (cmd === 'spawnThread') { - spawnThread(d); - } else if (cmd === 'cleanupThread') { - // 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)); -#if MAIN_MODULE - } else if (cmd === 'markAsFinished') { - markAsFinished(d.thread); -#endif - } else if (cmd === 'loaded') { - worker.loaded = true; -#if ENVIRONMENT_MAY_BE_NODE && PTHREAD_POOL_SIZE - // Check that this worker doesn't have an associated pthread. - if (ENVIRONMENT_IS_NODE && !worker.pthread_ptr) { - // 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(); + if (cmd === 'checkMailbox') { + checkMailbox(); + } else if (cmd === 'spawnThread') { + spawnThread(d); + } else if (cmd === 'cleanupThread') { + // 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)); + #if MAIN_MODULE + } else if (cmd === 'markAsFinished') { + markAsFinished(d.thread); + #endif + } else if (cmd === 'loaded') { + #if ENVIRONMENT_MAY_BE_NODE && PTHREAD_POOL_SIZE + // Check that this worker doesn't have an associated pthread. + if (ENVIRONMENT_IS_NODE && !worker.pthread_ptr) { + // 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(); + } else if (d.target === 'setimmediate') { + // Worker wants to postMessage() to itself to implement setImmediate() + // emulation. + worker.postMessage(d); + #if ENVIRONMENT_MAY_BE_NODE + } else if (cmd === 'uncaughtException') { + // 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); + #endif + } else if (cmd === 'callHandler') { + Module[d.handler](...d.args); + } else if (cmd) { + // 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: + err(`worker sent an unknown command ${cmd}`); } -#endif - onFinishedLoading(worker); - } else if (d.target === 'setimmediate') { - // Worker wants to postMessage() to itself to implement setImmediate() - // emulation. - worker.postMessage(d); -#if ENVIRONMENT_MAY_BE_NODE - } else if (cmd === 'uncaughtException') { - // 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); -#endif - } else if (cmd === 'callHandler') { - Module[d.handler](...d.args); - } else if (cmd) { - // 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: - err(`worker sent an unknown command ${cmd}`); - } - }; + }; + }); worker.onerror = (e) => { var message = 'worker sent an error!'; @@ -423,7 +423,9 @@ var LibraryPThread = { 'workerID': worker.workerID, #endif }); - }), + + return worker.loaded; + }, #if PTHREAD_POOL_SIZE async loadWasmModuleToAllWorkers() { @@ -709,12 +711,20 @@ var LibraryPThread = { // so that its existence does not prevent Node.js from exiting. This // has no effect if the worker is already weakly referenced (e.g. if // this worker was previously idle/unused). +#if ASYNCIFY + worker.loaded.then(() => worker.unref()); +#else worker.unref(); +#endif } #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) => { @@ -759,6 +769,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 syncronous 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/codesize/test_codesize_minimal_pthreads.json b/test/codesize/test_codesize_minimal_pthreads.json index 928806850fcd3..f860362c64537 100644 --- a/test/codesize/test_codesize_minimal_pthreads.json +++ b/test/codesize/test_codesize_minimal_pthreads.json @@ -1,10 +1,10 @@ { - "a.out.js": 7367, - "a.out.js.gz": 3587, + "a.out.js": 7363, + "a.out.js.gz": 3582, "a.out.nodebug.wasm": 19037, "a.out.nodebug.wasm.gz": 8787, - "total": 26404, - "total_gz": 12374, + "total": 26400, + "total_gz": 12369, "sent": [ "a (memory)", "b (exit)", diff --git a/test/codesize/test_codesize_minimal_pthreads_memgrowth.json b/test/codesize/test_codesize_minimal_pthreads_memgrowth.json index 5477e9ba79e30..6fb122dc21eb1 100644 --- a/test/codesize/test_codesize_minimal_pthreads_memgrowth.json +++ b/test/codesize/test_codesize_minimal_pthreads_memgrowth.json @@ -1,10 +1,10 @@ { - "a.out.js": 7776, - "a.out.js.gz": 3791, + "a.out.js": 7772, + "a.out.js.gz": 3785, "a.out.nodebug.wasm": 19038, "a.out.nodebug.wasm.gz": 8788, - "total": 26814, - "total_gz": 12579, + "total": 26810, + "total_gz": 12573, "sent": [ "a (memory)", "b (exit)", diff --git a/test/decorators.py b/test/decorators.py index 90cd0b3e976c5..664f62beb86b1 100644 --- a/test/decorators.py +++ b/test/decorators.py @@ -601,6 +601,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/pthread/test_pthread_64bit_atomics.c b/test/pthread/test_pthread_64bit_atomics.c index d1bd28dd99f13..d59cb270e3740 100644 --- a/test/pthread/test_pthread_64bit_atomics.c +++ b/test/pthread/test_pthread_64bit_atomics.c @@ -87,9 +87,6 @@ void RunTest(int test) { pthread_attr_init(&attr); pthread_attr_setstacksize(&attr, 4*1024); - emscripten_outf("Main thread has thread ID %p\n", pthread_self()); - assert(pthread_self() != 0); - switch(test) { case 2: memset(sharedData, 0xFF, sizeof(sharedData)); break; case 5: memset(sharedData, 0x10, sizeof(sharedData)); break; @@ -124,6 +121,9 @@ void RunTest(int test) { } int main() { + emscripten_outf("Main thread has thread ID %p\n", pthread_self()); + assert(pthread_self() != 0); + globalDouble = 5.0; globalU64 = 4; diff --git a/test/pthread/test_pthread_printf.c b/test/pthread/test_pthread_printf.c index 283421cbc4dd8..907fda10ea413 100644 --- a/test/pthread/test_pthread_printf.c +++ b/test/pthread/test_pthread_printf.c @@ -16,7 +16,9 @@ void *ThreadMain(void *arg) { int main() { pthread_t thread; + printf("in main\n"); int rc = pthread_create(&thread, NULL, ThreadMain, 0); + printf("pthread_create done\n"); assert(rc == 0); rc = pthread_join(thread, NULL); diff --git a/test/test_browser.py b/test/test_browser.py index d4b960c451fdd..3b6bce568f7e2 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, @@ -65,6 +66,7 @@ skip_if, skip_if_simple, with_all_sjlj, + with_asyncify_and_jspi, ) from tools import ports, shared, utils @@ -3663,6 +3665,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']) @@ -3678,8 +3684,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 7533ba5e8c0ad..6abc3457d89bc 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, @@ -75,6 +76,7 @@ with_all_eh_sjlj, with_all_fs, with_all_sjlj, + with_asyncify_and_jspi, with_env_modify, ) @@ -254,47 +256,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/test/test_other.py b/test/test_other.py index fa5190ecb6eb0..3525430077edf 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -11734,7 +11734,6 @@ def test_pthread_asyncify(self): # This was because PTHREADS_DEBUG calls back into WebAssembly for each call to `err()`. self.set_setting('PTHREADS_DEBUG') self.set_setting('ASYNCIFY') - self.set_setting('PTHREAD_POOL_SIZE', 2) self.do_other_test('test_pthread_asyncify.c') @requires_pthreads diff --git a/tools/link.py b/tools/link.py index 70fdfe43a2ce8..515b8d6cab0af 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'} @@ -1699,6 +1700,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