From 5c31998c4ecbc2dfc37e1e0ee04dd6bb8b38f56f Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Fri, 31 Jul 2026 12:31:49 -0700 Subject: [PATCH] [eventloop] Fix main loop keepalive cleanup when cancelling or pausing Pop runtimeKeepaliveCounter immediately in MainLoop.pause() when a main loop is paused or cancelled, rather than deferring the pop until checkIsRunning() runs at the end of the next frame tick. This fixes cases where exit() or emscripten_force_exit() is called immediately after emscripten_cancel_main_loop() from within the main loop callback. Additionally, remove the redundant MainLoop.running property, using MainLoop.scheduler directly to determine if a main loop is active. Fixes: #27456 --- src/lib/libeventloop.js | 32 +++++++++++-------- src/parseTools.mjs | 19 ++++++++--- .../test_codesize_hello_dylink_all.json | 4 +-- test/test_browser.py | 6 ++++ test/test_emscripten_main_loop_cancel_exit.c | 20 ++++++++++++ ...t_emscripten_main_loop_cancel_force_exit.c | 20 ++++++++++++ test/test_other.py | 14 ++++++++ 7 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 test/test_emscripten_main_loop_cancel_exit.c create mode 100644 test/test_emscripten_main_loop_cancel_force_exit.c diff --git a/src/lib/libeventloop.js b/src/lib/libeventloop.js index 6457dc4f55318..042dcad37b652 100644 --- a/src/lib/libeventloop.js +++ b/src/lib/libeventloop.js @@ -204,7 +204,11 @@ LibraryJSEventLoop = { Module['resumeMainLoop'] = MainLoop.resume; MainLoop.init();`, $MainLoop: { - running: false, + // The main loop tick function that will be called at each iteration. + // This will be non-null whenever a loop function is registered. + func: null, + // This will be non-null whenever a loop function is both registered and + // currently running. scheduler: null, // Each main loop is numbered with a ID in sequence order. Only one main // loop can run at a time. This variable stores the ordinal number of the @@ -212,8 +216,6 @@ LibraryJSEventLoop = { // will quit themselves. This is incremented whenever a new main loop is // created. currentlyRunningMainloop: 0, - // The main loop tick function that will be called at each iteration. - func: null, // The argument that will be passed to the main loop. (of type void*) arg: 0, timingMode: 0, @@ -224,9 +226,12 @@ LibraryJSEventLoop = { postMainLoop: [], pause() { - MainLoop.scheduler = null; - // Incrementing this signals the previous main loop that it's now become old, and it must return. - MainLoop.currentlyRunningMainloop++; + if (MainLoop.scheduler) { + MainLoop.scheduler = null; + // Incrementing this signals the previous main loop that it's now become old, and it must return. + MainLoop.currentlyRunningMainloop++; + {{{ runtimeKeepalivePop() }}} + } }, resume() { @@ -328,10 +333,13 @@ LibraryJSEventLoop = { return 1; // Return non-zero on failure, can't set timing mode when there is no main loop. } - if (!MainLoop.running) { - {{{ runtimeKeepalivePush() }}} - MainLoop.running = true; +#if useRuntimeKeepaliveStack() + // If there is no existing scheduler then we are transitioning from + // inactive to active and we add to runtime keepalive counter. + if (!MainLoop.scheduler) { + runtimeKeepalivePush(); } +#endif if (mode == {{{ cDefs.EM_TIMING_SETTIMEOUT }}}) { MainLoop.scheduler = function MainLoop_scheduler_setTimeout() { var timeUntilNextTick = Math.max(0, MainLoop.tickStartTime + value - _emscripten_get_now())|0; @@ -422,7 +430,6 @@ LibraryJSEventLoop = { #if RUNTIME_DEBUG dbg('main loop exiting'); #endif - {{{ runtimeKeepalivePop() }}} #if !MINIMAL_RUNTIME maybeExit(); #endif @@ -433,10 +440,7 @@ LibraryJSEventLoop = { // We create the loop runner here but it is not actually running until // _emscripten_set_main_loop_timing is called (which might happen at a - // later time). This member signifies that the current runner has not - // yet been started so that we can call runtimeKeepalivePush when it - // gets its timing set for the first time. - MainLoop.running = false; + // later time). MainLoop.runner = function MainLoop_runner() { if (ABORT) return; if (MainLoop.queue.length > 0) { diff --git a/src/parseTools.mjs b/src/parseTools.mjs index f4b285590f68c..96622dfceabfd 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -1063,13 +1063,20 @@ function awaitIf(condition) { return condition ? 'await ' : ''; } +function useRuntimeKeepaliveStack() { + return !(MINIMAL_RUNTIME || (EXIT_RUNTIME == 0 && PTHREADS == 0)); +} + // Adds a call to runtimeKeepalivePush, if needed by the current build // configuration. // We skip this completely in MINIMAL_RUNTIME and also in builds that // don't ever need to exit the runtime. function runtimeKeepalivePush() { - if (MINIMAL_RUNTIME || (EXIT_RUNTIME == 0 && PTHREADS == 0)) return ''; - return 'runtimeKeepalivePush();'; + if (useRuntimeKeepaliveStack()) { + return 'runtimeKeepalivePush();'; + } else { + return ''; + } } // Adds a call to runtimeKeepalivePush, if needed by the current build @@ -1077,8 +1084,11 @@ function runtimeKeepalivePush() { // We skip this completely in MINIMAL_RUNTIME and also in builds that // don't ever need to exit the runtime. function runtimeKeepalivePop() { - if (MINIMAL_RUNTIME || (EXIT_RUNTIME == 0 && PTHREADS == 0)) return ''; - return 'runtimeKeepalivePop();'; + if (useRuntimeKeepaliveStack()) { + return 'runtimeKeepalivePop();'; + } else { + return ''; + } } // Some web APIs like TextDecoder.decode() and XMLHttpRequest.send() do not @@ -1291,4 +1301,5 @@ addToCompileTimeContext({ nodeWWDetection, wasmWorkerDetection, pthreadDetection, + useRuntimeKeepaliveStack, }); diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 93e5f99d04b9d..cdb63a9263342 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 267539, + "a.out.js": 267525, "a.out.nodebug.wasm": 588311, - "total": 855850, + "total": 855836, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/test_browser.py b/test/test_browser.py index 09a17bc331f24..290e91414f6a1 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -1798,6 +1798,12 @@ def test_emscripten_api_infloop(self): def test_emscripten_main_loop(self): self.btest_exit('test_emscripten_main_loop.c') + def test_emscripten_main_loop_cancel_exit(self): + self.btest_exit('test_emscripten_main_loop_cancel_exit.c', cflags=['-sASSERTIONS=2']) + + def test_emscripten_main_loop_cancel_force_exit(self): + self.btest_exit('test_emscripten_main_loop_cancel_force_exit.c', cflags=['-sASSERTIONS=2']) + @parameterized({ '': ([],), # test pthreads + AUTO_JS_LIBRARIES mode as well diff --git a/test/test_emscripten_main_loop_cancel_exit.c b/test/test_emscripten_main_loop_cancel_exit.c new file mode 100644 index 0000000000000..0677e149125e1 --- /dev/null +++ b/test/test_emscripten_main_loop_cancel_exit.c @@ -0,0 +1,20 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +#include +#include +#include + +void loop(void) { + emscripten_cancel_main_loop(); + exit(0); +} + +int main(void) { + emscripten_set_main_loop(loop, -1, 0); + return 99; +} diff --git a/test/test_emscripten_main_loop_cancel_force_exit.c b/test/test_emscripten_main_loop_cancel_force_exit.c new file mode 100644 index 0000000000000..9d8c754db3206 --- /dev/null +++ b/test/test_emscripten_main_loop_cancel_force_exit.c @@ -0,0 +1,20 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +#include +#include +#include + +void loop(void) { + emscripten_cancel_main_loop(); + emscripten_force_exit(0); +} + +int main(void) { + emscripten_set_main_loop(loop, -1, 0); + return 99; +} diff --git a/test/test_other.py b/test/test_other.py index 0b734792f1c37..61b8f4d84e36c 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13429,6 +13429,20 @@ def test_emscripten_set_immediate_loop(self): def test_emscripten_main_loop(self, args): self.do_runf('test_emscripten_main_loop.c', cflags=args) + @parameterized({ + '': ([],), + 'exit_runtime': (['-sEXIT_RUNTIME'],), + }) + def test_emscripten_main_loop_cancel_exit(self, args): + self.do_runf('test_emscripten_main_loop_cancel_exit.c', cflags=['-sASSERTIONS=2'] + args) + + @parameterized({ + '': ([],), + 'exit_runtime': (['-sEXIT_RUNTIME'],), + }) + def test_emscripten_main_loop_cancel_force_exit(self, args): + self.do_runf('test_emscripten_main_loop_cancel_force_exit.c', cflags=['-sASSERTIONS=2'] + args) + def test_emscripten_main_loop_and_blocker(self): self.do_runf('test_emscripten_main_loop_and_blocker.c')