diff --git a/ChangeLog.md b/ChangeLog.md index 68b62107b58e0..a3fe632c528d1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -36,6 +36,9 @@ See docs/process.md for more on how version tagging works. the per-call garbage on such calls. The registration ABI gained size and triviality parameters, so object files built against an older `bind.h` need to be rebuilt. (#27610) +- `emscripten_clear_timeout` now releases the runtime keepalive held by the + pending timeout, and both `emscripten_clear_timeout` and + `emscripten_clear_immediate` are no-ops for ids that already fired. (#27720) 6.0.9 - 09/01/26 ---------------- diff --git a/src/lib/libeventloop.js b/src/lib/libeventloop.js index 042dcad37b652..2f4fc25a63d81 100644 --- a/src/lib/libeventloop.js +++ b/src/lib/libeventloop.js @@ -11,14 +11,32 @@ LibraryJSEventLoop = { throw 'unwind'; }, + // Just like setTimeout but returns an i32 that can be passed back to wasm + // rather than a JS object, and holds a runtime keepalive while pending. $safeSetTimeout__deps: ['$callUserCallback'], $safeSetTimeout__docs: '/** @param {number=} timeout */', $safeSetTimeout: (func, timeout) => { {{{ runtimeKeepalivePush() }}} - return setTimeout(() => { + // Slot 0 is reserved so that, like setTimeout, ids are always non-zero. + safeSetTimeout.mapping ||= [0]; + var id = safeSetTimeout.mapping.length; + safeSetTimeout.mapping[id] = setTimeout(() => { + safeSetTimeout.mapping[id] = undefined; {{{ runtimeKeepalivePop() }}} callUserCallback(func); }, timeout); + return id; + }, + + // Clears a pending safeSetTimeout and releases its keepalive. No-op if the + // timeout has already fired or been cleared. + $safeClearTimeout__deps: ['$safeSetTimeout'], + $safeClearTimeout: (id) => { + var handle = safeSetTimeout.mapping?.[id]; + if (!handle) return; + clearTimeout(handle); + safeSetTimeout.mapping[id] = undefined; + {{{ runtimeKeepalivePop() }}} }, // Just like setImmediate but returns an i32 that can be passed back @@ -43,13 +61,13 @@ LibraryJSEventLoop = { }, // Just like clearImmediate but takes an i32 rather than an object. + // Returns true if the immediate was still pending. $clearImmediateWrapped: (id) => { -#if ASSERTIONS - assert(id); - assert(setImmediateWrapped.mapping[id]); -#endif - clearImmediate(setImmediateWrapped.mapping[id]); + var handle = setImmediateWrapped.mapping[id]; + if (!handle) return false; + clearImmediate(handle); setImmediateWrapped.mapping[id] = undefined; + return true; }, $emSetImmediate__deps: ['$setImmediateWrapped', '$clearImmediateWrapped', '$emClearImmediate'], @@ -81,8 +99,10 @@ LibraryJSEventLoop = { } emClearImmediate = /**@type{function(number=)}*/((id) => { var index = id - __setImmediate_id_counter; + if (index < 0 || !__setImmediate_queue[index]) return false; // must preserve the order and count of elements in the queue, so replace the pending callback with an empty function - if (index >= 0 && index < __setImmediate_queue.length) __setImmediate_queue[index] = null; + __setImmediate_queue[index] = null; + return true; }) }`, $emSetImmediate: undefined, @@ -101,8 +121,9 @@ LibraryJSEventLoop = { emscripten_clear_immediate__deps: ['$emClearImmediate'], emscripten_clear_immediate: (id) => { - {{{ runtimeKeepalivePop(); }}} - emClearImmediate(id); + if (emClearImmediate(id)) { + {{{ runtimeKeepalivePop(); }}} + } }, emscripten_set_immediate_loop__deps: ['$emSetImmediate', '$callUserCallback'], @@ -124,13 +145,7 @@ LibraryJSEventLoop = { emscripten_set_timeout: (cb, msecs, userData) => safeSetTimeout(() => {{{ makeDynCall('vp', 'cb') }}}(userData), msecs), -#if AUDIO_WORKLET - // Use a wrapper function here since simply aliasing `clearTimeout` would - // cause the module to fail to load in the audio worklet context. - emscripten_clear_timeout: (id) => clearTimeout(id), -#else - emscripten_clear_timeout: 'clearTimeout', -#endif + emscripten_clear_timeout: '$safeClearTimeout', emscripten_set_timeout_loop__deps: ['$callUserCallback', 'emscripten_get_now'], emscripten_set_timeout_loop: (cb, msecs, userData) => { diff --git a/src/lib/libsdl.js b/src/lib/libsdl.js index a0e5e63f33e92..ccd5394ae16a9 100644 --- a/src/lib/libsdl.js +++ b/src/lib/libsdl.js @@ -2562,14 +2562,14 @@ var LibrarySDL = { }, SDL_PauseAudio__proxy: 'sync', - SDL_PauseAudio__deps: ['$safeSetTimeout'], + SDL_PauseAudio__deps: ['$safeSetTimeout', '$safeClearTimeout'], SDL_PauseAudio: (pauseOn) => { if (!SDL.audio) { return; } if (pauseOn) { if (SDL.audio.timer !== undefined) { - clearTimeout(SDL.audio.timer); + safeClearTimeout(SDL.audio.timer); SDL.audio.numAudioTimersPending = 0; SDL.audio.timer = undefined; } @@ -3579,8 +3579,9 @@ var LibrarySDL = { interval), SDL_RemoveTimer__proxy: 'sync', + SDL_RemoveTimer__deps: ['$safeClearTimeout'], SDL_RemoveTimer: (id) => { - clearTimeout(id); + safeClearTimeout(id); return true; }, diff --git a/test/codesize/test_codesize_hello_O0.json b/test/codesize/test_codesize_hello_O0.json index 017298ab01b7c..0a131354a605f 100644 --- a/test/codesize/test_codesize_hello_O0.json +++ b/test/codesize/test_codesize_hello_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 23579, - "a.out.js.gz": 8576, + "a.out.js": 23596, + "a.out.js.gz": 8582, "a.out.nodebug.wasm": 14279, "a.out.nodebug.wasm.gz": 7101, - "total": 37858, - "total_gz": 15677, + "total": 37875, + "total_gz": 15683, "sent": [ "fd_write" ], diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 19e642f329f77..2599da95e6573 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": 270520, + "a.out.js": 270695, "a.out.nodebug.wasm": 588289, - "total": 858809, + "total": 858984, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/codesize/test_codesize_minimal_O0.expected.js b/test/codesize/test_codesize_minimal_O0.expected.js index a8d77958ced86..61b5952de1fd0 100644 --- a/test/codesize/test_codesize_minimal_O0.expected.js +++ b/test/codesize/test_codesize_minimal_O0.expected.js @@ -948,6 +948,7 @@ Module['FS_createPreloadedFile'] = FS.createPreloadedFile; 'initRandomFill', 'randomFill', 'safeSetTimeout', + 'safeClearTimeout', 'setImmediateWrapped', 'safeRequestAnimationFrame', 'clearImmediateWrapped', diff --git a/test/codesize/test_codesize_minimal_O0.json b/test/codesize/test_codesize_minimal_O0.json index 00a34fec5ebd7..f960557acfdd1 100644 --- a/test/codesize/test_codesize_minimal_O0.json +++ b/test/codesize/test_codesize_minimal_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 18784, - "a.out.js.gz": 6752, + "a.out.js": 18801, + "a.out.js.gz": 6758, "a.out.nodebug.wasm": 1071, "a.out.nodebug.wasm.gz": 636, - "total": 19855, - "total_gz": 7388, + "total": 19872, + "total_gz": 7394, "sent": [], "imports": [], "exports": [ diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index 920e5a7844084..a6bc588a5bc49 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -1,16 +1,16 @@ { - "hello_world.js": 54683, - "hello_world.js.gz": 17343, + "hello_world.js": 54705, + "hello_world.js.gz": 17355, "hello_world.wasm": 14279, "hello_world.wasm.gz": 7101, "no_asserts.js": 23596, "no_asserts.js.gz": 8277, "no_asserts.wasm": 11393, "no_asserts.wasm.gz": 5646, - "strict.js": 51834, - "strict.js.gz": 16342, + "strict.js": 51856, + "strict.js.gz": 16348, "strict.wasm": 14279, "strict.wasm.gz": 7098, - "total": 170064, - "total_gz": 61807 + "total": 170108, + "total_gz": 61825 } diff --git a/test/test_emscripten_clear_timeout.c b/test/test_emscripten_clear_timeout.c new file mode 100644 index 0000000000000..ea2073dcbf622 --- /dev/null +++ b/test/test_emscripten_clear_timeout.c @@ -0,0 +1,60 @@ +#include +#include +#include +#include + +int fired_id; + +void never(void *arg) { + printf("cleared callback ran\n"); + abort(); +} + +void fired(void *arg) { + printf("fired\n"); + // Clearing an id that already fired must be a no-op. + emscripten_clear_timeout(fired_id); + emscripten_clear_timeout(fired_id); +} + +void immediate_fired(void *arg) { + emscripten_clear_immediate(fired_id); + emscripten_clear_immediate(fired_id); +} + +// Only runs if the runtime actually exits. +void at_exit() { + printf("done\n"); +} + +int main() { + atexit(at_exit); +#if MODE_CLEARED + // Clearing a pending timeout releases its keepalive so the runtime exits + // from main without waiting for it (a leak fails the check below; a timer + // that was not actually cleared aborts when it fires). + int id = emscripten_set_timeout(never, 1000, NULL); + assert(emscripten_runtime_keepalive_check()); + emscripten_clear_timeout(id); + assert(!emscripten_runtime_keepalive_check()); + return 42; +#elif MODE_IDEMPOTENT + int id = emscripten_set_timeout(never, 1000, NULL); + emscripten_clear_timeout(id); + emscripten_clear_timeout(id); + fired_id = emscripten_set_timeout(fired, 0, NULL); + // Still-pending timer must keep the runtime alive until it fires. + emscripten_set_timeout(fired, 50, NULL); + return 0; +#elif MODE_IMMEDIATE + int id = emscripten_set_immediate(never, NULL); + emscripten_clear_immediate(id); + emscripten_clear_immediate(id); + fired_id = emscripten_set_immediate(immediate_fired, NULL); + emscripten_set_timeout(fired, 50, NULL); + return 0; +#else + emscripten_set_timeout(fired, 50, NULL); + return 0; +#endif +} diff --git a/test/test_other.py b/test/test_other.py index d2c96fa81755b..bd63374a7c13b 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13724,6 +13724,15 @@ def test_emscripten_set_timeout(self): def test_emscripten_set_timeout_loop(self): self.do_runf('emscripten_set_timeout_loop.c', args=['-pthread', '-sPROXY_TO_PTHREAD']) + @parameterized({ + 'fires': ([], 0, 'fired\ndone\n'), + 'cleared': (['-DMODE_CLEARED'], 42, 'done\n'), + 'idempotent': (['-DMODE_IDEMPOTENT'], 0, 'fired\nfired\ndone\n'), + 'immediate': (['-DMODE_IMMEDIATE'], 0, 'fired\ndone\n'), + }) + def test_emscripten_clear_timeout(self, cflags, returncode, expected): + self.do_runf('test_emscripten_clear_timeout.c', expected, cflags=['-sEXIT_RUNTIME'] + cflags, assert_returncode=returncode) + @parameterized({ '': ([],), 'asyncify': (['-sASYNCIFY'],),