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
79 changes: 79 additions & 0 deletions doc/api/perf_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,54 @@
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 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.

### `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

Check warning on line 1130 in doc/api/perf_hooks.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
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 +1214,37 @@
(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'`.

### `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.

### `performanceResourceTiming.contentEncoding`

<!-- YAML
added: REPLACEME
-->

* Type: {string}

The content encoding of the fetched resource, such as `'gzip'` or `'br'`.

### `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
116 changes: 116 additions & 0 deletions test/parallel/test-perf-hooks-resourcetiming-attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'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 when no optional timing or body metadata is present.
{
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, '');
}

// responseStart falls back to the final response when no interim response timing was recorded.
{
const resource = markResourceTiming(createTimingInfo({
finalNetworkResponseStartTime: 123,
firstInterimNetworkResponseStartTime: 0,
}), {});

assert.strictEqual(resource.finalResponseHeadersStart, 123);
assert.strictEqual(resource.firstInterimResponseStart, 0);
assert.strictEqual(resource.responseStart, 123);
}

// 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();
67 changes: 67 additions & 0 deletions test/parallel/test-perf-hooks-resourcetiming-fetch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// The ESM common wrapper does not export localhostIPv4.
// eslint-disable-next-line node-core/require-common-first
import common from '../common/index.js';

import assert from 'node:assert';
import { once } from 'node:events';
import { createServer } from 'node:http';
import {
PerformanceResourceTiming,
performance,
} from 'node:perf_hooks';

// This test ensures that built-in fetch creates a PerformanceResourceTiming
// entry with the resource timing attributes exposed by Node.js.

const responseBody = 'Hello world';
const server = createServer(common.mustCall((req, res) => {
res.end(responseBody);
}));

server.listen(0, common.localhostIPv4);
await once(server, 'listening');

async function closeServer() {
if (server.listening) {
await new Promise((resolve) => server.close(resolve));
}
}

try {
performance.clearResourceTimings();

const url = `http://${common.localhostIPv4}:${server.address().port}/`;
const response = await fetch(url);
assert.strictEqual(await response.text(), responseBody);
await closeServer();

const entries = performance.getEntriesByName(url, 'resource');
assert.strictEqual(entries.length, 1);

const [entry] = entries;
assert(entry instanceof PerformanceResourceTiming);
assert.strictEqual(entry.initiatorType, 'fetch');
assert.strictEqual(entry.responseStatus, 200);
assert(entry.finalResponseHeadersStart > 0);
assert.strictEqual(entry.firstInterimResponseStart, 0);
assert.strictEqual(entry.responseStart, entry.finalResponseHeadersStart);
assert(entry.responseEnd >= entry.responseStart);
assert.strictEqual(entry.renderBlockingStatus, 'non-blocking');
assert.strictEqual(entry.contentType, '');
assert.strictEqual(entry.contentEncoding, '');

const json = entry.toJSON();
for (const name of [
'finalResponseHeadersStart',
'firstInterimResponseStart',
'responseStart',
'renderBlockingStatus',
'contentType',
'contentEncoding',
]) {
assert.strictEqual(json[name], entry[name], name);
}
} finally {
performance.clearResourceTimings();
await closeServer();
}
Loading
Loading