From d2d0f6f143c5e872ac2d1a35b3ec4429e67875e5 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Sat, 9 Apr 2022 23:37:03 -0300 Subject: [PATCH] perf_hooks: add PerformanceResourceTiming perf_hooks: create clearResourceTimings perf_hooks: add resourcetiming test parallel perf_hooks: add markResourceTiming perf_hooks: fix observable when using resource perf_hooks: fix observable when using resource perf_hooks: add class comments perf_hooks: add PerformanceResourceTiming perf_hooks: create clearResourceTimings perf_hooks: add resourcetiming test parallel perf_hooks: add markResourceTiming perf_hooks: fix observable when using resource perf_hooks: fix observable when using resource perf_hooks: add class comments perf_hooks: add Resource Timing documentation benchmark: measure resource timing module perf_hooks: add check avoiding new PerformanceResourceTiming perf_hooks: adjust doc PR-URL: https://github.com/nodejs/node/pull/42725 Fixes: https://github.com/nodejs/undici/issues/952 Reviewed-By: Robert Nagy Reviewed-By: James M Snell Reviewed-By: Stephen Belanger Reviewed-By: Paolo Insogna Reviewed-By: Chengzhong Wu --- benchmark/perf_hooks/resourcetiming.js | 77 +++++ doc/api/perf_hooks.md | 223 +++++++++++++++ lib/internal/perf/observe.js | 16 +- lib/internal/perf/performance.js | 21 ++ lib/internal/perf/resource_timing.js | 179 ++++++++++++ lib/perf_hooks.js | 2 + test/parallel/test-bootstrap-modules.js | 1 + .../test-perf-hooks-resourcetiming.js | 265 ++++++++++++++++++ 8 files changed, 781 insertions(+), 3 deletions(-) create mode 100644 benchmark/perf_hooks/resourcetiming.js create mode 100644 lib/internal/perf/resource_timing.js create mode 100644 test/parallel/test-perf-hooks-resourcetiming.js diff --git a/benchmark/perf_hooks/resourcetiming.js b/benchmark/perf_hooks/resourcetiming.js new file mode 100644 index 00000000000000..c71cfeae7ef06b --- /dev/null +++ b/benchmark/perf_hooks/resourcetiming.js @@ -0,0 +1,77 @@ +'use strict'; + +const common = require('../common.js'); + +const { + PerformanceObserver, + performance, +} = require('perf_hooks'); + +function createTimingInfo({ + startTime = 0, + redirectStartTime = 0, + redirectEndTime = 0, + postRedirectStartTime = 0, + finalServiceWorkerStartTime = 0, + finalNetworkRequestStartTime = 0, + finalNetworkResponseStartTime = 0, + endTime = 0, + encodedBodySize = 0, + decodedBodySize = 0, + finalConnectionTimingInfo = null +}) { + if (finalConnectionTimingInfo !== null) { + finalConnectionTimingInfo.domainLookupStartTime = + finalConnectionTimingInfo.domainLookupStartTime || 0; + finalConnectionTimingInfo.domainLookupEndTime = + finalConnectionTimingInfo.domainLookupEndTime || 0; + finalConnectionTimingInfo.connectionStartTime = + finalConnectionTimingInfo.connectionStartTime || 0; + finalConnectionTimingInfo.connectionEndTime = + finalConnectionTimingInfo.connectionEndTime || 0; + finalConnectionTimingInfo.secureConnectionStartTime = + finalConnectionTimingInfo.secureConnectionStartTime || 0; + finalConnectionTimingInfo.ALPNNegotiatedProtocol = + finalConnectionTimingInfo.ALPNNegotiatedProtocol || []; + } + return { + startTime, + redirectStartTime, + redirectEndTime, + postRedirectStartTime, + finalServiceWorkerStartTime, + finalNetworkRequestStartTime, + finalNetworkResponseStartTime, + endTime, + encodedBodySize, + decodedBodySize, + finalConnectionTimingInfo, + }; +} + +const bench = common.createBenchmark(main, { + n: [1e5], + observe: ['resource'], +}); + +function test() { + const timingInfo = createTimingInfo({ finalConnectionTimingInfo: {} }); + performance.markResourceTiming( + timingInfo, + 'http://localhost:8080', + 'fetch', + {}, + '' + ); +} + +function main({ n, observe }) { + const obs = new PerformanceObserver(() => { + bench.end(n); + }); + obs.observe({ entryTypes: [observe], buffered: true }); + + bench.start(); + for (let i = 0; i < 1e5; i++) + test(); +} diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index 73720139c534a0..099d9dfcefef90 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -15,6 +15,7 @@ Node.js supports the following [Web Performance APIs][]: * [High Resolution Time][] * [Performance Timeline][] * [User Timing][] +* [Resource Timing][] ```js const { PerformanceObserver, performance } = require('node:perf_hooks'); @@ -66,6 +67,17 @@ added: v16.7.0 If `name` is not provided, removes all `PerformanceMeasure` objects from the Performance Timeline. If `name` is provided, removes only the named mark. +### `performance.clearResourceTimings([name])` + + + +* `name` {string} + +If `name` is not provided, removes all `PerformanceResourceTiming` objects from +the Resource Timeline. If `name` is provided, removes only the named resource. + ### `performance.eventLoopUtilization([utilization1[, utilization2]])` + +* `timingInfo` {Object} [Fetch Timing Info][] +* `requestedUrl` {string} The resource url +* `initiatorType` {string} The initiator name, e.g: 'fetch' +* `global` {Object} +* `cacheMode` {string} The cache mode must be an empty string ('') or 'local' + +_This property is an extension by Node.js. It is not available in Web browsers._ + +Creates a new `PerformanceResourceTiming` entry in the Resource Timeline. A +`PerformanceResourceTiming` is a subclass of `PerformanceEntry` whose +`performanceEntry.entryType` is always `'resource'`. Performance resources +are used to mark moments in the Resource Timeline. + +The created `PerformanceMark` entry is put in the global Resource Timeline +and can be queried with `performance.getEntries`, +`performance.getEntriesByName`, and `performance.getEntriesByType`. When the +observation is performed, the entries should be cleared from the global +Performance Timeline manually with `performance.clearResourceTimings`. + ### `performance.measure(name[, startMarkOrOptions[, endMark]])` + +* Extends: {PerformanceEntry} + +Provides detailed network timing data regarding the loading of an application's +resources. + +The constructor of this class is not exposed to users directly. + +### `performanceResourceTiming.workerStart` + + + +* {number} + +The high resolution millisecond timestamp at immediately before dispatching +the `fetch` request. If the resource is not intercepted by a worker the property +will always return 0. + +### `performanceResourceTiming.redirectStart` + + + +* {number} + +The high resolution millisecond timestamp that represents the start time +of the fetch which initiates the redirect. + +### `performanceResourceTiming.redirectEnd` + + + +* {number} + +The high resolution millisecond timestamp that will be created immediately after +receiving the last byte of the response of the last redirect. + +### `performanceResourceTiming.fetchStart` + + + +* {number} + +The high resolution millisecond timestamp immediately before the Node.js starts +to fetch the resource. + +### `performanceResourceTiming.domainLookupStart` + + + +* {number} + +The high resolution millisecond timestamp immediately before the Node.js starts +the domain name lookup for the resource. + +### `performanceResourceTiming.domainLookupEnd` + + + +* {number} + +The high resolution millisecond timestamp representing the time immediately +after the Node.js finished the domain name lookup for the resource. + +### `performanceResourceTiming.connectStart` + + + +* {number} + +The high resolution millisecond timestamp representing the time immediately +before Node.js starts to establish the connection to the server to retrieve +the resource. + +### `performanceResourceTiming.connectEnd` + + + +* {number} + +The high resolution millisecond timestamp representing the time immediately +after Node.js finishes establishing the connection to the server to retrieve +the resource. + +### `performanceResourceTiming.secureConnectionStart` + + + +* {number} + +The high resolution millisecond timestamp representing the time immediately +before Node.js starts the handshake process to secure the current connection. + +### `performanceResourceTiming.requestStart` + + + +* {number} + +The high resolution millisecond timestamp representing the time immediately +before Node.js receives the first byte of the response from the server. + +### `performanceResourceTiming.responseEnd` + + + +* {number} + +The high resolution millisecond timestamp representing the time immediately +after Node.js receives the last byte of the resource or immediately before +the transport connection is closed, whichever comes first. + +### `performanceResourceTiming.transferSize` + + + +* {number} + +A number representing the size (in octets) of the fetched resource. The size +includes the response header fields plus the response payload body. + +### `performanceResourceTiming.encodedBodySize` + + + +* {number} + +A number representing the size (in octets) received from the fetch +(HTTP or cache), of the payload body, before removing any applied +content-codings. + +### `performanceResourceTiming.decodedBodySize` + + + +* {number} + +A number representing the size (in octets) received from the fetch +(HTTP or cache), of the message body, after removing any applied +content-codings. + +### `performanceResourceTiming.toJSON()` + + + +Returns a `object` that is the JSON representation of the +`PerformanceResourceTiming` object + ## Class: `perf_hooks.PerformanceObserver` ### `new PerformanceObserver(callback)` @@ -1337,8 +1558,10 @@ dns.promises.resolve('localhost'); ``` [Async Hooks]: async_hooks.md +[Fetch Timing Info]: https://fetch.spec.whatwg.org/#fetch-timing-info [High Resolution Time]: https://www.w3.org/TR/hr-time-2 [Performance Timeline]: https://w3c.github.io/performance-timeline/ +[Resource Timing]: https://www.w3.org/TR/resource-timing-2/ [User Timing]: https://www.w3.org/TR/user-timing/ [Web Performance APIs]: https://w3c.github.io/perf-timing-primer/ [Worker threads]: worker_threads.md#worker-threads diff --git a/lib/internal/perf/observe.js b/lib/internal/perf/observe.js index c3ce8345b156ea..8cff996315560c 100644 --- a/lib/internal/perf/observe.js +++ b/lib/internal/perf/observe.js @@ -89,15 +89,18 @@ const kSupportedEntryTypes = ObjectFreeze([ 'mark', 'measure', 'net', + 'resource', ]); // Performance timeline entry Buffers let markEntryBuffer = []; let measureEntryBuffer = []; +let resourceTimingBuffer = []; const kMaxPerformanceEntryBuffers = 1e6; const kClearPerformanceEntryBuffers = ObjectFreeze({ 'mark': 'performance.clearMarks', 'measure': 'performance.clearMeasures', + 'resource': 'performance.clearResourceTimings', }); const kWarnedEntryTypes = new SafeMap(); @@ -343,6 +346,8 @@ function enqueue(entry) { buffer = markEntryBuffer; } else if (entryType === 'measure') { buffer = measureEntryBuffer; + } else if (entryType === 'resource') { + buffer = resourceTimingBuffer; } else { return; } @@ -368,16 +373,19 @@ function enqueue(entry) { } function clearEntriesFromBuffer(type, name) { - if (type !== 'mark' && type !== 'measure') { + if (type !== 'mark' && type !== 'measure' && type !== 'resource') { return; } if (type === 'mark') { markEntryBuffer = name === undefined ? [] : ArrayPrototypeFilter(markEntryBuffer, (entry) => entry.name !== name); - } else { + } else if (type === 'measure') { measureEntryBuffer = name === undefined ? [] : ArrayPrototypeFilter(measureEntryBuffer, (entry) => entry.name !== name); + } else { + resourceTimingBuffer = name === undefined ? + [] : ArrayPrototypeFilter(resourceTimingBuffer, (entry) => entry.name !== name); } } @@ -387,11 +395,13 @@ function filterBufferMapByNameAndType(name, type) { bufferList = markEntryBuffer; } else if (type === 'measure') { bufferList = measureEntryBuffer; + } else if (type === 'resource') { + bufferList = resourceTimingBuffer; } else if (type !== undefined) { // Unrecognized type; return []; } else { - bufferList = ArrayPrototypeConcat(markEntryBuffer, measureEntryBuffer); + bufferList = ArrayPrototypeConcat(markEntryBuffer, measureEntryBuffer, resourceTimingBuffer); } if (name !== undefined) { bufferList = ArrayPrototypeFilter(bufferList, (buffer) => buffer.name === name); diff --git a/lib/internal/perf/performance.js b/lib/internal/perf/performance.js index 20603fa382e702..5c7c008ee14a8e 100644 --- a/lib/internal/perf/performance.js +++ b/lib/internal/perf/performance.js @@ -19,6 +19,8 @@ const { const { now } = require('internal/perf/utils'); +const { markResourceTiming } = require('internal/perf/resource_timing'); + const { mark, measure, @@ -82,6 +84,13 @@ function clearMeasures(name) { clearEntriesFromBuffer('measure', name); } +function clearResourceTimings(name) { + if (name !== undefined) { + name = `${name}`; + } + clearEntriesFromBuffer('resource', name); +} + function getEntries() { return filterBufferMapByNameAndType(); } @@ -117,6 +126,11 @@ ObjectDefineProperties(Performance.prototype, { enumerable: false, value: clearMeasures, }, + clearResourceTimings: { + configurable: true, + enumerable: false, + value: clearResourceTimings, + }, eventLoopUtilization: { configurable: true, enumerable: false, @@ -152,6 +166,13 @@ ObjectDefineProperties(Performance.prototype, { enumerable: false, value: nodeTiming, }, + // In the browser, this function is not public. However, it must be used inside fetch + // which is a Node.js dependency, not a internal module + markResourceTiming: { + configurable: true, + enumerable: false, + value: markResourceTiming, + }, now: { configurable: true, enumerable: false, diff --git a/lib/internal/perf/resource_timing.js b/lib/internal/perf/resource_timing.js new file mode 100644 index 00000000000000..ff0728c9f0ffff --- /dev/null +++ b/lib/internal/perf/resource_timing.js @@ -0,0 +1,179 @@ +'use strict'; +// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming + +const { InternalPerformanceEntry } = require('internal/perf/performance_entry'); +const { SymbolToStringTag } = primordials; +const assert = require('internal/assert'); +const { enqueue } = require('internal/perf/observe'); +const { Symbol, ObjectSetPrototypeOf } = primordials; + +const kCacheMode = Symbol('kCacheMode'); +const kRequestedUrl = Symbol('kRequestedUrl'); +const kTimingInfo = Symbol('kTimingInfo'); +const kInitiatorType = Symbol('kInitiatorType'); + +const { + codes: { + ERR_ILLEGAL_CONSTRUCTOR, + } +} = require('internal/errors'); + +class InternalPerformanceResourceTiming extends InternalPerformanceEntry { + constructor(requestedUrl, initiatorType, timingInfo, cacheMode = '') { + super(requestedUrl, 'resource'); + this[kInitiatorType] = initiatorType; + this[kRequestedUrl] = requestedUrl; + // https://fetch.spec.whatwg.org/#fetch-timing-info + // This class is using timingInfo assuming it's already validated. + // The spec doesn't say to validate it in the class construction. + this[kTimingInfo] = timingInfo; + this[kCacheMode] = cacheMode; + } + + get [SymbolToStringTag]() { + return 'PerformanceResourceTiming'; + } + + get name() { + return this[kRequestedUrl]; + } + + get startTime() { + return this[kTimingInfo].startTime; + } + + get duration() { + return this[kTimingInfo].endTime - this[kTimingInfo].startTime; + } + + get workerStart() { + return this[kTimingInfo].finalServiceWorkerStartTime; + } + + get redirectStart() { + return this[kTimingInfo].redirectStartTime; + } + + get redirectEnd() { + return this[kTimingInfo].redirectEndTime; + } + + get fetchStart() { + return this[kTimingInfo].postRedirectStartTime; + } + + get domainLookupStart() { + return this[kTimingInfo].finalConnectionTimingInfo?.domainLookupStartTime; + } + + get domainLookupEnd() { + return this[kTimingInfo].finalConnectionTimingInfo?.domainLookupEndTime; + } + + get connectStart() { + return this[kTimingInfo].finalConnectionTimingInfo?.connectionStartTime; + } + + get connectEnd() { + return this[kTimingInfo].finalConnectionTimingInfo?.connectionEndTime; + } + + get secureConnectionStart() { + return this[kTimingInfo] + .finalConnectionTimingInfo?.secureConnectionStartTime; + } + + get nextHopProtocol() { + return this[kTimingInfo] + .finalConnectionTimingInfo?.ALPNNegotiatedProtocol; + } + + get requestStart() { + return this[kTimingInfo].finalNetworkRequestStartTime; + } + + get responseStart() { + return this[kTimingInfo].finalNetworkResponseStartTime; + } + + get responseEnd() { + return this[kTimingInfo].endTime; + } + + get encodedBodySize() { + return this[kTimingInfo].encodedBodySize; + } + + get decodedBodySize() { + return this[kTimingInfo].decodedBodySize; + } + + get transferSize() { + if (this[kCacheMode] === 'local') return 0; + if (this[kCacheMode] === 'validated') return 300; + + return this[kTimingInfo].encodedBodySize + 300; + } + + toJSON() { + return { + name: this.name, + entryType: this.entryType, + startTime: this.startTime, + duration: this.duration, + initiatorType: this[kInitiatorType], + nextHopProtocol: this.nextHopProtocol, + workerStart: this.workerStart, + redirectStart: this.redirectStart, + redirectEnd: this.redirectEnd, + fetchStart: this.fetchStart, + domainLookupStart: this.domainLookupStart, + domainLookupEnd: this.domainLookupEnd, + connectStart: this.connectStart, + connectEnd: this.connectEnd, + secureConnectionStart: this.secureConnectionStart, + requestStart: this.requestStart, + responseStart: this.responseStart, + responseEnd: this.responseEnd, + transferSize: this.transferSize, + encodedBodySize: this.encodedBodySize, + decodedBodySize: this.decodedBodySize, + }; + } +} + +class PerformanceResourceTiming extends InternalPerformanceResourceTiming { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } +} + +// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing +function markResourceTiming( + timingInfo, + requestedUrl, + initiatorType, + global, + cacheMode, +) { + // https://w3c.github.io/resource-timing/#dfn-setup-the-resource-timing-entry + assert( + cacheMode === '' || cacheMode === 'local', + 'cache must be an empty string or \'local\'', + ); + const resource = new InternalPerformanceResourceTiming( + requestedUrl, + initiatorType, + timingInfo, + cacheMode, + ); + + ObjectSetPrototypeOf(resource, PerformanceResourceTiming.prototype); + enqueue(resource); + return resource; +} + +module.exports = { + PerformanceResourceTiming, + markResourceTiming, +}; diff --git a/lib/perf_hooks.js b/lib/perf_hooks.js index 74ba890f537bc2..2456bcb7e7e029 100644 --- a/lib/perf_hooks.js +++ b/lib/perf_hooks.js @@ -9,6 +9,7 @@ const { } = internalBinding('performance'); const { PerformanceEntry } = require('internal/perf/performance_entry'); +const { PerformanceResourceTiming } = require('internal/perf/resource_timing'); const { PerformanceObserver, PerformanceObserverEntryList, @@ -31,6 +32,7 @@ module.exports = { PerformanceMeasure, PerformanceObserver, PerformanceObserverEntryList, + PerformanceResourceTiming, monitorEventLoopDelay, createHistogram, performance: new InternalPerformance(), diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index de9414abb2d648..ff03a3238cbb0f 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -98,6 +98,7 @@ const expectedModules = new Set([ 'NativeModule internal/perf/performance', 'NativeModule internal/perf/timerify', 'NativeModule internal/perf/usertiming', + 'NativeModule internal/perf/resource_timing', 'NativeModule internal/perf/utils', 'NativeModule internal/priority_queue', 'NativeModule internal/process/esm_loader', diff --git a/test/parallel/test-perf-hooks-resourcetiming.js b/test/parallel/test-perf-hooks-resourcetiming.js new file mode 100644 index 00000000000000..60a7e908bed6f4 --- /dev/null +++ b/test/parallel/test-perf-hooks-resourcetiming.js @@ -0,0 +1,265 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + PerformanceObserver, + PerformanceEntry, + PerformanceResourceTiming, + performance: { + clearResourceTimings, + markResourceTiming, + }, +} = require('perf_hooks'); + +assert(PerformanceObserver); +assert(PerformanceEntry); +assert(PerformanceResourceTiming); +assert(clearResourceTimings); +assert(markResourceTiming); + +function createTimingInfo({ + startTime = 0, + redirectStartTime = 0, + redirectEndTime = 0, + postRedirectStartTime = 0, + finalServiceWorkerStartTime = 0, + finalNetworkRequestStartTime = 0, + finalNetworkResponseStartTime = 0, + endTime = 0, + encodedBodySize = 0, + decodedBodySize = 0, + finalConnectionTimingInfo = null +}) { + if (finalConnectionTimingInfo !== null) { + finalConnectionTimingInfo.domainLookupStartTime = + finalConnectionTimingInfo.domainLookupStartTime || 0; + finalConnectionTimingInfo.domainLookupEndTime = + finalConnectionTimingInfo.domainLookupEndTime || 0; + finalConnectionTimingInfo.connectionStartTime = + finalConnectionTimingInfo.connectionStartTime || 0; + finalConnectionTimingInfo.connectionEndTime = + finalConnectionTimingInfo.connectionEndTime || 0; + finalConnectionTimingInfo.secureConnectionStartTime = + finalConnectionTimingInfo.secureConnectionStartTime || 0; + finalConnectionTimingInfo.ALPNNegotiatedProtocol = + finalConnectionTimingInfo.ALPNNegotiatedProtocol || []; + } + return { + startTime, + redirectStartTime, + redirectEndTime, + postRedirectStartTime, + finalServiceWorkerStartTime, + finalNetworkRequestStartTime, + finalNetworkResponseStartTime, + endTime, + encodedBodySize, + decodedBodySize, + finalConnectionTimingInfo, + }; +} + +// PerformanceResourceTiming should not be initialized externally +{ + assert.throws(() => new PerformanceResourceTiming(), { + name: 'TypeError', + message: 'Illegal constructor', + code: 'ERR_ILLEGAL_CONSTRUCTOR', + }); +} + +// Using performance.getEntries*() +{ + const timingInfo = createTimingInfo({ finalConnectionTimingInfo: {} }); + const customGlobal = {}; + const requestedUrl = 'http://localhost:8080'; + const cacheMode = 'local'; + const initiatorType = 'fetch'; + const resource = markResourceTiming( + timingInfo, + requestedUrl, + initiatorType, + customGlobal, + cacheMode, + ); + + assert(resource instanceof PerformanceEntry); + assert(resource instanceof PerformanceResourceTiming); + + { + const entries = performance.getEntries(); + assert.strictEqual(entries.length, 1); + assert(entries[0] instanceof PerformanceResourceTiming); + } + + { + const entries = performance.getEntriesByType('resource'); + assert.strictEqual(entries.length, 1); + assert(entries[0] instanceof PerformanceResourceTiming); + } + + { + const entries = performance.getEntriesByName(resource.name); + assert.strictEqual(entries.length, 1); + assert(entries[0] instanceof PerformanceResourceTiming); + } + + clearResourceTimings(); + assert.strictEqual(performance.getEntries().length, 0); +} + +// Assert resource data based in timingInfo + +// default values +{ + const timingInfo = createTimingInfo({ finalConnectionTimingInfo: {} }); + const customGlobal = {}; + const requestedUrl = 'http://localhost:8080'; + const cacheMode = 'local'; + const initiatorType = 'fetch'; + const resource = markResourceTiming( + timingInfo, + requestedUrl, + initiatorType, + customGlobal, + cacheMode, + ); + + assert(resource instanceof PerformanceEntry); + assert(resource instanceof PerformanceResourceTiming); + + assert.strictEqual(resource.entryType, 'resource'); + assert.strictEqual(resource.name, requestedUrl); + assert.ok(typeof resource.cacheMode === 'undefined', 'cacheMode does not have a getter'); + assert.strictEqual(resource.startTime, timingInfo.startTime); + assert.strictEqual(resource.duration, 0); + assert.strictEqual(resource.workerStart, 0); + assert.strictEqual(resource.redirectStart, 0); + assert.strictEqual(resource.redirectEnd, 0); + assert.strictEqual(resource.fetchStart, 0); + assert.strictEqual(resource.domainLookupStart, 0); + assert.strictEqual(resource.domainLookupEnd, 0); + assert.strictEqual(resource.connectStart, 0); + assert.strictEqual(resource.connectEnd, 0); + assert.strictEqual(resource.secureConnectionStart, 0); + assert.deepStrictEqual(resource.nextHopProtocol, []); + assert.strictEqual(resource.requestStart, 0); + assert.strictEqual(resource.responseStart, 0); + assert.strictEqual(resource.responseEnd, 0); + assert.strictEqual(resource.encodedBodySize, 0); + assert.strictEqual(resource.decodedBodySize, 0); + assert.strictEqual(resource.transferSize, 0); + assert.deepStrictEqual(resource.toJSON(), { + name: requestedUrl, + entryType: 'resource', + startTime: 0, + duration: 0, + initiatorType, + nextHopProtocol: [], + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 0, + domainLookupStart: 0, + domainLookupEnd: 0, + connectStart: 0, + connectEnd: 0, + secureConnectionStart: 0, + requestStart: 0, + responseStart: 0, + responseEnd: 0, + transferSize: 0, + encodedBodySize: 0, + decodedBodySize: 0, + }); + + assert(resource instanceof PerformanceEntry); + assert(resource instanceof PerformanceResourceTiming); + + clearResourceTimings(); + const entries = performance.getEntries(); + assert.strictEqual(entries.length, 0); +} + +// custom getters math +{ + const timingInfo = createTimingInfo({ + endTime: 100, + startTime: 50, + encodedBodySize: 150, + }); + const customGlobal = {}; + const requestedUrl = 'http://localhost:8080'; + const cacheMode = ''; + const initiatorType = 'fetch'; + const resource = markResourceTiming( + timingInfo, + requestedUrl, + initiatorType, + customGlobal, + cacheMode, + ); + + assert(resource instanceof PerformanceEntry); + assert(resource instanceof PerformanceResourceTiming); + + assert.strictEqual(resource.entryType, 'resource'); + assert.strictEqual(resource.name, requestedUrl); + assert.ok(typeof resource.cacheMode === 'undefined', 'cacheMode does not have a getter'); + assert.strictEqual(resource.startTime, timingInfo.startTime); + // Duration should be the timingInfo endTime - startTime + assert.strictEqual(resource.duration, 50); + // TransferSize should be encodedBodySize + 300 when cacheMode is empty + assert.strictEqual(resource.transferSize, 450); + + assert(resource instanceof PerformanceEntry); + assert(resource instanceof PerformanceResourceTiming); + + clearResourceTimings(); + const entries = performance.getEntries(); + assert.strictEqual(entries.length, 0); +} + +// Using PerformanceObserver +{ + const obs = new PerformanceObserver(common.mustCall((list) => { + { + const entries = list.getEntries(); + assert.strictEqual(entries.length, 1); + assert(entries[0] instanceof PerformanceResourceTiming); + } + { + const entries = list.getEntriesByType('resource'); + assert.strictEqual(entries.length, 1); + assert(entries[0] instanceof PerformanceResourceTiming); + } + { + const entries = list.getEntriesByName('http://localhost:8080'); + assert.strictEqual(entries.length, 1); + assert(entries[0] instanceof PerformanceResourceTiming); + } + obs.disconnect(); + })); + obs.observe({ entryTypes: ['resource'] }); + + const timingInfo = createTimingInfo({ finalConnectionTimingInfo: {} }); + const customGlobal = {}; + const requestedUrl = 'http://localhost:8080'; + const cacheMode = 'local'; + const initiatorType = 'fetch'; + const resource = markResourceTiming( + timingInfo, + requestedUrl, + initiatorType, + customGlobal, + cacheMode, + ); + + assert(resource instanceof PerformanceEntry); + assert(resource instanceof PerformanceResourceTiming); + + clearResourceTimings(); + const entries = performance.getEntries(); + assert.strictEqual(entries.length, 0); +}