diff options
| author | Andreu Botella <andreu@andreubotella.com> | 2022-02-15 12:17:30 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-02-15 12:17:30 +0100 |
| commit | 760f4c9e2427e87815a8e59b0807693c8dcb623a (patch) | |
| tree | cb04b2b42cfcf1fc46d74a3e4c7f86c73491d2b0 /ext/web | |
| parent | 5e845442fade02cd12d13e74222b26e217c5971d (diff) | |
chore(ext/timers): move ext/timers to ext/web (#13665)
Diffstat (limited to 'ext/web')
| -rw-r--r-- | ext/web/02_timers.js | 394 | ||||
| -rw-r--r-- | ext/web/15_performance.js | 569 | ||||
| -rw-r--r-- | ext/web/Cargo.toml | 9 | ||||
| -rw-r--r-- | ext/web/benches/timers_ops.rs | 53 | ||||
| -rw-r--r-- | ext/web/lib.rs | 20 | ||||
| -rw-r--r-- | ext/web/timers.rs | 103 |
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(()) +} |
