Skip to content

Commit 8bff1ab

Browse files
0pcomdeadprogram
authored andcommitted
wasm: keep one pending scheduler wakeup instead of leaking a timer chain
sleepTicks armed a fresh setTimeout on every call and never cancelled the previous one. Its callback calls go_scheduler(), which re-enters the scheduler, which calls sleepTicks again whenever it finds nothing runnable but something sleeping — so every armed timer replaces itself, and every JS->Go callback that reaches the scheduler starts another such chain. The pending count grows without bound. The scheduler only needs one pending wakeup, the earliest. Track it, and drop any request that is no sooner than what is already armed. Measured in Chrome with the same wasm binary, 50 sleeping goroutines and a requestAnimationFrame loop, counting setTimeout calls per second at three points: 487/1498/2509 before, 9/5/5 after. Go-side timing is unchanged (a goroutine sleeping in a loop still advances 3s over 3s of wall clock) and frame pacing is unchanged at ~59fps. Fixes #5621
1 parent 61e5af3 commit 8bff1ab

1 file changed

Lines changed: 18 additions & 3 deletions

File tree

targets/wasm_exec.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,15 +270,24 @@
270270

271271
// func sleepTicks(timeout int64)
272272
"runtime.sleepTicks": (timeout) => {
273-
// Do not sleep, only reactivate scheduler after the given timeout.
274-
setTimeout(() => {
273+
// Do not sleep, only reactivate the scheduler after the given
274+
// timeout, keeping exactly one pending wakeup.
275+
const ms = Number(timeout) / 1e6;
276+
const due = Date.now() + ms;
277+
if (this._scheduledWakeup !== undefined) {
278+
if (this._scheduledWakeupDue <= due) return;
279+
clearTimeout(this._scheduledWakeup);
280+
}
281+
this._scheduledWakeupDue = due;
282+
this._scheduledWakeup = setTimeout(() => {
283+
this._scheduledWakeup = undefined;
275284
if (this.exited) return;
276285
try {
277286
this._inst.exports.go_scheduler();
278287
} catch (e) {
279288
if (e !== wasmExit) throw e;
280289
}
281-
}, Number(timeout) / 1e6);
290+
}, ms);
282291
},
283292

284293
// func finalizeRef(v ref)
@@ -489,6 +498,12 @@
489498
this._ids = new Map(); // mapping from JS values to reference ids
490499
this._idPool = []; // unused ids that have been garbage collected
491500
this.exited = false; // whether the Go program has exited
501+
// A wakeup left pending by a previous run would otherwise suppress the
502+
// first one this run asks for, and the scheduler would never start.
503+
if (this._scheduledWakeup !== undefined) {
504+
clearTimeout(this._scheduledWakeup);
505+
this._scheduledWakeup = undefined;
506+
}
492507
this.exitCode = 0;
493508
// syscall/js.handleEvent reads _pendingEvent and returns early only when
494509
// it IsNull(). Leaving it `undefined` is not null, so handleEvent falls

0 commit comments

Comments
 (0)