1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-08 15:19:40 -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:
Kevin (Kun) Kassimo Qian 2019-10-26 15:29:32 -07:00 committed by Bert Belder
parent d7a5aed511
commit 9d6cbb73a8
No known key found for this signature in database
GPG key ID: 7A77887B2E2ED461
2 changed files with 265 additions and 51 deletions

241
cli/js/rbtree.ts Normal file
View 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 };

View file

@ -3,6 +3,7 @@ import { assert } from "./util.ts";
import { window } from "./window.ts";
import * as dispatch from "./dispatch.ts";
import { sendSync, sendAsync } from "./dispatch_json.ts";
import { RBTree } from "./rbtree.ts";
const { console } = window;
@ -15,14 +16,6 @@ interface Timer {
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.
const TIMEOUT_MAX = 2 ** 31 - 1;
@ -30,14 +23,8 @@ let globalTimeoutDue: number | null = null;
let nextTimerId = 1;
const idMap = new Map<number, Timer>();
const dueMap: { [due: number]: Timer[] } = Object.create(null);
function getTime(): number {
// TODO: use a monotonic clock.
const now = Date.now() - EPOCH;
assert(now >= 0 && now < APOCALYPSE);
return now;
}
type DueNode = { due: number; timers: Timer[] };
const dueTree = new RBTree<DueNode>((a, b) => a.due - b.due);
function clearGlobalTimeout(): void {
globalTimeoutDue = null;
@ -73,12 +60,14 @@ function schedule(timer: Timer, now: number): void {
assert(!timer.scheduled);
assert(now <= timer.due);
// Find or create the list of timers that will fire at point-in-time `due`.
let list = dueMap[timer.due];
if (list === undefined) {
list = dueMap[timer.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.
list.push(timer);
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.
@ -91,21 +80,18 @@ function unschedule(timer: Timer): void {
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 = dueMap[timer.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);
delete dueMap[timer.due];
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) {
let nextTimerDue: number | null = null;
for (const key in dueMap) {
nextTimerDue = Number(key);
break;
}
setOrClearGlobalTimeout(nextTimerDue, getTime());
const nextDueNode: DueNode | null = dueTree.min();
setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, Date.now());
}
} else {
// Multiple timers that are due at the same point in time.
@ -129,7 +115,7 @@ function fire(timer: Timer): void {
} 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 = getTime();
const now = Date.now();
timer.due = Math.max(now, timer.due + timer.delay);
schedule(timer, now);
}
@ -140,40 +126,27 @@ function fire(timer: Timer): void {
}
function fireTimers(): void {
const now = getTime();
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 due time of
// the first timer that hasn't fired yet.
let nextTimerDue: number | null = null;
// Walk over the keys of the 'due' map. Since dueMap is actually a regular
// object and its keys are numerical and smaller than UINT32_MAX - 2,
// 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];
// 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 list) {
for (const timer of nextDueNode.timers) {
// With the list dropped, the timer is no longer scheduled.
timer.scheduled = false;
// Place the callback on the microtask queue.
Promise.resolve(timer).then(fire);
}
}
// Update the global alarm to go off when the first-up timer that hasn't fired
// yet is due.
setOrClearGlobalTimeout(nextTimerDue, now);
setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now);
}
export type Args = unknown[];
@ -201,7 +174,7 @@ function setTimer(
// 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 = getTime();
const now = Date.now();
if (delay > TIMEOUT_MAX) {
console.warn(
`${delay} does not fit into` +