Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions doc/api/perf_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,55 @@ changes:
The high resolution millisecond timestamp representing the time immediately
before Node.js receives the first byte of the response from the server.

### `performanceResourceTiming.finalResponseHeadersStart`

<!-- YAML
added: REPLACEME
-->

* Type: {number}

The high resolution millisecond timestamp representing the time immediately
after Node.js receives the first byte of the headers of the final response,
as opposed to an interim response.

### `performanceResourceTiming.firstInterimResponseStart`

<!-- YAML
added: REPLACEME
-->

* Type: {number}

The high resolution millisecond timestamp representing the time immediately
after Node.js receives the first byte of the first interim response, such as
a `103 Early Hints` response. Node.js does not currently record interim
responses, so the property always returns 0.

### `performanceResourceTiming.responseStart`

<!-- YAML
added:
- v18.2.0
- v16.17.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65017
description: This property now returns `firstInterimResponseStart`
when it is non-zero.
- version: v19.0.0
pr-url: https://github.com/nodejs/node/pull/44483
description: This property getter must be called with the
`PerformanceResourceTiming` object as the receiver.
-->

* Type: {number}

The high resolution millisecond timestamp representing the time immediately
after Node.js receives the first byte of the response from the server. This
is `firstInterimResponseStart` when it is non-zero, and
`finalResponseHeadersStart` otherwise.

### `performanceResourceTiming.responseEnd`

<!-- YAML
Expand Down Expand Up @@ -1166,6 +1215,43 @@ 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.renderBlockingStatus`

<!-- YAML
added: REPLACEME
-->

* Type: {string}

The render blocking status of the resource. It is either `'blocking'` or
`'non-blocking'`. Resources fetched by Node.js are never render blocking, so
the property always returns `'non-blocking'`.

### `performanceResourceTiming.contentType`

<!-- YAML
added: REPLACEME
-->

* Type: {string}

The minimized MIME type of the content of the fetched resource, or an empty
string if it cannot be determined. Node.js does not currently populate this
field, so the property returns an empty string.

### `performanceResourceTiming.contentEncoding`

<!-- YAML
added: REPLACEME
-->

* Type: {string}

The content encoding, such as `'gzip'` or `'br'`, that was applied to the
fetched resource, or an empty string if none was applied or it cannot be
determined. Node.js does not currently populate this field, so the property
returns an empty string.

### `performanceResourceTiming.toJSON()`

<!-- YAML
Expand Down
41 changes: 40 additions & 1 deletion lib/internal/perf/resource_timing.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const { enqueue, bufferResourceTiming } = require('internal/perf/observe');
const { validateThisInternalField } = require('internal/validators');
const { kEnumerableProperty } = require('internal/util');

const kBodyInfo = Symbol('kBodyInfo');
const kCacheMode = Symbol('kCacheMode');
const kRequestedUrl = Symbol('kRequestedUrl');
const kTimingInfo = Symbol('kTimingInfo');
Expand Down Expand Up @@ -110,11 +111,22 @@ class PerformanceResourceTiming extends PerformanceEntry {
return this[kTimingInfo].finalNetworkRequestStartTime;
}

get responseStart() {
get finalResponseHeadersStart() {
validateThisInternalField(this, kTimingInfo, 'PerformanceResourceTiming');
return this[kTimingInfo].finalNetworkResponseStartTime;
}

get firstInterimResponseStart() {
validateThisInternalField(this, kTimingInfo, 'PerformanceResourceTiming');
return this[kTimingInfo].firstInterimNetworkResponseStartTime ?? 0;
}

get responseStart() {
validateThisInternalField(this, kTimingInfo, 'PerformanceResourceTiming');
return this[kTimingInfo].firstInterimNetworkResponseStartTime ||
this[kTimingInfo].finalNetworkResponseStartTime;
}

get responseEnd() {
validateThisInternalField(this, kTimingInfo, 'PerformanceResourceTiming');
return this[kTimingInfo].endTime;
Expand Down Expand Up @@ -148,6 +160,22 @@ class PerformanceResourceTiming extends PerformanceEntry {
return this[kResponseStatus];
}

get renderBlockingStatus() {
validateThisInternalField(this, kTimingInfo, 'PerformanceResourceTiming');
return this[kTimingInfo].renderBlocking === true ?
'blocking' : 'non-blocking';
}

get contentType() {
validateThisInternalField(this, kTimingInfo, 'PerformanceResourceTiming');
return this[kBodyInfo]?.contentType ?? '';
}

get contentEncoding() {
validateThisInternalField(this, kTimingInfo, 'PerformanceResourceTiming');
return this[kBodyInfo]?.contentEncoding ?? '';
}

toJSON() {
validateThisInternalField(this, kInitiatorType, 'PerformanceResourceTiming');
return {
Expand All @@ -167,13 +195,18 @@ class PerformanceResourceTiming extends PerformanceEntry {
connectEnd: this.connectEnd,
secureConnectionStart: this.secureConnectionStart,
requestStart: this.requestStart,
finalResponseHeadersStart: this.finalResponseHeadersStart,
firstInterimResponseStart: this.firstInterimResponseStart,
responseStart: this.responseStart,
responseEnd: this.responseEnd,
transferSize: this.transferSize,
encodedBodySize: this.encodedBodySize,
decodedBodySize: this.decodedBodySize,
deliveryType: this.deliveryType,
responseStatus: this.responseStatus,
renderBlockingStatus: this.renderBlockingStatus,
contentType: this.contentType,
contentEncoding: this.contentEncoding,
};
}
}
Expand All @@ -191,13 +224,18 @@ ObjectDefineProperties(PerformanceResourceTiming.prototype, {
connectEnd: kEnumerableProperty,
secureConnectionStart: kEnumerableProperty,
requestStart: kEnumerableProperty,
finalResponseHeadersStart: kEnumerableProperty,
firstInterimResponseStart: kEnumerableProperty,
responseStart: kEnumerableProperty,
responseEnd: kEnumerableProperty,
transferSize: kEnumerableProperty,
encodedBodySize: kEnumerableProperty,
decodedBodySize: kEnumerableProperty,
deliveryType: kEnumerableProperty,
responseStatus: kEnumerableProperty,
renderBlockingStatus: kEnumerableProperty,
contentType: kEnumerableProperty,
contentEncoding: kEnumerableProperty,
toJSON: kEnumerableProperty,
[SymbolToStringTag]: {
__proto__: null,
Expand All @@ -224,6 +262,7 @@ function createPerformanceResourceTiming(
// The spec doesn't say to validate it in the class construction.
resourceTiming[kTimingInfo] = timingInfo;
resourceTiming[kCacheMode] = cacheMode;
resourceTiming[kBodyInfo] = bodyInfo;
resourceTiming[kDeliveryType] = deliveryType;
resourceTiming[kResponseStatus] = responseStatus;

Expand Down
105 changes: 105 additions & 0 deletions test/parallel/test-perf-hooks-resourcetiming-attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use strict';

require('../common');
const assert = require('assert');
const {
PerformanceResourceTiming,
performance,
} = require('perf_hooks');

// Covers the IDL attributes finalResponseHeadersStart,
// firstInterimResponseStart, renderBlockingStatus, contentType and contentEncoding,
// and the spec requirement that responseStart prefers a non-zero
// firstInterimResponseStart over finalResponseHeadersStart.

function createTimingInfo(overrides = {}) {
return {
startTime: 0,
redirectStartTime: 0,
redirectEndTime: 0,
postRedirectStartTime: 0,
finalServiceWorkerStartTime: 0,
finalNetworkRequestStartTime: 0,
finalNetworkResponseStartTime: 0,
endTime: 0,
encodedBodySize: 0,
decodedBodySize: 0,
finalConnectionTimingInfo: null,
...overrides,
};
}

function markResourceTiming(timingInfo, bodyInfo) {
return performance.markResourceTiming(
timingInfo,
'http://localhost:8080',
'fetch',
{},
'',
bodyInfo,
200,
'',
);
}

// Default values with an empty body info, mirroring what the fetch
// implementation passes for a response with no body metadata.
{
const resource = markResourceTiming(createTimingInfo(), {});

assert.strictEqual(resource.finalResponseHeadersStart, 0);
assert.strictEqual(resource.firstInterimResponseStart, 0);
assert.strictEqual(resource.responseStart, 0);
assert.strictEqual(resource.renderBlockingStatus, 'non-blocking');
assert.strictEqual(resource.contentType, '');
assert.strictEqual(resource.contentEncoding, '');
}

// Values reflected from timing info and body info.
{
const timingInfo = createTimingInfo({
finalNetworkResponseStartTime: 123,
firstInterimNetworkResponseStartTime: 45,
renderBlocking: true,
});
const bodyInfo = {
contentType: 'text/html',
contentEncoding: 'gzip',
};
const resource = markResourceTiming(timingInfo, bodyInfo);

assert.strictEqual(resource.finalResponseHeadersStart, 123);
assert.strictEqual(resource.firstInterimResponseStart, 45);
assert.strictEqual(resource.responseStart, 45);
assert.strictEqual(resource.renderBlockingStatus, 'blocking');
assert.strictEqual(resource.contentType, 'text/html');
assert.strictEqual(resource.contentEncoding, 'gzip');

const json = resource.toJSON();
assert.strictEqual(json.finalResponseHeadersStart, 123);
assert.strictEqual(json.firstInterimResponseStart, 45);
assert.strictEqual(json.responseStart, 45);
assert.strictEqual(json.renderBlockingStatus, 'blocking');
assert.strictEqual(json.contentType, 'text/html');
assert.strictEqual(json.contentEncoding, 'gzip');
}

// The attributes are enumerable getters on the prototype and perform a
// brand check like the other PerformanceResourceTiming attributes.
for (const name of [
'finalResponseHeadersStart',
'firstInterimResponseStart',
'renderBlockingStatus',
'contentType',
'contentEncoding',
]) {
const desc = Object.getOwnPropertyDescriptor(
PerformanceResourceTiming.prototype, name);
assert.strictEqual(desc.enumerable, true, name);
assert.strictEqual(typeof desc.get, 'function', name);
assert.throws(() => desc.get.call({}), {
code: 'ERR_INVALID_THIS',
}, name);
}

performance.clearResourceTimings();
19 changes: 17 additions & 2 deletions test/parallel/test-perf-hooks-resourcetiming.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,18 @@ function createTimingInfo({
connectEnd: 0,
secureConnectionStart: 0,
requestStart: 0,
finalResponseHeadersStart: 0,
firstInterimResponseStart: 0,
responseStart: 0,
responseEnd: 0,
transferSize: 0,
encodedBodySize: 0,
decodedBodySize: 0,
responseStatus: 200,
deliveryType: '',
renderBlockingStatus: 'non-blocking',
contentType: '',
contentEncoding: '',
});
assert.strictEqual(util.inspect(performance.getEntries()), `[
PerformanceResourceTiming {
Expand All @@ -200,13 +205,18 @@ function createTimingInfo({
connectEnd: 0,
secureConnectionStart: 0,
requestStart: 0,
finalResponseHeadersStart: 0,
firstInterimResponseStart: 0,
responseStart: 0,
responseEnd: 0,
transferSize: 0,
encodedBodySize: 0,
decodedBodySize: 0,
deliveryType: '',
responseStatus: 200
responseStatus: 200,
renderBlockingStatus: 'non-blocking',
contentType: '',
contentEncoding: ''
}
]`);
assert.strictEqual(util.inspect(resource), `PerformanceResourceTiming {
Expand All @@ -226,13 +236,18 @@ function createTimingInfo({
connectEnd: 0,
secureConnectionStart: 0,
requestStart: 0,
finalResponseHeadersStart: 0,
firstInterimResponseStart: 0,
responseStart: 0,
responseEnd: 0,
transferSize: 0,
encodedBodySize: 0,
decodedBodySize: 0,
deliveryType: '',
responseStatus: 200
responseStatus: 200,
renderBlockingStatus: 'non-blocking',
contentType: '',
contentEncoding: ''
}`);

assert(resource instanceof PerformanceEntry);
Expand Down
12 changes: 1 addition & 11 deletions test/wpt/status/resource-timing.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,15 @@
"idlharness.any.js": {
"fail": {
"expected": [
"PerformanceResourceTiming interface: attribute firstInterimResponseStart",
"PerformanceResourceTiming interface: attribute finalResponseHeadersStart",
"PerformanceResourceTiming interface: resource must inherit property \"finalResponseHeadersStart\" with the proper type",
"PerformanceResourceTiming interface: attribute renderBlockingStatus",
"PerformanceResourceTiming interface: attribute contentType",
"PerformanceResourceTiming interface: resource must inherit property \"firstInterimResponseStart\" with the proper type",
"PerformanceResourceTiming interface: resource must inherit property \"renderBlockingStatus\" with the proper type",
"PerformanceResourceTiming interface: resource must inherit property \"contentType\" with the proper type",
"PerformanceResourceTiming interface: default toJSON operation on resource",
"PerformanceResourceTiming interface: attribute workerRouterEvaluationStart",
"PerformanceResourceTiming interface: attribute workerCacheLookupStart",
"PerformanceResourceTiming interface: attribute workerMatchedRouterSource",
"PerformanceResourceTiming interface: attribute workerFinalRouterSource",
"PerformanceResourceTiming interface: attribute contentEncoding",
"PerformanceResourceTiming interface: resource must inherit property \"workerRouterEvaluationStart\" with the proper type",
"PerformanceResourceTiming interface: resource must inherit property \"workerCacheLookupStart\" with the proper type",
"PerformanceResourceTiming interface: resource must inherit property \"workerMatchedRouterSource\" with the proper type",
"PerformanceResourceTiming interface: resource must inherit property \"workerFinalRouterSource\" with the proper type",
"PerformanceResourceTiming interface: resource must inherit property \"contentEncoding\" with the proper type"
"PerformanceResourceTiming interface: resource must inherit property \"workerFinalRouterSource\" with the proper type"
]
}
}
Expand Down
Loading