From cefd3ed74069e4772003d42d3577b5736c392291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Mon, 25 Aug 2025 12:26:06 +0300 Subject: [PATCH 1/6] Sequentialize uncaught exceptions in Node.js Workers into the same postMessage() stream that other messages go through, to ensure they are received by the main thread in the order they were generated. Fixes https://github.com/emscripten-core/emscripten/issues/15014. See https://github.com/nodejs/node/issues/59617 Ruff --- src/lib/libpthread.js | 17 +++++++++++++++++ src/runtime_common.js | 15 +++++++++++++++ .../test_codesize_hello_dylink_all.json | 4 ++-- .../test_codesize_minimal_pthreads.json | 8 ++++---- ...est_codesize_minimal_pthreads_memgrowth.json | 8 ++++---- test/test_core.py | 4 +--- 6 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 47c3b4eb6a919..f9d56605239df 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -241,6 +241,9 @@ var LibraryPThread = { worker.onmessage = (e) => { var d = e['data']; var cmd = d.cmd; +#if PTHREADS_DEBUG + dbg(`main thread: received message '${cmd}' from worker. ${JSON.stringify(d)}`); +#endif // If this message is intended to a recipient that is not the main // thread, forward it to the target thread. @@ -283,6 +286,13 @@ var LibraryPThread = { // 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) { @@ -308,6 +318,13 @@ var LibraryPThread = { if (ENVIRONMENT_IS_NODE) { worker.on('message', (data) => worker.onmessage({ data: data })); worker.on('error', (e) => worker.onerror(e)); + +#if PTHREADS_DEBUG + worker.on('exit', (code) => { + if (worker.pthread_ptr) dbg(`Worker hosting pthread ${ptrToString(worker.pthread_ptr)} has terminated with code ${code}.`); + else dbg(`Worker has terminated with code ${code}.`); + }); +#endif } #endif diff --git a/src/runtime_common.js b/src/runtime_common.js index 59f4d918bd927..cc83ca6efcd7d 100644 --- a/src/runtime_common.js +++ b/src/runtime_common.js @@ -40,6 +40,21 @@ if (ENVIRONMENT_IS_NODE && {{{ ENVIRONMENT_IS_WORKER_THREAD() }}}) { self: global, postMessage: (msg) => parentPort['postMessage'](msg), }); + // Node.js Workers do not pass postMessage()s and uncaught exception events to the parent + // thread necessarily in the same order where they were generated in sequential program order. + // See https://github.com/nodejs/node/issues/59617 + // To remedy this, capture all uncaughtExceptions in the Worker, and sequentialize those over + // to the same postMessage pipe that other messages use. + process.on("uncaughtException", (err) => { +#if PTHREADS_DEBUG + dbg(`uncaughtException on worker thread: ${err.message}`); +#endif + parentPort['postMessage']({ cmd: 'uncaughtException', error: err }); + // Also shut down the Worker to match the same semantics as if this uncaughtException + // handler was not registered. + // (n.b. this will not shut down the whole Node.js app process, but just the Worker) + process.exit(1); + }); } #endif // (PTHREADS || WASM_WORKERS) && (ENVIRONMENT_MAY_BE_NODE && !WASM_ESM_INTEGRATION) diff --git a/test/code_size/test_codesize_hello_dylink_all.json b/test/code_size/test_codesize_hello_dylink_all.json index e50460946b90f..f1df2b8588e12 100644 --- a/test/code_size/test_codesize_hello_dylink_all.json +++ b/test/code_size/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { "a.out.js": 246640, - "a.out.nodebug.wasm": 597735, - "total": 844375, + "a.out.nodebug.wasm": 597720, + "total": 844360, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/code_size/test_codesize_minimal_pthreads.json b/test/code_size/test_codesize_minimal_pthreads.json index e277ff4d8f5fc..6d367900d88cf 100644 --- a/test/code_size/test_codesize_minimal_pthreads.json +++ b/test/code_size/test_codesize_minimal_pthreads.json @@ -1,10 +1,10 @@ { - "a.out.js": 7499, - "a.out.js.gz": 3721, + "a.out.js": 7652, + "a.out.js.gz": 3767, "a.out.nodebug.wasm": 19588, "a.out.nodebug.wasm.gz": 9025, - "total": 27087, - "total_gz": 12746, + "total": 27240, + "total_gz": 12792, "sent": [ "a (memory)", "b (emscripten_get_now)", diff --git a/test/code_size/test_codesize_minimal_pthreads_memgrowth.json b/test/code_size/test_codesize_minimal_pthreads_memgrowth.json index cbe886f575601..6ad744f14b2f8 100644 --- a/test/code_size/test_codesize_minimal_pthreads_memgrowth.json +++ b/test/code_size/test_codesize_minimal_pthreads_memgrowth.json @@ -1,10 +1,10 @@ { - "a.out.js": 7926, - "a.out.js.gz": 3924, + "a.out.js": 8079, + "a.out.js.gz": 3977, "a.out.nodebug.wasm": 19589, "a.out.nodebug.wasm.gz": 9025, - "total": 27515, - "total_gz": 12949, + "total": 27668, + "total_gz": 13002, "sent": [ "a (memory)", "b (emscripten_get_now)", diff --git a/test/test_core.py b/test/test_core.py index 9a95ebbd7f74f..2ab78e50be058 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -24,7 +24,7 @@ from tools import shared, building, config, utils, webassembly import common from common import RunnerCore, path_from_root, requires_native_clang, test_file, create_file -from common import skip_if, no_windows, no_mac, is_slow_test, parameterized, parameterize +from common import skip_if, no_windows, is_slow_test, parameterized, parameterize from common import env_modify, with_env_modify, disabled, flaky, node_pthreads, also_without_bigint from common import read_file, read_binary, requires_v8, requires_node, requires_dev_dependency, requires_wasm2js, requires_node_canary from common import compiler_for, crossplatform, no_4gb, no_2gb, also_with_minimal_runtime, also_with_modularize @@ -2668,8 +2668,6 @@ def test_pthread_attr_getstack(self): self.do_run_in_out_file_test('pthread/test_pthread_attr_getstack.c') @node_pthreads - @no_mac('https://github.com/emscripten-core/emscripten/issues/15014') - @flaky('https://github.com/emscripten-core/emscripten/issues/15014') def test_pthread_abort(self): self.set_setting('PROXY_TO_PTHREAD') # Add the onAbort handler at runtime during preRun. This means that onAbort From aa99c1bed3912572380ac76c2ccd37fb8610867a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Mon, 25 Aug 2025 16:33:59 +0300 Subject: [PATCH 2/6] Fix code size expectation --- test/code_size/test_codesize_hello_dylink_all.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/code_size/test_codesize_hello_dylink_all.json b/test/code_size/test_codesize_hello_dylink_all.json index f1df2b8588e12..e50460946b90f 100644 --- a/test/code_size/test_codesize_hello_dylink_all.json +++ b/test/code_size/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { "a.out.js": 246640, - "a.out.nodebug.wasm": 597720, - "total": 844360, + "a.out.nodebug.wasm": 597735, + "total": 844375, "sent": [ "IMG_Init", "IMG_Load", From 971560931d0211431c0e378b872ee7377ac236bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Mon, 25 Aug 2025 20:37:15 +0300 Subject: [PATCH 3/6] Use shorter postMessage form. --- src/runtime_common.js | 2 +- test/code_size/test_codesize_minimal_pthreads.json | 8 ++++---- .../test_codesize_minimal_pthreads_memgrowth.json | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/runtime_common.js b/src/runtime_common.js index cc83ca6efcd7d..9e00b0ac8b544 100644 --- a/src/runtime_common.js +++ b/src/runtime_common.js @@ -49,7 +49,7 @@ if (ENVIRONMENT_IS_NODE && {{{ ENVIRONMENT_IS_WORKER_THREAD() }}}) { #if PTHREADS_DEBUG dbg(`uncaughtException on worker thread: ${err.message}`); #endif - parentPort['postMessage']({ cmd: 'uncaughtException', error: err }); + postMessage({ cmd: 'uncaughtException', error: err }); // Also shut down the Worker to match the same semantics as if this uncaughtException // handler was not registered. // (n.b. this will not shut down the whole Node.js app process, but just the Worker) diff --git a/test/code_size/test_codesize_minimal_pthreads.json b/test/code_size/test_codesize_minimal_pthreads.json index 6d367900d88cf..d2d287006e49b 100644 --- a/test/code_size/test_codesize_minimal_pthreads.json +++ b/test/code_size/test_codesize_minimal_pthreads.json @@ -1,10 +1,10 @@ { - "a.out.js": 7652, - "a.out.js.gz": 3767, + "a.out.js": 7649, + "a.out.js.gz": 3768, "a.out.nodebug.wasm": 19588, "a.out.nodebug.wasm.gz": 9025, - "total": 27240, - "total_gz": 12792, + "total": 27237, + "total_gz": 12793, "sent": [ "a (memory)", "b (emscripten_get_now)", diff --git a/test/code_size/test_codesize_minimal_pthreads_memgrowth.json b/test/code_size/test_codesize_minimal_pthreads_memgrowth.json index 6ad744f14b2f8..3f98de951b241 100644 --- a/test/code_size/test_codesize_minimal_pthreads_memgrowth.json +++ b/test/code_size/test_codesize_minimal_pthreads_memgrowth.json @@ -1,10 +1,10 @@ { - "a.out.js": 8079, - "a.out.js.gz": 3977, + "a.out.js": 8076, + "a.out.js.gz": 3974, "a.out.nodebug.wasm": 19589, "a.out.nodebug.wasm.gz": 9025, - "total": 27668, - "total_gz": 13002, + "total": 27665, + "total_gz": 12999, "sent": [ "a (memory)", "b (emscripten_get_now)", From 2d3e92fbda5907ad3b4f9a998c5abffa411bc63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Mon, 25 Aug 2025 21:06:04 +0300 Subject: [PATCH 4/6] Remove stringify --- src/lib/libpthread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index f9d56605239df..e251f4508ba23 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -242,7 +242,7 @@ var LibraryPThread = { var d = e['data']; var cmd = d.cmd; #if PTHREADS_DEBUG - dbg(`main thread: received message '${cmd}' from worker. ${JSON.stringify(d)}`); + dbg(`main thread: received message '${cmd}' from worker. ${d}`); #endif // If this message is intended to a recipient that is not the main From 053c2457a08325ce5bd2cb0810831a395da9ad51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Mon, 25 Aug 2025 21:48:49 +0300 Subject: [PATCH 5/6] rebaseline --- test/code_size/test_codesize_hello_dylink_all.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/code_size/test_codesize_hello_dylink_all.json b/test/code_size/test_codesize_hello_dylink_all.json index e50460946b90f..f1df2b8588e12 100644 --- a/test/code_size/test_codesize_hello_dylink_all.json +++ b/test/code_size/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { "a.out.js": 246640, - "a.out.nodebug.wasm": 597735, - "total": 844375, + "a.out.nodebug.wasm": 597720, + "total": 844360, "sent": [ "IMG_Init", "IMG_Load", From 43e00dd31e5cd0afcfbf466c30d4337087524147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Mon, 25 Aug 2025 22:48:46 +0300 Subject: [PATCH 6/6] rebaseline --- test/code_size/test_codesize_hello_dylink_all.json | 4 ++-- .../test_minimal_runtime_code_size_hello_webgl2_wasm.json | 8 ++++---- ...st_minimal_runtime_code_size_hello_webgl2_wasm2js.json | 8 ++++---- .../test_minimal_runtime_code_size_hello_webgl_wasm.json | 8 ++++---- ...est_minimal_runtime_code_size_hello_webgl_wasm2js.json | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/test/code_size/test_codesize_hello_dylink_all.json b/test/code_size/test_codesize_hello_dylink_all.json index f1df2b8588e12..a1b98ce129966 100644 --- a/test/code_size/test_codesize_hello_dylink_all.json +++ b/test/code_size/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { "a.out.js": 246640, - "a.out.nodebug.wasm": 597720, - "total": 844360, + "a.out.nodebug.wasm": 597718, + "total": 844358, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/code_size/test_minimal_runtime_code_size_hello_webgl2_wasm.json b/test/code_size/test_minimal_runtime_code_size_hello_webgl2_wasm.json index 662f5d9a394a7..16da3c7b71605 100644 --- a/test/code_size/test_minimal_runtime_code_size_hello_webgl2_wasm.json +++ b/test/code_size/test_minimal_runtime_code_size_hello_webgl2_wasm.json @@ -3,8 +3,8 @@ "a.html.gz": 321, "a.js": 4437, "a.js.gz": 2281, - "a.wasm": 8290, - "a.wasm.gz": 5639, - "total": 13181, - "total_gz": 8241 + "a.wasm": 8299, + "a.wasm.gz": 5645, + "total": 13190, + "total_gz": 8247 } diff --git a/test/code_size/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json b/test/code_size/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json index 43b2910d4cc0b..0b0a8883f9e53 100644 --- a/test/code_size/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json +++ b/test/code_size/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json @@ -1,8 +1,8 @@ { "a.html": 346, "a.html.gz": 255, - "a.js": 18198, - "a.js.gz": 9822, - "total": 18544, - "total_gz": 10077 + "a.js": 18202, + "a.js.gz": 9823, + "total": 18548, + "total_gz": 10078 } diff --git a/test/code_size/test_minimal_runtime_code_size_hello_webgl_wasm.json b/test/code_size/test_minimal_runtime_code_size_hello_webgl_wasm.json index d0433713248fe..8f945a8bc4984 100644 --- a/test/code_size/test_minimal_runtime_code_size_hello_webgl_wasm.json +++ b/test/code_size/test_minimal_runtime_code_size_hello_webgl_wasm.json @@ -3,8 +3,8 @@ "a.html.gz": 321, "a.js": 3975, "a.js.gz": 2123, - "a.wasm": 8290, - "a.wasm.gz": 5639, - "total": 12719, - "total_gz": 8083 + "a.wasm": 8299, + "a.wasm.gz": 5645, + "total": 12728, + "total_gz": 8089 } diff --git a/test/code_size/test_minimal_runtime_code_size_hello_webgl_wasm2js.json b/test/code_size/test_minimal_runtime_code_size_hello_webgl_wasm2js.json index 87fc439b2de4c..b4ad61fd81f17 100644 --- a/test/code_size/test_minimal_runtime_code_size_hello_webgl_wasm2js.json +++ b/test/code_size/test_minimal_runtime_code_size_hello_webgl_wasm2js.json @@ -1,8 +1,8 @@ { "a.html": 346, "a.html.gz": 255, - "a.js": 17725, - "a.js.gz": 9659, - "total": 18071, - "total_gz": 9914 + "a.js": 17729, + "a.js.gz": 9660, + "total": 18075, + "total_gz": 9915 }