Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 83 additions & 71 deletions src/lib/libpthread.js
Original file line number Diff line number Diff line change
Expand Up @@ -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!';
Expand Down Expand Up @@ -443,7 +444,9 @@ var LibraryPThread = {
'workerID': worker.workerID,
#endif
});
}),

return worker.loaded;
},

#if PTHREAD_POOL_SIZE
async loadWasmModuleToAllWorkers() {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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: <BUG_LINK>
__pthread_create_js__async: 'auto',
__pthread_create_js__deps: ['$spawnThread', '$pthreadCreateProxied',
'emscripten_has_threading_support',
#if OFFSCREENCANVAS_SUPPORT
Expand Down
2 changes: 1 addition & 1 deletion src/postamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
sbc100 marked this conversation as resolved.
// 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
Expand Down
41 changes: 41 additions & 0 deletions test/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
11 changes: 10 additions & 1 deletion test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -66,6 +67,7 @@
skip_if,
skip_if_simple,
with_all_sjlj,
with_asyncify_and_jspi,
)

from tools import ports, shared, utils
Expand Down Expand Up @@ -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'])
Expand All @@ -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
Expand Down
43 changes: 2 additions & 41 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
)
from decorators import (
all_engines,
also_with_asyncify_and_jspi,
also_with_minimal_runtime,
also_with_modularize,
also_with_nodefs,
Expand Down Expand Up @@ -76,6 +77,7 @@
with_all_eh_sjlj,
with_all_fs,
with_all_sjlj,
with_asyncify_and_jspi,
with_env_modify,
)

Expand Down Expand Up @@ -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)

Expand Down
9 changes: 9 additions & 0 deletions tools/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
DEFAULT_ASYNCIFY_EXPORTS = [
'main',
'__main_argc_argv',
'_emscripten_proxy_main',
]

VALID_ENVIRONMENTS = {'web', 'webview', 'worker', 'node', 'shell', 'worklet'}
Expand Down Expand Up @@ -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
Expand Down
Loading