diff options
author | Ryan Dahl <ry@tinyclouds.org> | 2021-08-11 12:27:05 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-11 12:27:05 +0200 |
commit | a0285e2eb88f6254f6494b0ecd1878db3a3b2a58 (patch) | |
tree | 90671b004537e20f9493fd3277ffd21d30b39a0e /ext/timers | |
parent | 3a6994115176781b3a93d70794b1b81bc95e42b4 (diff) |
Rename extensions/ directory to ext/ (#11643)
Diffstat (limited to 'ext/timers')
-rw-r--r-- | ext/timers/01_timers.js | 595 | ||||
-rw-r--r-- | ext/timers/02_performance.js | 569 | ||||
-rw-r--r-- | ext/timers/Cargo.toml | 28 | ||||
-rw-r--r-- | ext/timers/README.md | 5 | ||||
-rw-r--r-- | ext/timers/benches/timers_ops.rs | 40 | ||||
-rw-r--r-- | ext/timers/lib.rs | 193 |
6 files changed, 1430 insertions, 0 deletions
diff --git a/ext/timers/01_timers.js b/ext/timers/01_timers.js new file mode 100644 index 000000000..a00d5d9b9 --- /dev/null +++ b/ext/timers/01_timers.js @@ -0,0 +1,595 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +"use strict"; + +((window) => { + const core = window.Deno.core; + const { + ArrayPrototypeIndexOf, + ArrayPrototypePush, + ArrayPrototypeShift, + ArrayPrototypeSplice, + DateNow, + Error, + FunctionPrototypeBind, + Map, + MapPrototypeDelete, + MapPrototypeGet, + MapPrototypeHas, + MapPrototypeSet, + MathMax, + Number, + String, + TypeError, + } = window.__bootstrap.primordials; + + // Shamelessly cribbed from extensions/fetch/11_streams.js + class AssertionError extends Error { + constructor(msg) { + super(msg); + this.name = "AssertionError"; + } + } + + /** + * @param {unknown} cond + * @param {string=} msg + * @returns {asserts cond} + */ + function assert(cond, msg = "Assertion failed.") { + if (!cond) { + throw new AssertionError(msg); + } + } + + function opStopGlobalTimer() { + core.opSync("op_global_timer_stop"); + } + + function opStartGlobalTimer(timeout) { + return core.opSync("op_global_timer_start", timeout); + } + + async function opWaitGlobalTimer() { + await core.opAsync("op_global_timer"); + } + + function opNow() { + return core.opSync("op_now"); + } + + function sleepSync(millis = 0) { + return core.opSync("op_sleep_sync", millis); + } + + // Derived from https://github.com/vadimg/js_bintrees. MIT Licensed. + + class RBNode { + constructor(data) { + this.data = data; + this.left = null; + this.right = null; + this.red = true; + } + + getChild(dir) { + return dir ? this.right : this.left; + } + + setChild(dir, val) { + if (dir) { + this.right = val; + } else { + this.left = val; + } + } + } + + class RBTree { + #comparator = null; + #root = null; + + constructor(comparator) { + this.#comparator = comparator; + this.#root = null; + } + + /** Returns `null` if tree is empty. */ + min() { + 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) { + 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) { + let ret = false; + + if (this.#root === null) { + // empty tree + this.#root = new RBNode(data); + ret = true; + } else { + const head = new RBNode(null); // fake tree root + + let dir = 0; + let last = 0; + + // setup + let gp = null; // grandparent + let ggp = head; // grand-grand-parent + let p = null; // parent + let node = 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; + + assert(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) { + if (this.#root === null) { + return false; + } + + const head = new RBNode(null); // 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 = 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 { + assert(gp); + 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); + assert(gpc); + gpc.red = true; + node.red = true; + assert(gpc.left); + gpc.left.red = false; + assert(gpc.right); + gpc.right.red = false; + } + } + } + } + } + + // replace and remove if found + if (found !== null) { + found.data = node.data; + assert(p); + 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(node) { + return node !== null && node.red; + } + + function singleRotate(root, dir) { + const save = root.getChild(!dir); + assert(save); + + root.setChild(!dir, save.getChild(dir)); + save.setChild(dir, root); + + root.red = true; + save.red = false; + + return save; + } + + function doubleRotate(root, dir) { + root.setChild(!dir, singleRotate(root.getChild(!dir), !dir)); + return singleRotate(root, dir); + } + + const { console } = globalThis; + + // Timeout values > TIMEOUT_MAX are set to 1. + const TIMEOUT_MAX = 2 ** 31 - 1; + + let globalTimeoutDue = null; + + let nextTimerId = 1; + const idMap = new Map(); + const dueTree = new RBTree((a, b) => a.due - b.due); + + function clearGlobalTimeout() { + globalTimeoutDue = null; + opStopGlobalTimer(); + } + + let pendingEvents = 0; + const pendingFireTimers = []; + + /** Process and run a single ready timer macrotask. + * This function should be registered through Deno.core.setMacrotaskCallback. + * Returns true when all ready macrotasks have been processed, false if more + * ready ones are available. The Isolate future would rely on the return value + * to repeatedly invoke this function until depletion. Multiple invocations + * of this function one at a time ensures newly ready microtasks are processed + * before next macrotask timer callback is invoked. */ + function handleTimerMacrotask() { + if (pendingFireTimers.length > 0) { + fire(ArrayPrototypeShift(pendingFireTimers)); + return pendingFireTimers.length === 0; + } + return true; + } + + async function setGlobalTimeout(due, now) { + // 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. + opStartGlobalTimer(timeout); + await opWaitGlobalTimer(); + pendingEvents--; + prepareReadyTimers(); + } + + function prepareReadyTimers() { + const now = DateNow(); + // 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; + 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. + ArrayPrototypePush(pendingFireTimers, timer); + } + } + setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now); + } + + function setOrClearGlobalTimeout(due, now) { + if (due == null) { + clearGlobalTimeout(); + } else { + setGlobalTimeout(due, now); + } + } + + function schedule(timer, now) { + assert(!timer.scheduled); + assert(now <= timer.due); + // 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. + ArrayPrototypePush(dueNode.timers, 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) { + // 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 = ArrayPrototypeIndexOf(pendingFireTimers, timer)) >= 0) { + ArrayPrototypeSplice(pendingFireTimers, 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 = dueTree.min(); + setOrClearGlobalTimeout( + nextDueNode && nextDueNode.due, + DateNow(), + ); + } + } else { + // Multiple timers that are due at the same point in time. + // Remove this timer from the list. + const index = ArrayPrototypeIndexOf(list, timer); + assert(index > -1); + ArrayPrototypeSplice(list, index, 1); + } + } + + function fire(timer) { + // 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 (!MapPrototypeHas(idMap, 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. + MapPrototypeDelete(idMap, 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 = DateNow(); + timer.due = MathMax(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; + if ("function" === typeof callback) { + callback(); + } else { + (0, eval)(callback); + } + } + + function checkThis(thisArg) { + if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) { + throw new TypeError("Illegal invocation"); + } + } + + function setTimer( + cb, + delay, + args, + repeat, + ) { + // If the callack is a function, bind `args` to the callback and bind `this` to globalThis(global). + // otherwise call `String` on it, and `eval` it on calls; do not pass variardic args to the string + let callback; + + if ("function" === typeof cb) { + callback = FunctionPrototypeBind(cb, globalThis, ...args); + } else { + callback = String(cb); + args = []; // args are ignored + } + // 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 = DateNow(); + 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 = MathMax(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. + MapPrototypeSet(idMap, timer.id, timer); + // Schedule the timer in the due table. + schedule(timer, now); + return timer.id; + } + + function setTimeout( + cb, + delay = 0, + ...args + ) { + delay >>>= 0; + checkThis(this); + return setTimer(cb, delay, args, false); + } + + function setInterval( + cb, + delay = 0, + ...args + ) { + delay >>>= 0; + checkThis(this); + return setTimer(cb, delay, args, true); + } + + function clearTimer(id) { + id >>>= 0; + const timer = MapPrototypeGet(idMap, 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); + MapPrototypeDelete(idMap, timer.id); + } + + function clearTimeout(id = 0) { + id >>>= 0; + if (id === 0) { + return; + } + clearTimer(id); + } + + function clearInterval(id = 0) { + id >>>= 0; + if (id === 0) { + return; + } + clearTimer(id); + } + + window.__bootstrap.timers = { + clearInterval, + setInterval, + clearTimeout, + setTimeout, + handleTimerMacrotask, + opStopGlobalTimer, + opStartGlobalTimer, + opNow, + sleepSync, + }; +})(this); diff --git a/ext/timers/02_performance.js b/ext/timers/02_performance.js new file mode 100644 index 000000000..f752ba933 --- /dev/null +++ b/ext/timers/02_performance.js @@ -0,0 +1,569 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +"use strict"; + +((window) => { + const { + ArrayPrototypeFilter, + ArrayPrototypeFind, + ArrayPrototypePush, + ArrayPrototypeReverse, + ArrayPrototypeSlice, + ObjectKeys, + Symbol, + SymbolFor, + SymbolToStringTag, + TypeError, + } = window.__bootstrap.primordials; + + const { webidl, structuredClone } = window.__bootstrap; + const consoleInternal = window.__bootstrap.console; + const { opNow } = window.__bootstrap.timers; + const { DOMException } = window.__bootstrap.domException; + + const illegalConstructorKey = Symbol("illegalConstructorKey"); + const customInspect = SymbolFor("Deno.customInspect"); + let performanceEntries = []; + + webidl.converters["PerformanceMarkOptions"] = webidl + .createDictionaryConverter( + "PerformanceMarkOptions", + [ + { + key: "detail", + converter: webidl.converters.any, + }, + { + key: "startTime", + converter: webidl.converters.DOMHighResTimeStamp, + }, + ], + ); + + webidl.converters["DOMString or DOMHighResTimeStamp"] = (V, opts) => { + if (webidl.type(V) === "Number" && V !== null) { + return webidl.converters.DOMHighResTimeStamp(V, opts); + } + return webidl.converters.DOMString(V, opts); + }; + + webidl.converters["PerformanceMeasureOptions"] = webidl + .createDictionaryConverter( + "PerformanceMeasureOptions", + [ + { + key: "detail", + converter: webidl.converters.any, + }, + { + key: "start", + converter: webidl.converters["DOMString or DOMHighResTimeStamp"], + }, + { + key: "duration", + converter: webidl.converters.DOMHighResTimeStamp, + }, + { + key: "end", + converter: webidl.converters["DOMString or DOMHighResTimeStamp"], + }, + ], + ); + + webidl.converters["DOMString or PerformanceMeasureOptions"] = (V, opts) => { + if (webidl.type(V) === "Object" && V !== null) { + return webidl.converters["PerformanceMeasureOptions"](V, opts); + } + return webidl.converters.DOMString(V, opts); + }; + + function findMostRecent( + name, + type, + ) { + return ArrayPrototypeFind( + ArrayPrototypeReverse(ArrayPrototypeSlice(performanceEntries)), + (entry) => entry.name === name && entry.entryType === type, + ); + } + + function convertMarkToTimestamp(mark) { + if (typeof mark === "string") { + const entry = findMostRecent(mark, "mark"); + if (!entry) { + throw new DOMException( + `Cannot find mark: "${mark}".`, + "SyntaxError", + ); + } + return entry.startTime; + } + if (mark < 0) { + throw new TypeError("Mark cannot be negative."); + } + return mark; + } + + function filterByNameType( + name, + type, + ) { + return ArrayPrototypeFilter( + performanceEntries, + (entry) => + (name ? entry.name === name : true) && + (type ? entry.entryType === type : true), + ); + } + + const now = opNow; + + const _name = Symbol("[[name]]"); + const _entryType = Symbol("[[entryType]]"); + const _startTime = Symbol("[[startTime]]"); + const _duration = Symbol("[[duration]]"); + class PerformanceEntry { + [_name] = ""; + [_entryType] = ""; + [_startTime] = 0; + [_duration] = 0; + + get name() { + webidl.assertBranded(this, PerformanceEntry); + return this[_name]; + } + + get entryType() { + webidl.assertBranded(this, PerformanceEntry); + return this[_entryType]; + } + + get startTime() { + webidl.assertBranded(this, PerformanceEntry); + return this[_startTime]; + } + + get duration() { + webidl.assertBranded(this, PerformanceEntry); + return this[_duration]; + } + + constructor( + name = null, + entryType = null, + startTime = null, + duration = null, + key = undefined, + ) { + if (key !== illegalConstructorKey) { + webidl.illegalConstructor(); + } + this[webidl.brand] = webidl.brand; + + this[_name] = name; + this[_entryType] = entryType; + this[_startTime] = startTime; + this[_duration] = duration; + } + + toJSON() { + webidl.assertBranded(this, PerformanceEntry); + return { + name: this[_name], + entryType: this[_entryType], + startTime: this[_startTime], + duration: this[_duration], + }; + } + + [customInspect](inspect) { + return inspect(consoleInternal.createFilteredInspectProxy({ + object: this, + evaluate: this instanceof PerformanceEntry, + keys: [ + "name", + "entryType", + "startTime", + "duration", + ], + })); + } + } + webidl.configurePrototype(PerformanceEntry); + + const _detail = Symbol("[[detail]]"); + class PerformanceMark extends PerformanceEntry { + [SymbolToStringTag] = "PerformanceMark"; + + [_detail] = null; + + get detail() { + webidl.assertBranded(this, PerformanceMark); + return this[_detail]; + } + + get entryType() { + webidl.assertBranded(this, PerformanceMark); + return "mark"; + } + + constructor( + name, + options = {}, + ) { + const prefix = "Failed to construct 'PerformanceMark'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + + name = webidl.converters.DOMString(name, { + prefix, + context: "Argument 1", + }); + + options = webidl.converters.PerformanceMarkOptions(options, { + prefix, + context: "Argument 2", + }); + + const { detail = null, startTime = now() } = options; + + super(name, "mark", startTime, 0, illegalConstructorKey); + this[webidl.brand] = webidl.brand; + if (startTime < 0) { + throw new TypeError("startTime cannot be negative"); + } + this[_detail] = structuredClone(detail); + } + + toJSON() { + webidl.assertBranded(this, PerformanceMark); + return { + name: this.name, + entryType: this.entryType, + startTime: this.startTime, + duration: this.duration, + detail: this.detail, + }; + } + + [customInspect](inspect) { + return inspect(consoleInternal.createFilteredInspectProxy({ + object: this, + evaluate: this instanceof PerformanceMark, + keys: [ + "name", + "entryType", + "startTime", + "duration", + "detail", + ], + })); + } + } + webidl.configurePrototype(PerformanceMark); + + class PerformanceMeasure extends PerformanceEntry { + [SymbolToStringTag] = "PerformanceMeasure"; + + [_detail] = null; + + get detail() { + webidl.assertBranded(this, PerformanceMeasure); + return this[_detail]; + } + + get entryType() { + webidl.assertBranded(this, PerformanceMeasure); + return "measure"; + } + + constructor( + name = null, + startTime = null, + duration = null, + detail = null, + key = undefined, + ) { + if (key !== illegalConstructorKey) { + webidl.illegalConstructor(); + } + + super(name, "measure", startTime, duration, key); + this[webidl.brand] = webidl.brand; + this[_detail] = structuredClone(detail); + } + + toJSON() { + webidl.assertBranded(this, PerformanceMeasure); + return { + name: this.name, + entryType: this.entryType, + startTime: this.startTime, + duration: this.duration, + detail: this.detail, + }; + } + + [customInspect](inspect) { + return inspect(consoleInternal.createFilteredInspectProxy({ + object: this, + evaluate: this instanceof PerformanceMeasure, + keys: [ + "name", + "entryType", + "startTime", + "duration", + "detail", + ], + })); + } + } + webidl.configurePrototype(PerformanceMeasure); + + class Performance { + constructor() { + webidl.illegalConstructor(); + } + + clearMarks(markName = undefined) { + webidl.assertBranded(this, Performance); + if (markName !== undefined) { + markName = webidl.converters.DOMString(markName, { + prefix: "Failed to execute 'clearMarks' on 'Performance'", + context: "Argument 1", + }); + + performanceEntries = ArrayPrototypeFilter( + performanceEntries, + (entry) => !(entry.name === markName && entry.entryType === "mark"), + ); + } else { + performanceEntries = ArrayPrototypeFilter( + performanceEntries, + (entry) => entry.entryType !== "mark", + ); + } + } + + clearMeasures(measureName = undefined) { + webidl.assertBranded(this, Performance); + if (measureName !== undefined) { + measureName = webidl.converters.DOMString(measureName, { + prefix: "Failed to execute 'clearMeasures' on 'Performance'", + context: "Argument 1", + }); + + performanceEntries = ArrayPrototypeFilter( + performanceEntries, + (entry) => + !(entry.name === measureName && entry.entryType === "measure"), + ); + } else { + performanceEntries = ArrayPrototypeFilter( + performanceEntries, + (entry) => entry.entryType !== "measure", + ); + } + } + + getEntries() { + webidl.assertBranded(this, Performance); + return filterByNameType(); + } + + getEntriesByName( + name, + type = undefined, + ) { + webidl.assertBranded(this, Performance); + const prefix = "Failed to execute 'getEntriesByName' on 'Performance'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + + name = webidl.converters.DOMString(name, { + prefix, + context: "Argument 1", + }); + + if (type !== undefined) { + type = webidl.converters.DOMString(type, { + prefix, + context: "Argument 2", + }); + } + + return filterByNameType(name, type); + } + + getEntriesByType(type) { + webidl.assertBranded(this, Performance); + const prefix = "Failed to execute 'getEntriesByName' on 'Performance'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + + type = webidl.converters.DOMString(type, { + prefix, + context: "Argument 1", + }); + + return filterByNameType(undefined, type); + } + + mark( + markName, + markOptions = {}, + ) { + webidl.assertBranded(this, Performance); + const prefix = "Failed to execute 'mark' on 'Performance'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + + markName = webidl.converters.DOMString(markName, { + prefix, + context: "Argument 1", + }); + + markOptions = webidl.converters.PerformanceMarkOptions(markOptions, { + prefix, + context: "Argument 2", + }); + + // 3.1.1.1 If the global object is a Window object and markName uses the + // same name as a read only attribute in the PerformanceTiming interface, + // throw a SyntaxError. - not implemented + const entry = new PerformanceMark(markName, markOptions); + // 3.1.1.7 Queue entry - not implemented + ArrayPrototypePush(performanceEntries, entry); + return entry; + } + + measure( + measureName, + startOrMeasureOptions = {}, + endMark = undefined, + ) { + webidl.assertBranded(this, Performance); + const prefix = "Failed to execute 'measure' on 'Performance'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + + measureName = webidl.converters.DOMString(measureName, { + prefix, + context: "Argument 1", + }); + + startOrMeasureOptions = webidl.converters + ["DOMString or PerformanceMeasureOptions"](startOrMeasureOptions, { + prefix, + context: "Argument 2", + }); + + if (endMark !== undefined) { + endMark = webidl.converters.DOMString(endMark, { + prefix, + context: "Argument 3", + }); + } + + if ( + startOrMeasureOptions && typeof startOrMeasureOptions === "object" && + ObjectKeys(startOrMeasureOptions).length > 0 + ) { + if (endMark) { + throw new TypeError("Options cannot be passed with endMark."); + } + if ( + !("start" in startOrMeasureOptions) && + !("end" in startOrMeasureOptions) + ) { + throw new TypeError( + "A start or end mark must be supplied in options.", + ); + } + if ( + "start" in startOrMeasureOptions && + "duration" in startOrMeasureOptions && + "end" in startOrMeasureOptions + ) { + throw new TypeError( + "Cannot specify start, end, and duration together in options.", + ); + } + } + let endTime; + if (endMark) { + endTime = convertMarkToTimestamp(endMark); + } else if ( + typeof startOrMeasureOptions === "object" && + "end" in startOrMeasureOptions + ) { + endTime = convertMarkToTimestamp(startOrMeasureOptions.end); + } else if ( + typeof startOrMeasureOptions === "object" && + "start" in startOrMeasureOptions && + "duration" in startOrMeasureOptions + ) { + const start = convertMarkToTimestamp(startOrMeasureOptions.start); + const duration = convertMarkToTimestamp(startOrMeasureOptions.duration); + endTime = start + duration; + } else { + endTime = now(); + } + let startTime; + if ( + typeof startOrMeasureOptions === "object" && + "start" in startOrMeasureOptions + ) { + startTime = convertMarkToTimestamp(startOrMeasureOptions.start); + } else if ( + typeof startOrMeasureOptions === "object" && + "end" in startOrMeasureOptions && + "duration" in startOrMeasureOptions + ) { + const end = convertMarkToTimestamp(startOrMeasureOptions.end); + const duration = convertMarkToTimestamp(startOrMeasureOptions.duration); + startTime = end - duration; + } else if (typeof startOrMeasureOptions === "string") { + startTime = convertMarkToTimestamp(startOrMeasureOptions); + } else { + startTime = 0; + } + const entry = new PerformanceMeasure( + measureName, + startTime, + endTime - startTime, + typeof startOrMeasureOptions === "object" + ? startOrMeasureOptions.detail ?? null + : null, + illegalConstructorKey, + ); + ArrayPrototypePush(performanceEntries, entry); + return entry; + } + + now() { + webidl.assertBranded(this, Performance); + return now(); + } + + toJSON() { + webidl.assertBranded(this, Performance); + return {}; + } + + [customInspect](inspect) { + return inspect(consoleInternal.createFilteredInspectProxy({ + object: this, + evaluate: this instanceof Performance, + keys: [], + })); + } + + get [SymbolToStringTag]() { + return "Performance"; + } + } + webidl.configurePrototype(Performance); + + window.__bootstrap.performance = { + PerformanceEntry, + PerformanceMark, + PerformanceMeasure, + Performance, + performance: webidl.createBranded(Performance), + }; +})(this); diff --git a/ext/timers/Cargo.toml b/ext/timers/Cargo.toml new file mode 100644 index 000000000..eeaef5749 --- /dev/null +++ b/ext/timers/Cargo.toml @@ -0,0 +1,28 @@ +# Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_timers" +version = "0.12.0" +authors = ["the Deno authors"] +edition = "2018" +license = "MIT" +readme = "README.md" +repository = "https://github.com/denoland/deno" +description = "Timers API implementation for Deno" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core = { version = "0.96.0", path = "../../core" } +tokio = { version = "1.8.1", features = ["full"] } + +[dev-dependencies] +deno_bench_util = { version = "0.8.0", path = "../../bench_util" } +deno_url = { version = "0.14.0", path = "../url" } +deno_web = { version = "0.45.0", path = "../web" } +deno_webidl = { version = "0.14.0", path = "../webidl" } + +[[bench]] +name = "timers_ops" +harness = false diff --git a/ext/timers/README.md b/ext/timers/README.md new file mode 100644 index 000000000..5a2a8e516 --- /dev/null +++ b/ext/timers/README.md @@ -0,0 +1,5 @@ +# deno_timers + +This crate implements the timers API. + +Spec: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers diff --git a/ext/timers/benches/timers_ops.rs b/ext/timers/benches/timers_ops.rs new file mode 100644 index 000000000..269d9627d --- /dev/null +++ b/ext/timers/benches/timers_ops.rs @@ -0,0 +1,40 @@ +use deno_core::Extension; + +use deno_bench_util::bench_or_profile; +use deno_bench_util::bencher::{benchmark_group, Bencher}; +use deno_bench_util::{bench_js_async, bench_js_sync}; +use deno_web::BlobStore; + +fn setup() -> Vec<Extension> { + vec![ + deno_webidl::init(), + deno_url::init(), + deno_web::init(BlobStore::default(), None), + deno_timers::init::<deno_timers::NoTimersPermission>(), + Extension::builder() + .js(vec![ + ("setup", + Box::new(|| Ok(r#" + const { opNow, setTimeout, handleTimerMacrotask } = globalThis.__bootstrap.timers; + Deno.core.setMacrotaskCallback(handleTimerMacrotask); + "#.to_owned())), + ), + ]) + .state(|state| { + state.put(deno_timers::NoTimersPermission{}); + Ok(()) + }) + .build() + ] +} + +fn bench_op_now(b: &mut Bencher) { + bench_js_sync(b, r#"opNow();"#, setup); +} + +fn bench_set_timeout(b: &mut Bencher) { + bench_js_async(b, r#"setTimeout(() => {}, 0);"#, setup); +} + +benchmark_group!(benches, bench_op_now, bench_set_timeout,); +bench_or_profile!(benches); diff --git a/ext/timers/lib.rs b/ext/timers/lib.rs new file mode 100644 index 000000000..2b9948d1f --- /dev/null +++ b/ext/timers/lib.rs @@ -0,0 +1,193 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +//! This module helps deno implement timers. +//! +//! As an optimization, we want to avoid an expensive calls into rust for every +//! setTimeout in JavaScript. Thus in //js/timers.ts a data structure is +//! implemented that calls into Rust for only the smallest timeout. Thus we +//! only need to be able to start, cancel and await a single timer (or Delay, as Tokio +//! calls it) for an entire Isolate. This is what is implemented here. + +use deno_core::error::AnyError; +use deno_core::futures; +use deno_core::futures::channel::oneshot; +use deno_core::futures::FutureExt; +use deno_core::futures::TryFutureExt; +use deno_core::include_js_files; +use deno_core::op_async; +use deno_core::op_sync; +use deno_core::Extension; +use deno_core::OpState; +use std::cell::RefCell; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; +use std::thread::sleep; +use std::time::Duration; +use std::time::Instant; + +pub trait TimersPermission { + fn allow_hrtime(&mut self) -> bool; + fn check_unstable(&self, state: &OpState, api_name: &'static str); +} + +pub struct NoTimersPermission; + +impl TimersPermission for NoTimersPermission { + fn allow_hrtime(&mut self) -> bool { + false + } + fn check_unstable(&self, _: &OpState, _: &'static str) {} +} + +pub fn init<P: TimersPermission + 'static>() -> Extension { + Extension::builder() + .js(include_js_files!( + prefix "deno:ext/timers", + "01_timers.js", + "02_performance.js", + )) + .ops(vec![ + ("op_global_timer_stop", op_sync(op_global_timer_stop)), + ("op_global_timer_start", op_sync(op_global_timer_start)), + ("op_global_timer", op_async(op_global_timer)), + ("op_now", op_sync(op_now::<P>)), + ("op_sleep_sync", op_sync(op_sleep_sync::<P>)), + ]) + .state(|state| { + state.put(GlobalTimer::default()); + state.put(StartTime::now()); + Ok(()) + }) + .build() +} + +pub type StartTime = Instant; + +type TimerFuture = Pin<Box<dyn Future<Output = Result<(), ()>>>>; + +#[derive(Default)] +pub struct GlobalTimer { + tx: Option<oneshot::Sender<()>>, + pub future: Option<TimerFuture>, +} + +impl GlobalTimer { + pub fn cancel(&mut self) { + if let Some(tx) = self.tx.take() { + tx.send(()).ok(); + } + } + + pub fn new_timeout(&mut self, deadline: Instant) { + if self.tx.is_some() { + self.cancel(); + } + assert!(self.tx.is_none()); + self.future.take(); + + let (tx, rx) = oneshot::channel(); + self.tx = Some(tx); + + let delay = tokio::time::sleep_until(deadline.into()).boxed_local(); + let rx = rx + .map_err(|err| panic!("Unexpected error in receiving channel {:?}", err)); + + let fut = futures::future::select(delay, rx) + .then(|_| futures::future::ok(())) + .boxed_local(); + self.future = Some(fut); + } +} + +pub fn op_global_timer_stop( + state: &mut OpState, + _args: (), + _: (), +) -> Result<(), AnyError> { + let global_timer = state.borrow_mut::<GlobalTimer>(); + global_timer.cancel(); + Ok(()) +} + +// Set up a timer that will be later awaited by JS promise. +// It's a separate op, because canceling a timeout immediately +// after setting it caused a race condition (because Tokio timeout) +// might have been registered after next event loop tick. +// +// See https://github.com/denoland/deno/issues/7599 for more +// details. +pub fn op_global_timer_start( + state: &mut OpState, + timeout: u64, + _: (), +) -> Result<(), AnyError> { + // According to spec, minimum allowed timeout is 4 ms. + // https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers + // TODO(#10974) Per spec this is actually a little more complicated than this. + // The minimum timeout depends on the nesting level of the timeout. + let timeout = std::cmp::max(timeout, 4); + + let deadline = Instant::now() + Duration::from_millis(timeout); + let global_timer = state.borrow_mut::<GlobalTimer>(); + global_timer.new_timeout(deadline); + Ok(()) +} + +pub async fn op_global_timer( + state: Rc<RefCell<OpState>>, + _args: (), + _: (), +) -> Result<(), AnyError> { + let maybe_timer_fut = { + let mut s = state.borrow_mut(); + let global_timer = s.borrow_mut::<GlobalTimer>(); + global_timer.future.take() + }; + if let Some(timer_fut) = maybe_timer_fut { + let _ = timer_fut.await; + } + Ok(()) +} + +// Returns a milliseconds and nanoseconds subsec +// since the start time of the deno runtime. +// If the High precision flag is not set, the +// nanoseconds are rounded on 2ms. +pub fn op_now<TP>( + state: &mut OpState, + _argument: (), + _: (), +) -> Result<f64, AnyError> +where + TP: TimersPermission + 'static, +{ + let start_time = state.borrow::<StartTime>(); + let seconds = start_time.elapsed().as_secs(); + let mut subsec_nanos = start_time.elapsed().subsec_nanos() as f64; + let reduced_time_precision = 2_000_000.0; // 2ms in nanoseconds + + // If the permission is not enabled + // Round the nano result on 2 milliseconds + // see: https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#Reduced_time_precision + if !state.borrow_mut::<TP>().allow_hrtime() { + subsec_nanos -= subsec_nanos % reduced_time_precision; + } + + let result = (seconds * 1_000) as f64 + (subsec_nanos / 1_000_000.0); + + Ok(result) +} + +pub fn op_sleep_sync<TP>( + state: &mut OpState, + millis: u64, + _: (), +) -> Result<(), AnyError> +where + TP: TimersPermission + 'static, +{ + state.borrow::<TP>().check_unstable(state, "Deno.sleepSync"); + sleep(Duration::from_millis(millis)); + Ok(()) +} |