summaryrefslogtreecommitdiff
path: root/ext/web
diff options
context:
space:
mode:
authorAndreu Botella <andreu@andreubotella.com>2022-02-15 12:17:30 +0100
committerGitHub <noreply@github.com>2022-02-15 12:17:30 +0100
commit760f4c9e2427e87815a8e59b0807693c8dcb623a (patch)
treecb04b2b42cfcf1fc46d74a3e4c7f86c73491d2b0 /ext/web
parent5e845442fade02cd12d13e74222b26e217c5971d (diff)
chore(ext/timers): move ext/timers to ext/web (#13665)
Diffstat (limited to 'ext/web')
-rw-r--r--ext/web/02_timers.js394
-rw-r--r--ext/web/15_performance.js569
-rw-r--r--ext/web/Cargo.toml9
-rw-r--r--ext/web/benches/timers_ops.rs53
-rw-r--r--ext/web/lib.rs20
-rw-r--r--ext/web/timers.rs103
6 files changed, 1147 insertions, 1 deletions
diff --git a/ext/web/02_timers.js b/ext/web/02_timers.js
new file mode 100644
index 000000000..a0b1deb45
--- /dev/null
+++ b/ext/web/02_timers.js
@@ -0,0 +1,394 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+"use strict";
+
+((window) => {
+ const core = window.Deno.core;
+ const {
+ ArrayPrototypePush,
+ ArrayPrototypeShift,
+ Error,
+ FunctionPrototypeCall,
+ Map,
+ MapPrototypeDelete,
+ MapPrototypeGet,
+ MapPrototypeHas,
+ MapPrototypeSet,
+ // deno-lint-ignore camelcase
+ NumberPOSITIVE_INFINITY,
+ PromisePrototypeThen,
+ ObjectPrototypeIsPrototypeOf,
+ SafeArrayIterator,
+ SymbolFor,
+ TypeError,
+ } = window.__bootstrap.primordials;
+ const { webidl } = window.__bootstrap;
+
+ // 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 opNow() {
+ return core.opSync("op_now");
+ }
+
+ function sleepSync(millis = 0) {
+ return core.opSync("op_sleep_sync", millis);
+ }
+
+ // ---------------------------------------------------------------------------
+
+ /**
+ * The task queue corresponding to the timer task source.
+ *
+ * @type { {action: () => void, nestingLevel: number}[] }
+ */
+ const timerTasks = [];
+
+ /**
+ * The current task's timer nesting level, or zero if we're not currently
+ * running a timer task (since the minimum nesting level is 1).
+ *
+ * @type {number}
+ */
+ let timerNestingLevel = 0;
+
+ function handleTimerMacrotask() {
+ if (timerTasks.length === 0) {
+ return true;
+ }
+
+ const task = ArrayPrototypeShift(timerTasks);
+
+ timerNestingLevel = task.nestingLevel;
+
+ try {
+ task.action();
+ } finally {
+ timerNestingLevel = 0;
+ }
+ return timerTasks.length === 0;
+ }
+
+ // ---------------------------------------------------------------------------
+
+ /**
+ * The keys in this map correspond to the key ID's in the spec's map of active
+ * timers. The values are the timeout's cancel rid.
+ *
+ * @type {Map<number, { cancelRid: number, isRef: boolean, promiseId: number }>}
+ */
+ const activeTimers = new Map();
+
+ let nextId = 1;
+
+ /**
+ * @param {Function | string} callback
+ * @param {number} timeout
+ * @param {Array<any>} args
+ * @param {boolean} repeat
+ * @param {number | undefined} prevId
+ * @returns {number} The timer ID
+ */
+ function initializeTimer(
+ callback,
+ timeout,
+ args,
+ repeat,
+ prevId,
+ ) {
+ // 2. If previousId was given, let id be previousId; otherwise, let
+ // previousId be an implementation-defined integer than is greater than zero
+ // and does not already exist in global's map of active timers.
+ let id;
+ let timerInfo;
+ if (prevId !== undefined) {
+ // `prevId` is only passed for follow-up calls on intervals
+ assert(repeat);
+ id = prevId;
+ timerInfo = MapPrototypeGet(activeTimers, id);
+ } else {
+ // TODO(@andreubotella): Deal with overflow.
+ // https://github.com/whatwg/html/issues/7358
+ id = nextId++;
+ const cancelRid = core.opSync("op_timer_handle");
+ timerInfo = { cancelRid, isRef: true, promiseId: -1 };
+
+ // Step 4 in "run steps after a timeout".
+ MapPrototypeSet(activeTimers, id, timerInfo);
+ }
+
+ // 3. If the surrounding agent's event loop's currently running task is a
+ // task that was created by this algorithm, then let nesting level be the
+ // task's timer nesting level. Otherwise, let nesting level be zero.
+ // 4. If timeout is less than 0, then set timeout to 0.
+ // 5. If nesting level is greater than 5, and timeout is less than 4, then
+ // set timeout to 4.
+ //
+ // The nesting level of 5 and minimum of 4 ms are spec-mandated magic
+ // constants.
+ if (timeout < 0) timeout = 0;
+ if (timerNestingLevel > 5 && timeout < 4) timeout = 4;
+
+ // 9. Let task be a task that runs the following steps:
+ const task = {
+ action: () => {
+ // 1. If id does not exist in global's map of active timers, then abort
+ // these steps.
+ //
+ // This is relevant if the timer has been canceled after the sleep op
+ // resolves but before this task runs.
+ if (!MapPrototypeHas(activeTimers, id)) {
+ return;
+ }
+
+ // 2.
+ // 3.
+ // TODO(@andreubotella): Error handling.
+ if (typeof callback === "function") {
+ FunctionPrototypeCall(
+ callback,
+ globalThis,
+ ...new SafeArrayIterator(args),
+ );
+ } else {
+ // TODO(@andreubotella): eval doesn't seem to have a primordial, but
+ // it can be redefined in the global scope.
+ (0, eval)(callback);
+ }
+
+ if (repeat) {
+ if (MapPrototypeHas(activeTimers, id)) {
+ // 4. If id does not exist in global's map of active timers, then
+ // abort these steps.
+ // NOTE: If might have been removed via the author code in handler
+ // calling clearTimeout() or clearInterval().
+ // 5. If repeat is true, then perform the timer initialization steps
+ // again, given global, handler, timeout, arguments, true, and id.
+ initializeTimer(callback, timeout, args, true, id);
+ }
+ } else {
+ // 6. Otherwise, remove global's map of active timers[id].
+ core.tryClose(timerInfo.cancelRid);
+ MapPrototypeDelete(activeTimers, id);
+ }
+ },
+
+ // 10. Increment nesting level by one.
+ // 11. Set task's timer nesting level to nesting level.
+ nestingLevel: timerNestingLevel + 1,
+ };
+
+ // 12. Let completionStep be an algorithm step which queues a global task on
+ // the timer task source given global to run task.
+ // 13. Run steps after a timeout given global, "setTimeout/setInterval",
+ // timeout, completionStep, and id.
+ runAfterTimeout(
+ () => ArrayPrototypePush(timerTasks, task),
+ timeout,
+ timerInfo,
+ );
+
+ return id;
+ }
+
+ // ---------------------------------------------------------------------------
+
+ /**
+ * @typedef ScheduledTimer
+ * @property {number} millis
+ * @property {() => void} cb
+ * @property {boolean} resolved
+ * @property {ScheduledTimer | null} prev
+ * @property {ScheduledTimer | null} next
+ */
+
+ /**
+ * A doubly linked list of timers.
+ * @type { { head: ScheduledTimer | null, tail: ScheduledTimer | null } }
+ */
+ const scheduledTimers = { head: null, tail: null };
+
+ /**
+ * @param {() => void} cb Will be run after the timeout, if it hasn't been
+ * cancelled.
+ * @param {number} millis
+ * @param {{ cancelRid: number, isRef: boolean, promiseId: number }} timerInfo
+ */
+ function runAfterTimeout(cb, millis, timerInfo) {
+ const cancelRid = timerInfo.cancelRid;
+ const sleepPromise = core.opAsync("op_sleep", millis, cancelRid);
+ timerInfo.promiseId =
+ sleepPromise[SymbolFor("Deno.core.internalPromiseId")];
+ if (!timerInfo.isRef) {
+ core.unrefOp(timerInfo.promiseId);
+ }
+
+ /** @type {ScheduledTimer} */
+ const timerObject = {
+ millis,
+ cb,
+ resolved: false,
+ prev: scheduledTimers.tail,
+ next: null,
+ };
+
+ // Add timerObject to the end of the list.
+ if (scheduledTimers.tail === null) {
+ assert(scheduledTimers.head === null);
+ scheduledTimers.head = scheduledTimers.tail = timerObject;
+ } else {
+ scheduledTimers.tail.next = timerObject;
+ scheduledTimers.tail = timerObject;
+ }
+
+ // 1.
+ PromisePrototypeThen(
+ sleepPromise,
+ () => {
+ // 2. Wait until any invocations of this algorithm that had the same
+ // global and orderingIdentifier, that started before this one, and
+ // whose milliseconds is equal to or less than this one's, have
+ // completed.
+ // 4. Perform completionSteps.
+
+ // IMPORTANT: Since the sleep ops aren't guaranteed to resolve in the
+ // right order, whenever one resolves, we run through the scheduled
+ // timers list (which is in the order in which they were scheduled), and
+ // we call the callback for every timer which both:
+ // a) has resolved, and
+ // b) its timeout is lower than the lowest unresolved timeout found so
+ // far in the list.
+
+ timerObject.resolved = true;
+
+ let lowestUnresolvedTimeout = NumberPOSITIVE_INFINITY;
+
+ let currentEntry = scheduledTimers.head;
+ while (currentEntry !== null) {
+ if (currentEntry.millis < lowestUnresolvedTimeout) {
+ if (currentEntry.resolved) {
+ currentEntry.cb();
+ removeFromScheduledTimers(currentEntry);
+ } else {
+ lowestUnresolvedTimeout = currentEntry.millis;
+ }
+ }
+
+ currentEntry = currentEntry.next;
+ }
+ },
+ (err) => {
+ if (ObjectPrototypeIsPrototypeOf(core.InterruptedPrototype, err)) {
+ // The timer was cancelled.
+ removeFromScheduledTimers(timerObject);
+ } else {
+ throw err;
+ }
+ },
+ );
+ }
+
+ /** @param {ScheduledTimer} timerObj */
+ function removeFromScheduledTimers(timerObj) {
+ if (timerObj.prev !== null) {
+ timerObj.prev.next = timerObj.next;
+ } else {
+ assert(scheduledTimers.head === timerObj);
+ scheduledTimers.head = timerObj.next;
+ }
+ if (timerObj.next !== null) {
+ timerObj.next.prev = timerObj.prev;
+ } else {
+ assert(scheduledTimers.tail === timerObj);
+ scheduledTimers.tail = timerObj.prev;
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+
+ function checkThis(thisArg) {
+ if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) {
+ throw new TypeError("Illegal invocation");
+ }
+ }
+
+ function setTimeout(callback, timeout = 0, ...args) {
+ checkThis(this);
+ if (typeof callback !== "function") {
+ callback = webidl.converters.DOMString(callback);
+ }
+ timeout = webidl.converters.long(timeout);
+
+ return initializeTimer(callback, timeout, args, false);
+ }
+
+ function setInterval(callback, timeout = 0, ...args) {
+ checkThis(this);
+ if (typeof callback !== "function") {
+ callback = webidl.converters.DOMString(callback);
+ }
+ timeout = webidl.converters.long(timeout);
+
+ return initializeTimer(callback, timeout, args, true);
+ }
+
+ function clearTimeout(id = 0) {
+ checkThis(this);
+ id = webidl.converters.long(id);
+ const timerInfo = MapPrototypeGet(activeTimers, id);
+ if (timerInfo !== undefined) {
+ core.tryClose(timerInfo.cancelRid);
+ MapPrototypeDelete(activeTimers, id);
+ }
+ }
+
+ function clearInterval(id = 0) {
+ checkThis(this);
+ clearTimeout(id);
+ }
+
+ function refTimer(id) {
+ const timerInfo = MapPrototypeGet(activeTimers, id);
+ if (timerInfo === undefined || timerInfo.isRef) {
+ return;
+ }
+ timerInfo.isRef = true;
+ core.refOp(timerInfo.promiseId);
+ }
+
+ function unrefTimer(id) {
+ const timerInfo = MapPrototypeGet(activeTimers, id);
+ if (timerInfo === undefined || !timerInfo.isRef) {
+ return;
+ }
+ timerInfo.isRef = false;
+ core.unrefOp(timerInfo.promiseId);
+ }
+
+ window.__bootstrap.timers = {
+ setTimeout,
+ setInterval,
+ clearTimeout,
+ clearInterval,
+ handleTimerMacrotask,
+ opNow,
+ sleepSync,
+ refTimer,
+ unrefTimer,
+ };
+})(this);
diff --git a/ext/web/15_performance.js b/ext/web/15_performance.js
new file mode 100644
index 000000000..c48a3d888
--- /dev/null
+++ b/ext/web/15_performance.js
@@ -0,0 +1,569 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+"use strict";
+
+((window) => {
+ const {
+ ArrayPrototypeFilter,
+ ArrayPrototypeFind,
+ ArrayPrototypePush,
+ ArrayPrototypeReverse,
+ ArrayPrototypeSlice,
+ ObjectKeys,
+ ObjectPrototypeIsPrototypeOf,
+ Symbol,
+ SymbolFor,
+ 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, PerformanceEntryPrototype);
+ return this[_name];
+ }
+
+ get entryType() {
+ webidl.assertBranded(this, PerformanceEntryPrototype);
+ return this[_entryType];
+ }
+
+ get startTime() {
+ webidl.assertBranded(this, PerformanceEntryPrototype);
+ return this[_startTime];
+ }
+
+ get duration() {
+ webidl.assertBranded(this, PerformanceEntryPrototype);
+ 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, PerformanceEntryPrototype);
+ return {
+ name: this[_name],
+ entryType: this[_entryType],
+ startTime: this[_startTime],
+ duration: this[_duration],
+ };
+ }
+
+ [customInspect](inspect) {
+ return inspect(consoleInternal.createFilteredInspectProxy({
+ object: this,
+ evaluate: ObjectPrototypeIsPrototypeOf(
+ PerformanceEntryPrototype,
+ this,
+ ),
+ keys: [
+ "name",
+ "entryType",
+ "startTime",
+ "duration",
+ ],
+ }));
+ }
+ }
+ webidl.configurePrototype(PerformanceEntry);
+ const PerformanceEntryPrototype = PerformanceEntry.prototype;
+
+ const _detail = Symbol("[[detail]]");
+ class PerformanceMark extends PerformanceEntry {
+ [_detail] = null;
+
+ get detail() {
+ webidl.assertBranded(this, PerformanceMarkPrototype);
+ return this[_detail];
+ }
+
+ get entryType() {
+ webidl.assertBranded(this, PerformanceMarkPrototype);
+ 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, PerformanceMarkPrototype);
+ 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: ObjectPrototypeIsPrototypeOf(PerformanceMarkPrototype, this),
+ keys: [
+ "name",
+ "entryType",
+ "startTime",
+ "duration",
+ "detail",
+ ],
+ }));
+ }
+ }
+ webidl.configurePrototype(PerformanceMark);
+ const PerformanceMarkPrototype = PerformanceMark.prototype;
+ class PerformanceMeasure extends PerformanceEntry {
+ [_detail] = null;
+
+ get detail() {
+ webidl.assertBranded(this, PerformanceMeasurePrototype);
+ return this[_detail];
+ }
+
+ get entryType() {
+ webidl.assertBranded(this, PerformanceMeasurePrototype);
+ 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, PerformanceMeasurePrototype);
+ 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: ObjectPrototypeIsPrototypeOf(
+ PerformanceMeasurePrototype,
+ this,
+ ),
+ keys: [
+ "name",
+ "entryType",
+ "startTime",
+ "duration",
+ "detail",
+ ],
+ }));
+ }
+ }
+ webidl.configurePrototype(PerformanceMeasure);
+ const PerformanceMeasurePrototype = PerformanceMeasure.prototype;
+ class Performance {
+ constructor() {
+ webidl.illegalConstructor();
+ }
+
+ clearMarks(markName = undefined) {
+ webidl.assertBranded(this, PerformancePrototype);
+ 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, PerformancePrototype);
+ 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, PerformancePrototype);
+ return filterByNameType();
+ }
+
+ getEntriesByName(
+ name,
+ type = undefined,
+ ) {
+ webidl.assertBranded(this, PerformancePrototype);
+ 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, PerformancePrototype);
+ 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, PerformancePrototype);
+ 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, PerformancePrototype);
+ 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, PerformancePrototype);
+ return now();
+ }
+
+ toJSON() {
+ webidl.assertBranded(this, PerformancePrototype);
+ return {};
+ }
+
+ [customInspect](inspect) {
+ return inspect(consoleInternal.createFilteredInspectProxy({
+ object: this,
+ evaluate: ObjectPrototypeIsPrototypeOf(PerformancePrototype, this),
+ keys: [],
+ }));
+ }
+ }
+ webidl.configurePrototype(Performance);
+ const PerformancePrototype = Performance.prototype;
+
+ window.__bootstrap.performance = {
+ PerformanceEntry,
+ PerformanceMark,
+ PerformanceMeasure,
+ Performance,
+ performance: webidl.createBranded(Performance),
+ };
+})(this);
diff --git a/ext/web/Cargo.toml b/ext/web/Cargo.toml
index 0e1c1433b..f32a05999 100644
--- a/ext/web/Cargo.toml
+++ b/ext/web/Cargo.toml
@@ -22,3 +22,12 @@ flate2 = "1"
serde = "1.0.129"
tokio = { version = "1.10.1", features = ["full"] }
uuid = { version = "0.8.2", features = ["v4", "serde"] }
+
+[dev-dependencies]
+deno_bench_util = { version = "0.30.0", path = "../../bench_util" }
+deno_url = { version = "0.36.0", path = "../url" }
+deno_webidl = { version = "0.36.0", path = "../webidl" }
+
+[[bench]]
+name = "timers_ops"
+harness = false
diff --git a/ext/web/benches/timers_ops.rs b/ext/web/benches/timers_ops.rs
new file mode 100644
index 000000000..30f50b7d9
--- /dev/null
+++ b/ext/web/benches/timers_ops.rs
@@ -0,0 +1,53 @@
+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;
+
+struct Permissions;
+
+impl deno_web::TimersPermission for Permissions {
+ fn allow_hrtime(&mut self) -> bool {
+ true
+ }
+ fn check_unstable(
+ &self,
+ _state: &deno_core::OpState,
+ _api_name: &'static str,
+ ) {
+ }
+}
+
+fn setup() -> Vec<Extension> {
+ vec![
+ deno_webidl::init(),
+ deno_url::init(),
+ deno_web::init::<Permissions>(BlobStore::default(), None),
+ 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(Permissions{});
+ 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/web/lib.rs b/ext/web/lib.rs
index b32deeb97..b10cb972d 100644
--- a/ext/web/lib.rs
+++ b/ext/web/lib.rs
@@ -3,6 +3,7 @@
mod blob;
mod compression;
mod message_port;
+mod timers;
use deno_core::error::range_error;
use deno_core::error::type_error;
@@ -47,8 +48,18 @@ use crate::message_port::op_message_port_recv_message;
pub use crate::message_port::JsMessageData;
pub use crate::message_port::MessagePort;
+use crate::timers::op_now;
+use crate::timers::op_sleep;
+use crate::timers::op_sleep_sync;
+use crate::timers::op_timer_handle;
+use crate::timers::StartTime;
+pub use crate::timers::TimersPermission;
+
/// Load and execute the javascript code.
-pub fn init(blob_store: BlobStore, maybe_location: Option<Url>) -> Extension {
+pub fn init<P: TimersPermission + 'static>(
+ blob_store: BlobStore,
+ maybe_location: Option<Url>,
+) -> Extension {
Extension::builder()
.js(include_js_files!(
prefix "deno:ext/web",
@@ -57,6 +68,7 @@ pub fn init(blob_store: BlobStore, maybe_location: Option<Url>) -> Extension {
"01_mimesniff.js",
"02_event.js",
"02_structured_clone.js",
+ "02_timers.js",
"03_abort_signal.js",
"04_global_interfaces.js",
"05_base64.js",
@@ -68,6 +80,7 @@ pub fn init(blob_store: BlobStore, maybe_location: Option<Url>) -> Extension {
"12_location.js",
"13_message_port.js",
"14_compression.js",
+ "15_performance.js",
))
.ops(vec![
("op_base64_decode", op_sync(op_base64_decode)),
@@ -116,12 +129,17 @@ pub fn init(blob_store: BlobStore, maybe_location: Option<Url>) -> Extension {
"op_compression_finish",
op_sync(compression::op_compression_finish),
),
+ ("op_now", op_sync(op_now::<P>)),
+ ("op_timer_handle", op_sync(op_timer_handle)),
+ ("op_sleep", op_async(op_sleep)),
+ ("op_sleep_sync", op_sync(op_sleep_sync::<P>)),
])
.state(move |state| {
state.put(blob_store.clone());
if let Some(location) = maybe_location.clone() {
state.put(Location(location));
}
+ state.put(StartTime::now());
Ok(())
})
.build()
diff --git a/ext/web/timers.rs b/ext/web/timers.rs
new file mode 100644
index 000000000..7f17aa969
--- /dev/null
+++ b/ext/web/timers.rs
@@ -0,0 +1,103 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+//! This module helps deno implement timers and performance APIs.
+
+use deno_core::error::AnyError;
+use deno_core::CancelFuture;
+use deno_core::CancelHandle;
+use deno_core::OpState;
+use deno_core::Resource;
+use deno_core::ResourceId;
+use std::borrow::Cow;
+use std::cell::RefCell;
+use std::rc::Rc;
+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 type StartTime = Instant;
+
+// 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 struct TimerHandle(Rc<CancelHandle>);
+
+impl Resource for TimerHandle {
+ fn name(&self) -> Cow<str> {
+ "timer".into()
+ }
+
+ fn close(self: Rc<Self>) {
+ self.0.cancel();
+ }
+}
+
+/// Creates a [`TimerHandle`] resource that can be used to cancel invocations of
+/// [`op_sleep`].
+pub fn op_timer_handle(
+ state: &mut OpState,
+ _: (),
+ _: (),
+) -> Result<ResourceId, AnyError> {
+ let rid = state
+ .resource_table
+ .add(TimerHandle(CancelHandle::new_rc()));
+ Ok(rid)
+}
+
+/// Waits asynchronously until either `millis` milliseconds have passed or the
+/// [`TimerHandle`] resource given by `rid` has been canceled.
+pub async fn op_sleep(
+ state: Rc<RefCell<OpState>>,
+ millis: u64,
+ rid: ResourceId,
+) -> Result<(), AnyError> {
+ let handle = state.borrow().resource_table.get::<TimerHandle>(rid)?;
+ tokio::time::sleep(Duration::from_millis(millis))
+ .or_cancel(handle.0.clone())
+ .await?;
+ Ok(())
+}
+
+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");
+ std::thread::sleep(Duration::from_millis(millis));
+ Ok(())
+}