From e8b7249facaa4b64f35027f2ebe5c6c6f0e46c3f Mon Sep 17 00:00:00 2001 From: legendecas Date: Thu, 18 Aug 2022 00:46:14 +0800 Subject: [PATCH] perf_hooks: hrtime idlharness 1. Enforce receiver checks on IDL interface `Performance.now`. 2. Avoid prototype manipulation on constructing `Performance`. 3. `defineReplaceableAttribute` should create IDL getter/setter. --- lib/internal/bootstrap/browser.js | 31 ++- lib/internal/perf/performance.js | 228 ++++++++----------- lib/internal/perf/usertiming.js | 2 +- lib/internal/perf/utils.js | 8 +- lib/internal/process/pre_execution.js | 1 - lib/internal/validators.js | 9 +- test/common/wpt.js | 49 +++- test/sequential/test-worker-eventlooputil.js | 5 +- test/wpt/status/hr-time.json | 3 - test/wpt/test-hr-time.js | 2 + 10 files changed, 187 insertions(+), 151 deletions(-) diff --git a/lib/internal/bootstrap/browser.js b/lib/internal/bootstrap/browser.js index d0c01ca2a512be..92d57688e21115 100644 --- a/lib/internal/bootstrap/browser.js +++ b/lib/internal/bootstrap/browser.js @@ -75,8 +75,8 @@ exposeInterface(globalThis, 'Blob', buffer.Blob); // https://www.w3.org/TR/hr-time-2/#the-performance-attribute const perf_hooks = require('perf_hooks'); exposeInterface(globalThis, 'Performance', perf_hooks.Performance); -defineReplacableAttribute(globalThis, 'performance', - perf_hooks.performance); +defineReplaceableAttribute(globalThis, 'performance', + perf_hooks.performance); function createGlobalConsole() { const consoleFromNode = @@ -114,14 +114,33 @@ function exposeGetterAndSetter(target, name, getter, setter = undefined) { }); } -// https://heycam.github.io/webidl/#Replaceable -function defineReplacableAttribute(target, name, value) { +// https://webidl.spec.whatwg.org/#Replaceable +function defineReplaceableAttribute(target, name, value) { + let slot = value; + + // https://webidl.spec.whatwg.org/#dfn-attribute-getter + function get() { + return slot; + } + ObjectDefineProperty(get, 'name', { + __proto__: null, + value: `get ${name}`, + }); + + function set(value) { + slot = value; + } + ObjectDefineProperty(set, 'name', { + __proto__: null, + value: `set ${name}`, + }); + ObjectDefineProperty(target, name, { __proto__: null, - writable: true, enumerable: true, configurable: true, - value, + get, + set, }); } diff --git a/lib/internal/perf/performance.js b/lib/internal/perf/performance.js index 610523853f86f5..44e5d6f868c42c 100644 --- a/lib/internal/perf/performance.js +++ b/lib/internal/perf/performance.js @@ -1,9 +1,9 @@ 'use strict'; const { - ObjectDefineProperty, ObjectDefineProperties, - ObjectSetPrototypeOf, + ReflectConstruct, + SymbolToStringTag, } = primordials; const { @@ -17,9 +17,13 @@ const { EventTarget, Event, kTrustEvent, + initEventTarget, } = require('internal/event_target'); -const { now } = require('internal/perf/utils'); +const { + now, + kPerformanceBrand, +} = require('internal/perf/utils'); const { markResourceTiming } = require('internal/perf/resource_timing'); @@ -38,8 +42,9 @@ const { const { eventLoopUtilization } = require('internal/perf/event_loop_utilization'); const nodeTiming = require('internal/perf/nodetiming'); const timerify = require('internal/perf/timerify'); -const { customInspectSymbol: kInspect } = require('internal/util'); +const { customInspectSymbol: kInspect, kEnumerableProperty, kEmptyObject } = require('internal/util'); const { inspect } = require('util'); +const { validateInternalField } = require('internal/validators'); const { getTimeOriginTimestamp @@ -63,121 +68,117 @@ class Performance extends EventTarget { timeOrigin: this.timeOrigin, }, opts)}`; } -} -function toJSON() { - return { - nodeTiming: this.nodeTiming, - timeOrigin: this.timeOrigin, - eventLoopUtilization: this.eventLoopUtilization() - }; -} + clearMarks(name) { + if (name !== undefined) { + name = `${name}`; + } + clearMarkTimings(name); + clearEntriesFromBuffer('mark', name); + } -function clearMarks(name) { - if (name !== undefined) { - name = `${name}`; + clearMeasures(name) { + if (name !== undefined) { + name = `${name}`; + } + clearEntriesFromBuffer('measure', name); } - clearMarkTimings(name); - clearEntriesFromBuffer('mark', name); -} -function clearMeasures(name) { - if (name !== undefined) { - name = `${name}`; + clearResourceTimings(name = undefined) { + if (name !== undefined) { + name = `${name}`; + } + clearEntriesFromBuffer('resource', name); + } + + getEntries() { + return filterBufferMapByNameAndType(); } - clearEntriesFromBuffer('measure', name); -} -function clearResourceTimings(name) { - if (name !== undefined) { + getEntriesByName(name) { + if (arguments.length === 0) { + throw new ERR_MISSING_ARGS('name'); + } name = `${name}`; + return filterBufferMapByNameAndType(name, undefined); } - clearEntriesFromBuffer('resource', name); -} -function getEntries() { - return filterBufferMapByNameAndType(); -} + getEntriesByType(type) { + if (arguments.length === 0) { + throw new ERR_MISSING_ARGS('type'); + } + type = `${type}`; + return filterBufferMapByNameAndType(undefined, type); + } -function getEntriesByName(name) { - if (arguments.length === 0) { - throw new ERR_MISSING_ARGS('name'); + mark(name, options = kEmptyObject) { + return mark(name, options); } - name = `${name}`; - return filterBufferMapByNameAndType(name, undefined); -} -function getEntriesByType(type) { - if (arguments.length === 0) { - throw new ERR_MISSING_ARGS('type'); + measure(name, startOrMeasureOptions, endMark) { + return measure(name, startOrMeasureOptions, endMark); } - type = `${type}`; - return filterBufferMapByNameAndType(undefined, type); -} -class InternalPerformance extends EventTarget {} -InternalPerformance.prototype.constructor = Performance.prototype.constructor; -ObjectSetPrototypeOf(InternalPerformance.prototype, Performance.prototype); + now() { + validateInternalField(this, kPerformanceBrand, 'Performance'); + return now(); + } + + setResourceTimingBufferSize(maxSize) { + return setResourceTimingBufferSize(maxSize); + } + + get timeOrigin() { + validateInternalField(this, kPerformanceBrand, 'Performance'); + return getTimeOriginTimestamp(); + } + + toJSON() { + validateInternalField(this, kPerformanceBrand, 'Performance'); + return { + nodeTiming: this.nodeTiming, + timeOrigin: this.timeOrigin, + eventLoopUtilization: this.eventLoopUtilization() + }; + } +} ObjectDefineProperties(Performance.prototype, { - clearMarks: { - __proto__: null, - configurable: true, + clearMarks: kEnumerableProperty, + clearMeasures: kEnumerableProperty, + clearResourceTimings: kEnumerableProperty, + getEntries: kEnumerableProperty, + getEntriesByName: kEnumerableProperty, + getEntriesByType: kEnumerableProperty, + mark: kEnumerableProperty, + measure: kEnumerableProperty, + now: kEnumerableProperty, + timeOrigin: kEnumerableProperty, + toJSON: kEnumerableProperty, + setResourceTimingBufferSize: kEnumerableProperty, + [SymbolToStringTag]: { + __proto__: null, + writable: false, enumerable: false, - value: clearMarks, - }, - clearMeasures: { - __proto__: null, configurable: true, - enumerable: false, - value: clearMeasures, - }, - clearResourceTimings: { - __proto__: null, - configurable: true, - enumerable: false, - value: clearResourceTimings, + value: 'Performance', }, + + // Node.js specific extensions. eventLoopUtilization: { __proto__: null, configurable: true, + // Node.js specific extensions. enumerable: false, + writable: true, value: eventLoopUtilization, }, - getEntries: { - __proto__: null, - configurable: true, - enumerable: false, - value: getEntries, - }, - getEntriesByName: { - __proto__: null, - configurable: true, - enumerable: false, - value: getEntriesByName, - }, - getEntriesByType: { - __proto__: null, - configurable: true, - enumerable: false, - value: getEntriesByType, - }, - mark: { - __proto__: null, - configurable: true, - enumerable: false, - value: mark, - }, - measure: { - __proto__: null, - configurable: true, - enumerable: false, - value: measure, - }, nodeTiming: { __proto__: null, configurable: true, + // Node.js specific extensions. enumerable: false, + writable: true, value: nodeTiming, }, // In the browser, this function is not public. However, it must be used inside fetch @@ -185,55 +186,29 @@ ObjectDefineProperties(Performance.prototype, { markResourceTiming: { __proto__: null, configurable: true, + // Node.js specific extensions. enumerable: false, + writable: true, value: markResourceTiming, }, - now: { - __proto__: null, - configurable: true, - enumerable: false, - value: now, - }, - setResourceTimingBufferSize: { - __proto__: null, - configurable: true, - enumerable: false, - value: setResourceTimingBufferSize - }, timerify: { __proto__: null, configurable: true, + // Node.js specific extensions. enumerable: false, + writable: true, value: timerify, }, - // This would be updated during pre-execution in case - // the process is launched from a snapshot. - // TODO(joyeecheung): we may want to warn about access to - // this during snapshot building. - timeOrigin: { - __proto__: null, - configurable: true, - enumerable: true, - value: getTimeOriginTimestamp(), - }, - toJSON: { - __proto__: null, - configurable: true, - enumerable: true, - value: toJSON, - } }); -function refreshTimeOrigin() { - ObjectDefineProperty(Performance.prototype, 'timeOrigin', { - __proto__: null, - configurable: true, - enumerable: true, - value: getTimeOriginTimestamp(), - }); +function createPerformance() { + return ReflectConstruct(function Performance() { + initEventTarget(this); + this[kPerformanceBrand] = true; + }, [], Performance); } -const performance = new InternalPerformance(); +const performance = createPerformance(); function dispatchBufferFull(type) { const event = new Event(type, { @@ -246,5 +221,4 @@ setDispatchBufferFull(dispatchBufferFull); module.exports = { Performance, performance, - refreshTimeOrigin }; diff --git a/lib/internal/perf/usertiming.js b/lib/internal/perf/usertiming.js index c61be708829db1..298acc3738e101 100644 --- a/lib/internal/perf/usertiming.js +++ b/lib/internal/perf/usertiming.js @@ -94,7 +94,7 @@ class PerformanceMeasure extends InternalPerformanceEntry { } } -function mark(name, options = kEmptyObject) { +function mark(name, options) { const mark = new PerformanceMark(name, options); enqueue(mark); bufferUserTiming(mark); diff --git a/lib/internal/perf/utils.js b/lib/internal/perf/utils.js index bcc7e223b8c882..539ee0c62847e3 100644 --- a/lib/internal/perf/utils.js +++ b/lib/internal/perf/utils.js @@ -1,5 +1,9 @@ 'use strict'; +const { + Symbol, +} = primordials; + const binding = internalBinding('performance'); const { milestones, @@ -9,6 +13,7 @@ const { // TODO(joyeecheung): we may want to warn about access to // this during snapshot building. let timeOrigin = getTimeOrigin(); +const kPerformanceBrand = Symbol('performance'); function now() { const hr = process.hrtime(); @@ -29,5 +34,6 @@ function refreshTimeOrigin() { module.exports = { now, getMilestoneTimestamp, - refreshTimeOrigin + refreshTimeOrigin, + kPerformanceBrand, }; diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index d2f2dad8dd445a..aa1502607e838d 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -363,7 +363,6 @@ function setupTraceCategoryState() { } function setupPerfHooks() { - require('internal/perf/performance').refreshTimeOrigin(); require('internal/perf/utils').refreshTimeOrigin(); } diff --git a/lib/internal/validators.js b/lib/internal/validators.js index de8a8bb9b83b34..a192f9e6302ddb 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -418,6 +418,12 @@ function validateLinkHeaderValue(value, name) { } } +const validateInternalField = hideStackFrames((object, fieldKey, className) => { + if (typeof object !== 'object' || object === null || !ObjectPrototypeHasOwnProperty(object, fieldKey)) { + throw new ERR_INVALID_ARG_TYPE('this', className, object); + } +}); + module.exports = { isInt32, isUint32, @@ -440,5 +446,6 @@ module.exports = { validateUndefined, validateUnion, validateAbortSignal, - validateLinkHeaderValue + validateLinkHeaderValue, + validateInternalField, }; diff --git a/test/common/wpt.js b/test/common/wpt.js index 01f937fafce841..abf862b374cfca 100644 --- a/test/common/wpt.js +++ b/test/common/wpt.js @@ -298,7 +298,7 @@ class WPTRunner { this.resource = new ResourceLoader(path); this.flags = []; - this.dummyGlobalThisScript = null; + this.globalThisInitScripts = []; this.initScript = null; this.status = new StatusLoader(path); @@ -340,17 +340,17 @@ class WPTRunner { } get fullInitScript() { - if (this.initScript === null && this.dummyGlobalThisScript === null) { - return null; + if (this.globalThisInitScripts.length === null) { + return this.initScript; } + const globalThisInitScript = this.globalThisInitScripts.join('\n\n//===\n'); + if (this.initScript === null) { - return this.dummyGlobalThisScript; - } else if (this.dummyGlobalThisScript === null) { - return this.initScript; + return globalThisInitScript; } - return `${this.dummyGlobalThisScript}\n\n//===\n${this.initScript}`; + return `${globalThisInitScript}\n\n//===\n${this.initScript}`; } /** @@ -361,8 +361,9 @@ class WPTRunner { pretendGlobalThisAs(name) { switch (name) { case 'Window': { - this.dummyGlobalThisScript = - 'global.Window = Object.getPrototypeOf(globalThis).constructor;'; + this.globalThisInitScripts.push( + `global.Window = Object.getPrototypeOf(globalThis).constructor; + self.GLOBAL.isWorker = () => false;`); break; } @@ -376,6 +377,36 @@ class WPTRunner { } } + brandCheckGlobalScopeAttribute(name) { + // TODO(legendecas): idlharness GlobalScope attribute receiver validation. + const script = ` + const desc = Object.getOwnPropertyDescriptor(globalThis, '${name}'); + function getter() { + // Mimic GlobalScope instance brand check. + if (this !== globalThis) { + throw new TypeError('Illegal invocation'); + } + return desc.get(); + } + Object.defineProperty(getter, 'name', { value: 'get ${name}' }); + + function setter(value) { + // Mimic GlobalScope instance brand check. + if (this !== globalThis) { + throw new TypeError('Illegal invocation'); + } + desc.set(value); + } + Object.defineProperty(setter, 'name', { value: 'set ${name}' }); + + Object.defineProperty(globalThis, 'performance', { + get: getter, + set: setter, + }); + `; + this.globalThisInitScripts.push(script); + } + // TODO(joyeecheung): work with the upstream to port more tests in .html // to .js. async runJsTests() { diff --git a/test/sequential/test-worker-eventlooputil.js b/test/sequential/test-worker-eventlooputil.js index 7e012cb2b02e7a..5255051ab82dea 100644 --- a/test/sequential/test-worker-eventlooputil.js +++ b/test/sequential/test-worker-eventlooputil.js @@ -9,6 +9,7 @@ const { MessagePort, parentPort, } = require('worker_threads'); +const { performance } = require('perf_hooks'); const { eventLoopUtilization, now } = require('perf_hooks').performance; // Use argv to detect whether we're running as a Worker called by this test vs. @@ -35,8 +36,8 @@ function workerOnMetricsMsg(msg) { if (msg.cmd === 'spin') { const elu = eventLoopUtilization(); - const t = now(); - while (now() - t < msg.dur); + const t = performance.now(); + while (performance.now() - t < msg.dur); return this.postMessage(eventLoopUtilization(elu)); } } diff --git a/test/wpt/status/hr-time.json b/test/wpt/status/hr-time.json index b23a5a4e96a6a4..973e32b298a557 100644 --- a/test/wpt/status/hr-time.json +++ b/test/wpt/status/hr-time.json @@ -1,7 +1,4 @@ { - "idlharness.any.js": { - "skip": "TODO: update IDL parser" - }, "window-worker-timeOrigin.window.js": { "skip": "depends on URL.createObjectURL(blob)" } diff --git a/test/wpt/test-hr-time.js b/test/wpt/test-hr-time.js index 36fdde8036c0cc..7fb55de5cf835a 100644 --- a/test/wpt/test-hr-time.js +++ b/test/wpt/test-hr-time.js @@ -5,6 +5,8 @@ const { WPTRunner } = require('../common/wpt'); const runner = new WPTRunner('hr-time'); +runner.pretendGlobalThisAs('Window'); +runner.brandCheckGlobalScopeAttribute('performance'); runner.setInitScript(` const { Blob } = require('buffer'); global.Blob = Blob;