diff options
Diffstat (limited to 'cli/js')
-rw-r--r-- | cli/js/rbtree.ts | 241 | ||||
-rw-r--r-- | cli/js/timers.ts | 75 |
2 files changed, 265 insertions, 51 deletions
diff --git a/cli/js/rbtree.ts b/cli/js/rbtree.ts new file mode 100644 index 000000000..5de9f125d --- /dev/null +++ b/cli/js/rbtree.ts @@ -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 }; diff --git a/cli/js/timers.ts b/cli/js/timers.ts index 9ebe4f6cb..ef7318126 100644 --- a/cli/js/timers.ts +++ b/cli/js/timers.ts @@ -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` + |