summaryrefslogtreecommitdiff
path: root/ext/timers
diff options
context:
space:
mode:
authorRyan Dahl <ry@tinyclouds.org>2021-08-11 12:27:05 +0200
committerGitHub <noreply@github.com>2021-08-11 12:27:05 +0200
commita0285e2eb88f6254f6494b0ecd1878db3a3b2a58 (patch)
tree90671b004537e20f9493fd3277ffd21d30b39a0e /ext/timers
parent3a6994115176781b3a93d70794b1b81bc95e42b4 (diff)
Rename extensions/ directory to ext/ (#11643)
Diffstat (limited to 'ext/timers')
-rw-r--r--ext/timers/01_timers.js595
-rw-r--r--ext/timers/02_performance.js569
-rw-r--r--ext/timers/Cargo.toml28
-rw-r--r--ext/timers/README.md5
-rw-r--r--ext/timers/benches/timers_ops.rs40
-rw-r--r--ext/timers/lib.rs193
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(())
+}