fix(ext/web): improve timers resolution for 0ms timeouts (#19212)

This commit changes the implementation of `ext/web` timers, by using
"op_void_async_deferred" for timeouts of 0ms.

0ms timeout is meant to be run at the end of the event loop tick and
currently Tokio timers that we use to back timeouts have at least 1ms
resolution. That means that 0ms timeout actually take >1ms. This
commit changes that and runs 0ms timeout at the end of the event
loop tick.

One consequence is that "unrefing" a 0ms timer will actually keep
the event loop alive (which I believe actually makes sense, the test
we had only worked because the timeout took more than 1ms).

Ref https://github.com/denoland/deno/issues/19034
This commit is contained in:
Bartek Iwańczuk 2023-05-22 14:09:31 +02:00 committed by GitHub
parent 40bda07ff5
commit ffd3ed9b8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 18 additions and 5 deletions

View file

@ -50,7 +50,7 @@ function opSanitizerDelay() {
return new Promise((resolve) => {
setTimeout(() => {
ArrayPrototypePush(opSanitizerDelayResolveQueue, resolve);
}, 0);
}, 1);
});
}

View file

@ -557,7 +557,7 @@ Deno.test({
permissions: { run: true, read: true },
fn: async () => {
const [statusCode, output] = await execCode(`
const timer = setTimeout(() => console.log("1"));
const timer = setTimeout(() => console.log("1"), 1);
Deno.unrefTimer(timer);
`);
assertEquals(statusCode, 0);

View file

@ -27,7 +27,10 @@ const {
import * as webidl from "ext:deno_webidl/00_webidl.js";
import { reportException } from "ext:deno_web/02_event.js";
import { assert } from "ext:deno_web/00_infra.js";
const { op_sleep } = core.generateAsyncOpHandler("op_sleep");
const { op_sleep, op_void_async_deferred } = core.generateAsyncOpHandler(
"op_sleep",
"op_void_async_deferred",
);
const hrU8 = new Uint8Array(8);
const hr = new Uint32Array(TypedArrayPrototypeGetBuffer(hrU8));
@ -218,7 +221,16 @@ const scheduledTimers = { head: null, tail: null };
*/
function runAfterTimeout(cb, millis, timerInfo) {
const cancelRid = timerInfo.cancelRid;
const sleepPromise = op_sleep(millis, cancelRid);
let sleepPromise;
// If this timeout is scheduled for 0ms it means we want it to run at the
// end of the event loop turn. There's no point in setting up a Tokio timer,
// since its lowest resolution is 1ms. Firing of a "void async" op is better
// in this case, because the timer will take closer to 0ms instead of >1ms.
if (millis === 0) {
sleepPromise = op_void_async_deferred();
} else {
sleepPromise = op_sleep(millis, cancelRid);
}
timerInfo.promiseId = sleepPromise[SymbolFor("Deno.core.internalPromiseId")];
if (!timerInfo.isRef) {
core.unrefOp(timerInfo.promiseId);
@ -246,7 +258,8 @@ function runAfterTimeout(cb, millis, timerInfo) {
PromisePrototypeThen(
sleepPromise,
(cancelled) => {
if (!cancelled) {
// "op_void_async_deferred" returns null
if (cancelled !== null && !cancelled) {
// The timer was cancelled.
removeFromScheduledTimers(timerObject);
return;