diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2020-03-11 10:53:06 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-11 10:53:06 +0100 |
commit | 99a0c6df79b903e4fe72ce066787039bdede3868 (patch) | |
tree | 1c0abdb964a2e052b593dc8fa3e515f76dbc0642 /cli/js/web/timers.ts | |
parent | 94f4c6807a34a564f567b984e71e36e9cb9c5005 (diff) |
reorg: cli/js/compiler/, move more API to cli/js/web/ (#4310)
- moves compiler implementation to "cli/js/compiler/" directory
- moves more APIs to "cli/js/web":
* "console.ts"
* "console_table.ts"
* "performance.ts"
* "timers.ts"
* "workers.ts"
- removes some dead code from "cli/js/"
Diffstat (limited to 'cli/js/web/timers.ts')
-rw-r--r-- | cli/js/web/timers.ts | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/cli/js/web/timers.ts b/cli/js/web/timers.ts new file mode 100644 index 000000000..806b7c160 --- /dev/null +++ b/cli/js/web/timers.ts @@ -0,0 +1,315 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { assert } from "../util.ts"; +import { startGlobalTimer, stopGlobalTimer } from "../ops/timers.ts"; +import { RBTree } from "../rbtree.ts"; + +const { console } = globalThis; + +interface Timer { + id: number; + callback: () => void; + delay: number; + due: number; + repeat: boolean; + scheduled: boolean; +} + +// Timeout values > TIMEOUT_MAX are set to 1. +const TIMEOUT_MAX = 2 ** 31 - 1; + +let globalTimeoutDue: number | null = null; + +let nextTimerId = 1; +const idMap = new Map<number, Timer>(); +type DueNode = { due: number; timers: Timer[] }; +const dueTree = new RBTree<DueNode>((a, b) => a.due - b.due); + +function clearGlobalTimeout(): void { + globalTimeoutDue = null; + stopGlobalTimer(); +} + +let pendingEvents = 0; +const pendingFireTimers: Timer[] = []; +let hasPendingFireTimers = false; +let pendingScheduleTimers: Timer[] = []; + +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 + // relative time value. On the Rust side we'll turn that into an absolute + // value again. + const timeout = due - now; + assert(timeout >= 0); + // Send message to the backend. + globalTimeoutDue = due; + pendingEvents++; + // FIXME(bartlomieju): this is problematic, because `clearGlobalTimeout` + // is synchronous. That means that timer is cancelled, but this promise is still pending + // until next turn of event loop. This leads to "leaking of async ops" in tests; + // because `clearTimeout/clearInterval` might be the last statement in test function + // `opSanitizer` will immediately complain that there is pending op going on, unless + // some timeout/defer is put in place to allow promise resolution. + // Ideally `clearGlobalTimeout` doesn't return until this op is resolved, but + // I'm not if that's possible. + await startGlobalTimer(timeout); + pendingEvents--; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + fireTimers(); +} + +function setOrClearGlobalTimeout(due: number | null, now: number): void { + if (due == null) { + clearGlobalTimeout(); + } else { + setGlobalTimeout(due, now); + } +} + +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); + if (dueNode === null) { + dueTree.insert(maybeNewDueNode); + dueNode = maybeNewDueNode; + } + // Append the newly scheduled timer to the list and mark it as scheduled. + dueNode!.timers.push(timer); + timer.scheduled = true; + // If the new timer is scheduled to fire before any timer that existed before, + // update the global timeout to reflect this. + if (globalTimeoutDue === null || globalTimeoutDue > timer.due) { + setOrClearGlobalTimeout(timer.due, now); + } +} + +function unschedule(timer: Timer): void { + // Check if our timer is pending scheduling or pending firing. + // 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; + } + // If timer is not in the 2 pending queues and is unscheduled, + // it is not in the tree. + if (!timer.scheduled) { + return; + } + const searchKey = { due: timer.due, timers: [] }; + // Find the list of timers that will fire at point-in-time `due`. + const list = dueTree.find(searchKey)!.timers; + if (list.length === 1) { + // Time timer is the only one in the list. Remove the entire list. + assert(list[0] === timer); + dueTree.remove(searchKey); + // If the unscheduled timer was 'next up', find when the next timer that + // still exists is due, and update the global alarm accordingly. + if (timer.due === globalTimeoutDue) { + const nextDueNode: DueNode | null = dueTree.min(); + setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, Date.now()); + } + } else { + // Multiple timers that are due at the same point in time. + // Remove this timer from the list. + const index = list.indexOf(timer); + assert(index > -1); + list.splice(index, 1); + } +} + +function fire(timer: Timer): void { + // If the timer isn't found in the ID map, that means it has been cancelled + // between the timer firing and the promise callback (this function). + if (!idMap.has(timer.id)) { + return; + } + // Reschedule the timer if it is a repeating one, otherwise drop it. + if (!timer.repeat) { + // One-shot timer: remove the timer from this id-to-timer map. + idMap.delete(timer.id); + } else { + // Interval timer: compute when timer was supposed to fire next. + // However make sure to never schedule the next interval in the past. + const now = Date.now(); + timer.due = Math.max(now, timer.due + timer.delay); + schedule(timer, now); + } + // Call the user callback. Intermediate assignment is to avoid leaking `this` + // to it, while also keeping the stack trace neat when it shows up in there. + const callback = timer.callback; + 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 { + if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) { + throw new TypeError("Illegal invocation"); + } +} + +function checkBigInt(n: unknown): void { + if (typeof n === "bigint") { + throw new TypeError("Cannot convert a BigInt value to a number"); + } +} + +function setTimer( + cb: (...args: Args) => void, + delay: number, + args: Args, + repeat: boolean +): number { + // Bind `args` to the callback and bind `this` to globalThis(global). + const callback: () => void = cb.bind(globalThis, ...args); + // In the browser, the delay value must be coercible to an integer between 0 + // and INT32_MAX. Any other value will cause the timer to fire immediately. + // We emulate this behavior. + const now = Date.now(); + if (delay > TIMEOUT_MAX) { + console.warn( + `${delay} does not fit into` + + " a 32-bit signed integer." + + "\nTimeout duration was set to 1." + ); + delay = 1; + } + delay = Math.max(0, delay | 0); + + // Create a new, unscheduled timer object. + const timer = { + id: nextTimerId++, + callback, + args, + delay, + due: now + delay, + repeat, + scheduled: false + }; + // Register the timer's existence in the id-to-timer map. + idMap.set(timer.id, timer); + // Schedule the timer in the due table. + schedule(timer, now); + return timer.id; +} + +/** Sets a timer which executes a function once after the timer expires. */ +export function setTimeout( + cb: (...args: Args) => void, + delay = 0, + ...args: Args +): number { + checkBigInt(delay); + // @ts-ignore + checkThis(this); + return setTimer(cb, delay, args, false); +} + +/** Repeatedly calls a function, with a fixed time delay between each call. */ +export function setInterval( + cb: (...args: Args) => void, + delay = 0, + ...args: Args +): number { + checkBigInt(delay); + // @ts-ignore + checkThis(this); + return setTimer(cb, delay, args, true); +} + +/** Clears a previously set timer by id. AKA clearTimeout and clearInterval. */ +function clearTimer(id: number): void { + id = Number(id); + const timer = idMap.get(id); + if (timer === undefined) { + // Timer doesn't exist any more or never existed. This is not an error. + return; + } + // Unschedule the timer if it is currently scheduled, and forget about it. + unschedule(timer); + idMap.delete(timer.id); +} + +export function clearTimeout(id = 0): void { + checkBigInt(id); + if (id === 0) { + return; + } + clearTimer(id); +} + +export function clearInterval(id = 0): void { + checkBigInt(id); + if (id === 0) { + return; + } + clearTimer(id); +} |