From c02b9c27adf9821ae31ff02a0c06afb56442cccc 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 | 31 ++++++++++--------- 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, 94 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..f97a72d4066d3 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,11 @@ 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 runtimeKeepaliveStack() + 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; @@ -413,6 +419,7 @@ LibraryJSEventLoop = { #if ASSERTIONS assert(!MainLoop.func, 'emscripten_set_main_loop: there can only be one main loop function at once') #endif + MainLoop.pause(); MainLoop.func = iterFunc; MainLoop.arg = arg; @@ -422,7 +429,6 @@ LibraryJSEventLoop = { #if RUNTIME_DEBUG dbg('main loop exiting'); #endif - {{{ runtimeKeepalivePop() }}} #if !MINIMAL_RUNTIME maybeExit(); #endif @@ -433,10 +439,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..72c25aadf4db8 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -1063,13 +1063,20 @@ function awaitIf(condition) { return condition ? 'await ' : ''; } +function runtimeKeepaliveStack() { + 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 (runtimeKeepaliveStack()) { + 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 (runtimeKeepaliveStack()) { + return 'runtimeKeepalivePop();'; + } else { + return ''; + } } // Some web APIs like TextDecoder.decode() and XMLHttpRequest.send() do not @@ -1284,6 +1294,7 @@ addToCompileTimeContext({ runIfWorkerThread, runtimeKeepalivePop, runtimeKeepalivePush, + runtimeKeepaliveStack, splitI64, to64, toIndexType, diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 7935152e5948e..d432e378f31ea 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": 267530, "a.out.nodebug.wasm": 588309, - "total": 855848, + "total": 855839, "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 af0d39f4fb216..4b68e39790c9d 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13381,6 +13381,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')