diff options
Diffstat (limited to 'cli/js')
-rw-r--r-- | cli/js/globals.ts | 2 | ||||
-rw-r--r-- | cli/js/runtime.ts | 2 | ||||
-rw-r--r-- | cli/js/tests/timers_test.ts | 31 | ||||
-rw-r--r-- | cli/js/web/timers.ts | 105 |
4 files changed, 63 insertions, 77 deletions
diff --git a/cli/js/globals.ts b/cli/js/globals.ts index 1104762b9..ada491bde 100644 --- a/cli/js/globals.ts +++ b/cli/js/globals.ts @@ -91,6 +91,8 @@ declare global { data?: ArrayBufferView ): null | Uint8Array; + setMacrotaskCallback(cb: () => boolean): void; + shared: SharedArrayBuffer; evalContext( diff --git a/cli/js/runtime.ts b/cli/js/runtime.ts index db755d954..d586503af 100644 --- a/cli/js/runtime.ts +++ b/cli/js/runtime.ts @@ -7,6 +7,7 @@ import { setBuildInfo } from "./build.ts"; import { setVersions } from "./version.ts"; import { setPrepareStackTrace } from "./error_stack.ts"; import { Start, start as startOp } from "./ops/runtime.ts"; +import { handleTimerMacrotask } from "./web/timers.ts"; export let OPS_CACHE: { [name: string]: number }; @@ -27,6 +28,7 @@ export function initOps(): void { for (const [name, opId] of Object.entries(OPS_CACHE)) { core.setAsyncHandler(opId, getAsyncHandler(name)); } + core.setMacrotaskCallback(handleTimerMacrotask); } export function start(source?: string): Start { diff --git a/cli/js/tests/timers_test.ts b/cli/js/tests/timers_test.ts index 429e42692..077e27ae6 100644 --- a/cli/js/tests/timers_test.ts +++ b/cli/js/tests/timers_test.ts @@ -127,6 +127,9 @@ unitTest(async function intervalSuccess(): Promise<void> { clearInterval(id); // count should increment twice assertEquals(count, 1); + // Similar false async leaking alarm. + // Force next round of polling. + await waitForMs(0); }); unitTest(async function intervalCancelSuccess(): Promise<void> { @@ -330,24 +333,32 @@ unitTest(async function timerNestedMicrotaskOrdering(): Promise<void> { s += "0"; setTimeout(() => { s += "4"; - setTimeout(() => (s += "8")); - Promise.resolve().then(() => { - setTimeout(() => { - s += "9"; - resolve(); + setTimeout(() => (s += "A")); + Promise.resolve() + .then(() => { + setTimeout(() => { + s += "B"; + resolve(); + }); + }) + .then(() => { + s += "5"; }); - }); }); - setTimeout(() => (s += "5")); + setTimeout(() => (s += "6")); Promise.resolve().then(() => (s += "2")); Promise.resolve().then(() => setTimeout(() => { - s += "6"; - Promise.resolve().then(() => (s += "7")); + s += "7"; + Promise.resolve() + .then(() => (s += "8")) + .then(() => { + s += "9"; + }); }) ); Promise.resolve().then(() => Promise.resolve().then(() => (s += "3"))); s += "1"; await promise; - assertEquals(s, "0123456789"); + assertEquals(s, "0123456789AB"); }); diff --git a/cli/js/web/timers.ts b/cli/js/web/timers.ts index 520a2722a..9a957f3fe 100644 --- a/cli/js/web/timers.ts +++ b/cli/js/web/timers.ts @@ -31,8 +31,21 @@ function clearGlobalTimeout(): void { let pendingEvents = 0; const pendingFireTimers: Timer[] = []; -let hasPendingFireTimers = false; -let pendingScheduleTimers: Timer[] = []; + +/** Process and run a single ready timer macrotask. + * This function should be registered through Deno.core.setMacrotaskCallback. + * Returns true when all ready macrotasks have been processed, false if more + * ready ones are available. The Isolate future would rely on the return value + * to repeatedly invoke this function until depletion. Multiple invocations + * of this function one at a time ensures newly ready microtasks are processed + * before next macrotask timer callback is invoked. */ +export function handleTimerMacrotask(): boolean { + if (pendingFireTimers.length > 0) { + fire(pendingFireTimers.shift()!); + return pendingFireTimers.length === 0; + } + return true; +} async function setGlobalTimeout(due: number, now: number): Promise<void> { // Since JS and Rust don't use the same clock, pass the time to rust as a @@ -54,7 +67,29 @@ async function setGlobalTimeout(due: number, now: number): Promise<void> { await startGlobalTimer(timeout); pendingEvents--; // eslint-disable-next-line @typescript-eslint/no-use-before-define - fireTimers(); + prepareReadyTimers(); +} + +function prepareReadyTimers(): void { + const now = Date.now(); + // Bail out if we're not expecting the global timer to fire. + if (globalTimeoutDue === null || pendingEvents > 0) { + return; + } + // After firing the timers that are due now, this will hold the first timer + // list that hasn't fired yet. + let nextDueNode: DueNode | null; + while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) { + dueTree.remove(nextDueNode); + // Fire all the timers in the list. + for (const timer of nextDueNode.timers) { + // With the list dropped, the timer is no longer scheduled. + timer.scheduled = false; + // Place the callback to pending timers to fire. + pendingFireTimers.push(timer); + } + } + setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now); } function setOrClearGlobalTimeout(due: number | null, now: number): void { @@ -68,15 +103,6 @@ function setOrClearGlobalTimeout(due: number | null, now: number): void { function schedule(timer: Timer, now: number): void { assert(!timer.scheduled); assert(now <= timer.due); - // There are more timers pending firing. - // We must ensure new timer scheduled after them. - // Push them to a queue that would be depleted after last pending fire - // timer is fired. - // (This also implies behavior of setInterval) - if (hasPendingFireTimers) { - pendingScheduleTimers.push(timer); - return; - } // Find or create the list of timers that will fire at point-in-time `due`. const maybeNewDueNode = { due: timer.due, timers: [] }; let dueNode = dueTree.find(maybeNewDueNode); @@ -99,10 +125,6 @@ function unschedule(timer: Timer): void { // If either is true, they are not in tree, and their idMap entry // will be deleted soon. Remove it from queue. let index = -1; - if ((index = pendingScheduleTimers.indexOf(timer)) >= 0) { - pendingScheduleTimers.splice(index); - return; - } if ((index = pendingFireTimers.indexOf(timer)) >= 0) { pendingFireTimers.splice(index); return; @@ -157,57 +179,6 @@ function fire(timer: Timer): void { callback(); } -function fireTimers(): void { - const now = Date.now(); - // Bail out if we're not expecting the global timer to fire. - if (globalTimeoutDue === null || pendingEvents > 0) { - return; - } - // After firing the timers that are due now, this will hold the first timer - // list that hasn't fired yet. - let nextDueNode: DueNode | null; - while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) { - dueTree.remove(nextDueNode); - // Fire all the timers in the list. - for (const timer of nextDueNode.timers) { - // With the list dropped, the timer is no longer scheduled. - timer.scheduled = false; - // Place the callback to pending timers to fire. - pendingFireTimers.push(timer); - } - } - if (pendingFireTimers.length > 0) { - hasPendingFireTimers = true; - // Fire the list of pending timers as a chain of microtasks. - globalThis.queueMicrotask(firePendingTimers); - } else { - setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now); - } -} - -function firePendingTimers(): void { - if (pendingFireTimers.length === 0) { - // All timer tasks are done. - hasPendingFireTimers = false; - // Schedule all new timers pushed during previous timer executions - const now = Date.now(); - for (const newTimer of pendingScheduleTimers) { - newTimer.due = Math.max(newTimer.due, now); - schedule(newTimer, now); - } - pendingScheduleTimers = []; - // Reschedule for next round of timeout. - const nextDueNode = dueTree.min(); - const due = nextDueNode && Math.max(nextDueNode.due, now); - setOrClearGlobalTimeout(due, now); - } else { - // Fire a single timer and allow its children microtasks scheduled first. - fire(pendingFireTimers.shift()!); - // ...and we schedule next timer after this. - globalThis.queueMicrotask(firePendingTimers); - } -} - export type Args = unknown[]; function checkThis(thisArg: unknown): void { |