Skip to content
Merged
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
3 changes: 3 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------
Expand Down
47 changes: 31 additions & 16 deletions src/lib/libeventloop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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];
Comment thread
guybedford marked this conversation as resolved.
if (!handle) return false;
clearImmediate(handle);
setImmediateWrapped.mapping[id] = undefined;
return true;
},

$emSetImmediate__deps: ['$setImmediateWrapped', '$clearImmediateWrapped', '$emClearImmediate'],
Expand Down Expand Up @@ -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,
Expand All @@ -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'],
Expand All @@ -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) => {
Expand Down
7 changes: 4 additions & 3 deletions src/lib/libsdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -3579,8 +3579,9 @@ var LibrarySDL = {
interval),

SDL_RemoveTimer__proxy: 'sync',
SDL_RemoveTimer__deps: ['$safeClearTimeout'],
SDL_RemoveTimer: (id) => {
clearTimeout(id);
safeClearTimeout(id);
return true;
},

Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_hello_O0.json
Original file line number Diff line number Diff line change
@@ -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"
],
Expand Down
4 changes: 2 additions & 2 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
1 change: 1 addition & 0 deletions test/codesize/test_codesize_minimal_O0.expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,7 @@ Module['FS_createPreloadedFile'] = FS.createPreloadedFile;
'initRandomFill',
'randomFill',
'safeSetTimeout',
'safeClearTimeout',
'setImmediateWrapped',
'safeRequestAnimationFrame',
'clearImmediateWrapped',
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_minimal_O0.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
12 changes: 6 additions & 6 deletions test/codesize/test_unoptimized_code_size.json
Original file line number Diff line number Diff line change
@@ -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
}
60 changes: 60 additions & 0 deletions test/test_emscripten_clear_timeout.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <emscripten/eventloop.h>

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
}
9 changes: 9 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'],),
Expand Down
Loading