mirror of
https://github.com/denoland/deno.git
synced 2025-02-01 12:16:11 -05:00
cli: replace timer map with red-black tree (#3218)
This avoids a crash when the Deno process has been running for 2**32 ms (about 50 days). Additionaly, time complexity of finding which timer is due to fire next is reduced from from O(n) to O(log n).
This commit is contained in:
parent
d7a5aed511
commit
9d6cbb73a8
2 changed files with 265 additions and 51 deletions
241
cli/js/rbtree.ts
Normal file
241
cli/js/rbtree.ts
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
// Derived from https://github.com/vadimg/js_bintrees. MIT Licensed.
|
||||||
|
|
||||||
|
class RBNode<T> {
|
||||||
|
public left: this | null;
|
||||||
|
public right: this | null;
|
||||||
|
public red: boolean;
|
||||||
|
|
||||||
|
constructor(public data: T) {
|
||||||
|
this.left = null;
|
||||||
|
this.right = null;
|
||||||
|
this.red = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getChild(dir: boolean | number): this | null {
|
||||||
|
return dir ? this.right : this.left;
|
||||||
|
}
|
||||||
|
|
||||||
|
setChild(dir: boolean | number, val: this | null): void {
|
||||||
|
if (dir) {
|
||||||
|
this.right = val;
|
||||||
|
} else {
|
||||||
|
this.left = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RBTree<T> {
|
||||||
|
private root: RBNode<T> | null;
|
||||||
|
|
||||||
|
constructor(private comparator: (a: T, b: T) => number) {
|
||||||
|
this.root = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns null if tree is empty
|
||||||
|
min(): T | null {
|
||||||
|
let res = this.root;
|
||||||
|
if (res === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
while (res.left !== null) {
|
||||||
|
res = res.left;
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns node data if found, null otherwise
|
||||||
|
find(data: T): T | null {
|
||||||
|
let res = this.root;
|
||||||
|
while (res !== null) {
|
||||||
|
const c = this.comparator(data, res.data!);
|
||||||
|
if (c === 0) {
|
||||||
|
return res.data;
|
||||||
|
} else {
|
||||||
|
res = res.getChild(c > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if inserted, false if duplicate
|
||||||
|
insert(data: T): boolean {
|
||||||
|
let ret = false;
|
||||||
|
|
||||||
|
if (this.root === null) {
|
||||||
|
// empty tree
|
||||||
|
this.root = new RBNode(data);
|
||||||
|
ret = true;
|
||||||
|
} else {
|
||||||
|
const head = new RBNode((null as unknown) as T); // fake tree root
|
||||||
|
|
||||||
|
let dir = 0;
|
||||||
|
let last = 0;
|
||||||
|
|
||||||
|
// setup
|
||||||
|
let gp = null; // grandparent
|
||||||
|
let ggp = head; // grand-grand-parent
|
||||||
|
let p: RBNode<T> | null = null; // parent
|
||||||
|
let node: RBNode<T> | null = this.root;
|
||||||
|
ggp.right = this.root;
|
||||||
|
|
||||||
|
// search down
|
||||||
|
while (true) {
|
||||||
|
if (node === null) {
|
||||||
|
// insert new node at the bottom
|
||||||
|
node = new RBNode(data);
|
||||||
|
p!.setChild(dir, node);
|
||||||
|
ret = true;
|
||||||
|
} else if (isRed(node.left) && isRed(node.right)) {
|
||||||
|
// color flip
|
||||||
|
node.red = true;
|
||||||
|
node.left!.red = false;
|
||||||
|
node.right!.red = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix red violation
|
||||||
|
if (isRed(node) && isRed(p)) {
|
||||||
|
const dir2 = ggp.right === gp;
|
||||||
|
|
||||||
|
if (node === p!.getChild(last)) {
|
||||||
|
ggp.setChild(dir2, singleRotate(gp!, !last));
|
||||||
|
} else {
|
||||||
|
ggp.setChild(dir2, doubleRotate(gp!, !last));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmp = this.comparator(node.data!, data);
|
||||||
|
|
||||||
|
// stop if found
|
||||||
|
if (cmp === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
last = dir;
|
||||||
|
dir = Number(cmp < 0); // Fix type
|
||||||
|
|
||||||
|
// update helpers
|
||||||
|
if (gp !== null) {
|
||||||
|
ggp = gp;
|
||||||
|
}
|
||||||
|
gp = p;
|
||||||
|
p = node;
|
||||||
|
node = node.getChild(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update root
|
||||||
|
this.root = head.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make root black
|
||||||
|
this.root!.red = false;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if removed, false if not found
|
||||||
|
remove(data: T): boolean {
|
||||||
|
if (this.root === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const head = new RBNode((null as unknown) as T); // fake tree root
|
||||||
|
let node = head;
|
||||||
|
node.right = this.root;
|
||||||
|
let p = null; // parent
|
||||||
|
let gp = null; // grand parent
|
||||||
|
let found = null; // found item
|
||||||
|
let dir: boolean | number = 1;
|
||||||
|
|
||||||
|
while (node.getChild(dir) !== null) {
|
||||||
|
const last = dir;
|
||||||
|
|
||||||
|
// update helpers
|
||||||
|
gp = p;
|
||||||
|
p = node;
|
||||||
|
node = node.getChild(dir)!;
|
||||||
|
|
||||||
|
const cmp = this.comparator(data, node.data);
|
||||||
|
|
||||||
|
dir = cmp > 0;
|
||||||
|
|
||||||
|
// save found node
|
||||||
|
if (cmp === 0) {
|
||||||
|
found = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push the red node down
|
||||||
|
if (!isRed(node) && !isRed(node.getChild(dir))) {
|
||||||
|
if (isRed(node.getChild(!dir))) {
|
||||||
|
const sr = singleRotate(node, dir);
|
||||||
|
p.setChild(last, sr);
|
||||||
|
p = sr;
|
||||||
|
} else if (!isRed(node.getChild(!dir))) {
|
||||||
|
const sibling = p.getChild(!last);
|
||||||
|
if (sibling !== null) {
|
||||||
|
if (
|
||||||
|
!isRed(sibling.getChild(!last)) &&
|
||||||
|
!isRed(sibling.getChild(last))
|
||||||
|
) {
|
||||||
|
// color flip
|
||||||
|
p.red = false;
|
||||||
|
sibling.red = true;
|
||||||
|
node.red = true;
|
||||||
|
} else {
|
||||||
|
const dir2 = gp!.right === p;
|
||||||
|
|
||||||
|
if (isRed(sibling.getChild(last))) {
|
||||||
|
gp!.setChild(dir2, doubleRotate(p, last));
|
||||||
|
} else if (isRed(sibling.getChild(!last))) {
|
||||||
|
gp!.setChild(dir2, singleRotate(p, last));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure correct coloring
|
||||||
|
const gpc = gp!.getChild(dir2)!;
|
||||||
|
gpc.red = true;
|
||||||
|
node.red = true;
|
||||||
|
gpc.left!.red = false;
|
||||||
|
gpc.right!.red = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace and remove if found
|
||||||
|
if (found !== null) {
|
||||||
|
found.data = node.data;
|
||||||
|
p!.setChild(p!.right === node, node.getChild(node.left === null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// update root and make it black
|
||||||
|
this.root = head.right;
|
||||||
|
if (this.root !== null) {
|
||||||
|
this.root.red = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return found !== null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRed<T>(node: RBNode<T> | null): boolean {
|
||||||
|
return node !== null && node.red;
|
||||||
|
}
|
||||||
|
|
||||||
|
function singleRotate<T>(root: RBNode<T>, dir: boolean | number): RBNode<T> {
|
||||||
|
const save = root.getChild(!dir)!;
|
||||||
|
|
||||||
|
root.setChild(!dir, save.getChild(dir));
|
||||||
|
save.setChild(dir, root);
|
||||||
|
|
||||||
|
root.red = true;
|
||||||
|
save.red = false;
|
||||||
|
|
||||||
|
return save;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doubleRotate<T>(root: RBNode<T>, dir: boolean | number): RBNode<T> {
|
||||||
|
root.setChild(!dir, singleRotate(root.getChild(!dir)!, !dir));
|
||||||
|
return singleRotate(root, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RBTree };
|
|
@ -3,6 +3,7 @@ import { assert } from "./util.ts";
|
||||||
import { window } from "./window.ts";
|
import { window } from "./window.ts";
|
||||||
import * as dispatch from "./dispatch.ts";
|
import * as dispatch from "./dispatch.ts";
|
||||||
import { sendSync, sendAsync } from "./dispatch_json.ts";
|
import { sendSync, sendAsync } from "./dispatch_json.ts";
|
||||||
|
import { RBTree } from "./rbtree.ts";
|
||||||
|
|
||||||
const { console } = window;
|
const { console } = window;
|
||||||
|
|
||||||
|
@ -15,14 +16,6 @@ interface Timer {
|
||||||
scheduled: boolean;
|
scheduled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll subtract EPOCH every time we retrieve the time with Date.now(). This
|
|
||||||
// ensures that absolute time values stay below UINT32_MAX - 2, which is the
|
|
||||||
// maximum object key that EcmaScript considers "numerical". After running for
|
|
||||||
// about a month, this is no longer true, and Deno explodes.
|
|
||||||
// TODO(piscisaureus): fix that ^.
|
|
||||||
const EPOCH = Date.now();
|
|
||||||
const APOCALYPSE = 2 ** 32 - 2;
|
|
||||||
|
|
||||||
// Timeout values > TIMEOUT_MAX are set to 1.
|
// Timeout values > TIMEOUT_MAX are set to 1.
|
||||||
const TIMEOUT_MAX = 2 ** 31 - 1;
|
const TIMEOUT_MAX = 2 ** 31 - 1;
|
||||||
|
|
||||||
|
@ -30,14 +23,8 @@ let globalTimeoutDue: number | null = null;
|
||||||
|
|
||||||
let nextTimerId = 1;
|
let nextTimerId = 1;
|
||||||
const idMap = new Map<number, Timer>();
|
const idMap = new Map<number, Timer>();
|
||||||
const dueMap: { [due: number]: Timer[] } = Object.create(null);
|
type DueNode = { due: number; timers: Timer[] };
|
||||||
|
const dueTree = new RBTree<DueNode>((a, b) => a.due - b.due);
|
||||||
function getTime(): number {
|
|
||||||
// TODO: use a monotonic clock.
|
|
||||||
const now = Date.now() - EPOCH;
|
|
||||||
assert(now >= 0 && now < APOCALYPSE);
|
|
||||||
return now;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearGlobalTimeout(): void {
|
function clearGlobalTimeout(): void {
|
||||||
globalTimeoutDue = null;
|
globalTimeoutDue = null;
|
||||||
|
@ -73,12 +60,14 @@ function schedule(timer: Timer, now: number): void {
|
||||||
assert(!timer.scheduled);
|
assert(!timer.scheduled);
|
||||||
assert(now <= timer.due);
|
assert(now <= timer.due);
|
||||||
// Find or create the list of timers that will fire at point-in-time `due`.
|
// Find or create the list of timers that will fire at point-in-time `due`.
|
||||||
let list = dueMap[timer.due];
|
const maybeNewDueNode = { due: timer.due, timers: [] };
|
||||||
if (list === undefined) {
|
let dueNode = dueTree.find(maybeNewDueNode);
|
||||||
list = dueMap[timer.due] = [];
|
if (dueNode === null) {
|
||||||
|
dueTree.insert(maybeNewDueNode);
|
||||||
|
dueNode = maybeNewDueNode;
|
||||||
}
|
}
|
||||||
// Append the newly scheduled timer to the list and mark it as scheduled.
|
// Append the newly scheduled timer to the list and mark it as scheduled.
|
||||||
list.push(timer);
|
dueNode!.timers.push(timer);
|
||||||
timer.scheduled = true;
|
timer.scheduled = true;
|
||||||
// If the new timer is scheduled to fire before any timer that existed before,
|
// If the new timer is scheduled to fire before any timer that existed before,
|
||||||
// update the global timeout to reflect this.
|
// update the global timeout to reflect this.
|
||||||
|
@ -91,21 +80,18 @@ function unschedule(timer: Timer): void {
|
||||||
if (!timer.scheduled) {
|
if (!timer.scheduled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const searchKey = { due: timer.due, timers: [] };
|
||||||
// Find the list of timers that will fire at point-in-time `due`.
|
// Find the list of timers that will fire at point-in-time `due`.
|
||||||
const list = dueMap[timer.due];
|
const list = dueTree.find(searchKey)!.timers;
|
||||||
if (list.length === 1) {
|
if (list.length === 1) {
|
||||||
// Time timer is the only one in the list. Remove the entire list.
|
// Time timer is the only one in the list. Remove the entire list.
|
||||||
assert(list[0] === timer);
|
assert(list[0] === timer);
|
||||||
delete dueMap[timer.due];
|
dueTree.remove(searchKey);
|
||||||
// If the unscheduled timer was 'next up', find when the next timer that
|
// If the unscheduled timer was 'next up', find when the next timer that
|
||||||
// still exists is due, and update the global alarm accordingly.
|
// still exists is due, and update the global alarm accordingly.
|
||||||
if (timer.due === globalTimeoutDue) {
|
if (timer.due === globalTimeoutDue) {
|
||||||
let nextTimerDue: number | null = null;
|
const nextDueNode: DueNode | null = dueTree.min();
|
||||||
for (const key in dueMap) {
|
setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, Date.now());
|
||||||
nextTimerDue = Number(key);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
setOrClearGlobalTimeout(nextTimerDue, getTime());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Multiple timers that are due at the same point in time.
|
// Multiple timers that are due at the same point in time.
|
||||||
|
@ -129,7 +115,7 @@ function fire(timer: Timer): void {
|
||||||
} else {
|
} else {
|
||||||
// Interval timer: compute when timer was supposed to fire next.
|
// Interval timer: compute when timer was supposed to fire next.
|
||||||
// However make sure to never schedule the next interval in the past.
|
// However make sure to never schedule the next interval in the past.
|
||||||
const now = getTime();
|
const now = Date.now();
|
||||||
timer.due = Math.max(now, timer.due + timer.delay);
|
timer.due = Math.max(now, timer.due + timer.delay);
|
||||||
schedule(timer, now);
|
schedule(timer, now);
|
||||||
}
|
}
|
||||||
|
@ -140,40 +126,27 @@ function fire(timer: Timer): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fireTimers(): void {
|
function fireTimers(): void {
|
||||||
const now = getTime();
|
const now = Date.now();
|
||||||
// Bail out if we're not expecting the global timer to fire.
|
// Bail out if we're not expecting the global timer to fire.
|
||||||
if (globalTimeoutDue === null || pendingEvents > 0) {
|
if (globalTimeoutDue === null || pendingEvents > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// After firing the timers that are due now, this will hold the due time of
|
// After firing the timers that are due now, this will hold the first timer
|
||||||
// the first timer that hasn't fired yet.
|
// list that hasn't fired yet.
|
||||||
let nextTimerDue: number | null = null;
|
let nextDueNode: DueNode | null;
|
||||||
// Walk over the keys of the 'due' map. Since dueMap is actually a regular
|
while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) {
|
||||||
// object and its keys are numerical and smaller than UINT32_MAX - 2,
|
dueTree.remove(nextDueNode);
|
||||||
// keys are iterated in ascending order.
|
|
||||||
for (const key in dueMap) {
|
|
||||||
// Convert the object key (a string) to a number.
|
|
||||||
const due = Number(key);
|
|
||||||
// Break out of the loop if the next timer isn't due to fire yet.
|
|
||||||
if (Number(due) > now) {
|
|
||||||
nextTimerDue = due;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Get the list of timers that have this due time, then drop it.
|
|
||||||
const list = dueMap[key];
|
|
||||||
delete dueMap[key];
|
|
||||||
// Fire all the timers in the list.
|
// Fire all the timers in the list.
|
||||||
for (const timer of list) {
|
for (const timer of nextDueNode.timers) {
|
||||||
// With the list dropped, the timer is no longer scheduled.
|
// With the list dropped, the timer is no longer scheduled.
|
||||||
timer.scheduled = false;
|
timer.scheduled = false;
|
||||||
// Place the callback on the microtask queue.
|
// Place the callback on the microtask queue.
|
||||||
Promise.resolve(timer).then(fire);
|
Promise.resolve(timer).then(fire);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the global alarm to go off when the first-up timer that hasn't fired
|
// Update the global alarm to go off when the first-up timer that hasn't fired
|
||||||
// yet is due.
|
// yet is due.
|
||||||
setOrClearGlobalTimeout(nextTimerDue, now);
|
setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Args = unknown[];
|
export type Args = unknown[];
|
||||||
|
@ -201,7 +174,7 @@ function setTimer(
|
||||||
// In the browser, the delay value must be coercible to an integer between 0
|
// 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.
|
// and INT32_MAX. Any other value will cause the timer to fire immediately.
|
||||||
// We emulate this behavior.
|
// We emulate this behavior.
|
||||||
const now = getTime();
|
const now = Date.now();
|
||||||
if (delay > TIMEOUT_MAX) {
|
if (delay > TIMEOUT_MAX) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`${delay} does not fit into` +
|
`${delay} does not fit into` +
|
||||||
|
|
Loading…
Add table
Reference in a new issue