mirror of
https://github.com/denoland/deno.git
synced 2024-12-23 07:44:48 -05:00
ee452ad883
* add "assertOps" test assertion which makes sure test case is not "leaking" ops - ie. after test finishes there are no pending async ops * apply "assertOps" to all tests in "cli/js/" * fix numerous tests leaking ops * document problem with edge case in "clearInterval" and "clearTimeout" implementation where they may leak async ops * move "cli/js/worker_test.ts" to "cli/tests/worker_test.ts" and run as integration test; workers leak ops because of missing "terminate" implementation
315 lines
9.9 KiB
TypeScript
315 lines
9.9 KiB
TypeScript
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
|
import { assert } from "./util.ts";
|
|
import { sendSync, sendAsync } from "./dispatch_json.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;
|
|
sendSync("op_global_timer_stop");
|
|
}
|
|
|
|
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 sendAsync("op_global_timer", { 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);
|
|
}
|