From 760f4c9e2427e87815a8e59b0807693c8dcb623a Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Tue, 15 Feb 2022 12:17:30 +0100 Subject: chore(ext/timers): move ext/timers to ext/web (#13665) --- Cargo.lock | 16 +- Cargo.toml | 1 - cli/tests/testdata/worker_drop_handle_race.js.out | 4 +- ext/timers/01_timers.js | 394 --------------- ext/timers/02_performance.js | 569 ---------------------- ext/timers/Cargo.toml | 28 -- ext/timers/README.md | 5 - ext/timers/benches/timers_ops.rs | 54 -- ext/timers/lib.rs | 127 ----- ext/web/02_timers.js | 394 +++++++++++++++ ext/web/15_performance.js | 569 ++++++++++++++++++++++ ext/web/Cargo.toml | 9 + ext/web/benches/timers_ops.rs | 53 ++ ext/web/lib.rs | 20 +- ext/web/timers.rs | 103 ++++ runtime/Cargo.toml | 2 - runtime/build.rs | 8 +- runtime/lib.rs | 1 - runtime/permissions.rs | 2 +- runtime/web_worker.rs | 6 +- runtime/worker.rs | 3 +- 21 files changed, 1163 insertions(+), 1205 deletions(-) delete mode 100644 ext/timers/01_timers.js delete mode 100644 ext/timers/02_performance.js delete mode 100644 ext/timers/Cargo.toml delete mode 100644 ext/timers/README.md delete mode 100644 ext/timers/benches/timers_ops.rs delete mode 100644 ext/timers/lib.rs create mode 100644 ext/web/02_timers.js create mode 100644 ext/web/15_performance.js create mode 100644 ext/web/benches/timers_ops.rs create mode 100644 ext/web/timers.rs diff --git a/Cargo.lock b/Cargo.lock index 49d478a00..170272e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1006,7 +1006,6 @@ dependencies = [ "deno_ffi", "deno_http", "deno_net", - "deno_timers", "deno_tls", "deno_url", "deno_web", @@ -1041,18 +1040,6 @@ dependencies = [ "winres", ] -[[package]] -name = "deno_timers" -version = "0.34.0" -dependencies = [ - "deno_bench_util", - "deno_core", - "deno_url", - "deno_web", - "deno_webidl", - "tokio", -] - [[package]] name = "deno_tls" version = "0.23.0" @@ -1085,7 +1072,10 @@ version = "0.67.0" dependencies = [ "async-trait", "base64 0.13.0", + "deno_bench_util", "deno_core", + "deno_url", + "deno_webidl", "encoding_rs", "flate2", "serde", diff --git a/Cargo.toml b/Cargo.toml index 3f206b223..781b88cb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ members = [ "ext/ffi", "ext/http", "ext/net", - "ext/timers", "ext/url", "ext/web", "ext/webgpu", diff --git a/cli/tests/testdata/worker_drop_handle_race.js.out b/cli/tests/testdata/worker_drop_handle_race.js.out index b7218e8f6..34c2d5be2 100644 --- a/cli/tests/testdata/worker_drop_handle_race.js.out +++ b/cli/tests/testdata/worker_drop_handle_race.js.out @@ -2,7 +2,7 @@ error: Uncaught (in worker "") Error throw new Error(); ^ at [WILDCARD]/workers/drop_handle_race.js:2:9 - at Object.action (deno:ext/timers/[WILDCARD]) - at handleTimerMacrotask (deno:ext/timers/[WILDCARD]) + at Object.action (deno:ext/web/02_timers.js:[WILDCARD]) + at handleTimerMacrotask (deno:ext/web/02_timers.js:[WILDCARD]) error: Uncaught (in promise) Error: Unhandled error event in child worker. at Worker.#pollControl (deno:runtime/js/11_workers.js:[WILDCARD]) diff --git a/ext/timers/01_timers.js b/ext/timers/01_timers.js deleted file mode 100644 index a0b1deb45..000000000 --- a/ext/timers/01_timers.js +++ /dev/null @@ -1,394 +0,0 @@ -// 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} - */ - const activeTimers = new Map(); - - let nextId = 1; - - /** - * @param {Function | string} callback - * @param {number} timeout - * @param {Array} 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/timers/02_performance.js b/ext/timers/02_performance.js deleted file mode 100644 index c48a3d888..000000000 --- a/ext/timers/02_performance.js +++ /dev/null @@ -1,569 +0,0 @@ -// 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/timers/Cargo.toml b/ext/timers/Cargo.toml deleted file mode 100644 index 15979388e..000000000 --- a/ext/timers/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. - -[package] -name = "deno_timers" -version = "0.34.0" -authors = ["the Deno authors"] -edition = "2021" -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.118.0", path = "../../core" } -tokio = { version = "1.10.1", features = ["full"] } - -[dev-dependencies] -deno_bench_util = { version = "0.30.0", path = "../../bench_util" } -deno_url = { version = "0.36.0", path = "../url" } -deno_web = { version = "0.67.0", path = "../web" } -deno_webidl = { version = "0.36.0", path = "../webidl" } - -[[bench]] -name = "timers_ops" -harness = false diff --git a/ext/timers/README.md b/ext/timers/README.md deleted file mode 100644 index 5a2a8e516..000000000 --- a/ext/timers/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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 deleted file mode 100644 index 8d13d5807..000000000 --- a/ext/timers/benches/timers_ops.rs +++ /dev/null @@ -1,54 +0,0 @@ -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_timers::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 { - vec![ - deno_webidl::init(), - deno_url::init(), - deno_web::init(BlobStore::default(), None), - deno_timers::init::(), - 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/timers/lib.rs b/ext/timers/lib.rs deleted file mode 100644 index 63aabe9d4..000000000 --- a/ext/timers/lib.rs +++ /dev/null @@ -1,127 +0,0 @@ -// 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::include_js_files; -use deno_core::op_async; -use deno_core::op_sync; -use deno_core::CancelFuture; -use deno_core::CancelHandle; -use deno_core::Extension; -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 fn init() -> Extension { - Extension::builder() - .js(include_js_files!( - prefix "deno:ext/timers", - "01_timers.js", - "02_performance.js", - )) - .ops(vec![ - ("op_now", op_sync(op_now::

)), - ("op_timer_handle", op_sync(op_timer_handle)), - ("op_sleep", op_async(op_sleep)), - ("op_sleep_sync", op_sync(op_sleep_sync::

)), - ]) - .state(|state| { - state.put(StartTime::now()); - Ok(()) - }) - .build() -} - -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( - state: &mut OpState, - _argument: (), - _: (), -) -> Result -where - TP: TimersPermission + 'static, -{ - let start_time = state.borrow::(); - 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::().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); - -impl Resource for TimerHandle { - fn name(&self) -> Cow { - "timer".into() - } - - fn close(self: Rc) { - 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 { - 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>, - millis: u64, - rid: ResourceId, -) -> Result<(), AnyError> { - let handle = state.borrow().resource_table.get::(rid)?; - tokio::time::sleep(Duration::from_millis(millis)) - .or_cancel(handle.0.clone()) - .await?; - Ok(()) -} - -pub fn op_sleep_sync( - state: &mut OpState, - millis: u64, - _: (), -) -> Result<(), AnyError> -where - TP: TimersPermission + 'static, -{ - state.borrow::().check_unstable(state, "Deno.sleepSync"); - std::thread::sleep(Duration::from_millis(millis)); - Ok(()) -} 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} + */ + const activeTimers = new Map(); + + let nextId = 1; + + /** + * @param {Function | string} callback + * @param {number} timeout + * @param {Array} 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 { + vec![ + deno_webidl::init(), + deno_url::init(), + deno_web::init::(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) -> Extension { +pub fn init( + blob_store: BlobStore, + maybe_location: Option, +) -> Extension { Extension::builder() .js(include_js_files!( prefix "deno:ext/web", @@ -57,6 +68,7 @@ pub fn init(blob_store: BlobStore, maybe_location: Option) -> 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) -> 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) -> Extension { "op_compression_finish", op_sync(compression::op_compression_finish), ), + ("op_now", op_sync(op_now::

)), + ("op_timer_handle", op_sync(op_timer_handle)), + ("op_sleep", op_async(op_sleep)), + ("op_sleep_sync", op_sync(op_sleep_sync::

)), ]) .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( + state: &mut OpState, + _argument: (), + _: (), +) -> Result +where + TP: TimersPermission + 'static, +{ + let start_time = state.borrow::(); + 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::().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); + +impl Resource for TimerHandle { + fn name(&self) -> Cow { + "timer".into() + } + + fn close(self: Rc) { + 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 { + 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>, + millis: u64, + rid: ResourceId, +) -> Result<(), AnyError> { + let handle = state.borrow().resource_table.get::(rid)?; + tokio::time::sleep(Duration::from_millis(millis)) + .or_cancel(handle.0.clone()) + .await?; + Ok(()) +} + +pub fn op_sleep_sync( + state: &mut OpState, + millis: u64, + _: (), +) -> Result<(), AnyError> +where + TP: TimersPermission + 'static, +{ + state.borrow::().check_unstable(state, "Deno.sleepSync"); + std::thread::sleep(Duration::from_millis(millis)); + Ok(()) +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 6333d3387..1763d6feb 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -30,7 +30,6 @@ deno_fetch = { version = "0.59.0", path = "../ext/fetch" } deno_ffi = { version = "0.23.0", path = "../ext/ffi" } deno_http = { version = "0.28.0", path = "../ext/http" } deno_net = { version = "0.28.0", path = "../ext/net" } -deno_timers = { version = "0.34.0", path = "../ext/timers" } deno_tls = { version = "0.23.0", path = "../ext/tls" } deno_url = { version = "0.36.0", path = "../ext/url" } deno_web = { version = "0.67.0", path = "../ext/web" } @@ -54,7 +53,6 @@ deno_fetch = { version = "0.59.0", path = "../ext/fetch" } deno_ffi = { version = "0.23.0", path = "../ext/ffi" } deno_http = { version = "0.28.0", path = "../ext/http" } deno_net = { version = "0.28.0", path = "../ext/net" } -deno_timers = { version = "0.34.0", path = "../ext/timers" } deno_tls = { version = "0.23.0", path = "../ext/tls" } deno_url = { version = "0.36.0", path = "../ext/url" } deno_web = { version = "0.67.0", path = "../ext/web" } diff --git a/runtime/build.rs b/runtime/build.rs index e2fe21b9e..eea7a3602 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -93,7 +93,7 @@ mod not_docs { } } - impl deno_timers::TimersPermission for Permissions { + impl deno_web::TimersPermission for Permissions { fn allow_hrtime(&mut self) -> bool { unreachable!("snapshotting!") } @@ -145,13 +145,15 @@ mod not_docs { deno_console::init(), deno_url::init(), deno_tls::init(), - deno_web::init(deno_web::BlobStore::default(), Default::default()), + deno_web::init::( + deno_web::BlobStore::default(), + Default::default(), + ), deno_fetch::init::(Default::default()), deno_websocket::init::("".to_owned(), None, None), deno_webstorage::init(None), deno_crypto::init(None), deno_webgpu::init(false), - deno_timers::init::(), deno_broadcast_channel::init( deno_broadcast_channel::InMemoryBroadcastChannel::default(), false, // No --unstable. diff --git a/runtime/lib.rs b/runtime/lib.rs index fef3956ea..543d3a0a2 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -8,7 +8,6 @@ pub use deno_fetch; pub use deno_ffi; pub use deno_http; pub use deno_net; -pub use deno_timers; pub use deno_tls; pub use deno_url; pub use deno_web; diff --git a/runtime/permissions.rs b/runtime/permissions.rs index c404436d6..77ad8496f 100644 --- a/runtime/permissions.rs +++ b/runtime/permissions.rs @@ -1314,7 +1314,7 @@ impl deno_fetch::FetchPermissions for Permissions { } } -impl deno_timers::TimersPermission for Permissions { +impl deno_web::TimersPermission for Permissions { fn allow_hrtime(&mut self) -> bool { self.hrtime.check().is_ok() } diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index 8cbbb5d4f..c90e91b92 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -368,7 +368,10 @@ impl WebWorker { deno_webidl::init(), deno_console::init(), deno_url::init(), - deno_web::init(options.blob_store.clone(), Some(main_module.clone())), + deno_web::init::( + options.blob_store.clone(), + Some(main_module.clone()), + ), deno_fetch::init::(deno_fetch::Options { user_agent: options.user_agent.clone(), root_cert_store: options.root_cert_store.clone(), @@ -386,7 +389,6 @@ impl WebWorker { deno_broadcast_channel::init(options.broadcast_channel.clone(), unstable), deno_crypto::init(options.seed), deno_webgpu::init(unstable), - deno_timers::init::(), // ffi deno_ffi::init::(unstable), // Permissions ext (worker specific state) diff --git a/runtime/worker.rs b/runtime/worker.rs index 1dc9504d6..1e31b84dc 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -100,7 +100,7 @@ impl MainWorker { deno_webidl::init(), deno_console::init(), deno_url::init(), - deno_web::init( + deno_web::init::( options.blob_store.clone(), options.bootstrap.location.clone(), ), @@ -122,7 +122,6 @@ impl MainWorker { deno_crypto::init(options.seed), deno_broadcast_channel::init(options.broadcast_channel.clone(), unstable), deno_webgpu::init(unstable), - deno_timers::init::(), // ffi deno_ffi::init::(unstable), // Runtime ops -- cgit v1.2.3