Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
* **onConnect** `(abort: () => void, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
* **onError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
* **onUpgrade** `(statusCode: number, headers: Buffer[], socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
* **onResponseStarted** `() => void` (optional) - Invoked when response is received, before headers have been read.
* **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void, statusText: string) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
* **onData** `(chunk: Buffer) => boolean` - Invoked when response payload data is received. Not required for `upgrade` requests.
* **onComplete** `(trailers: Buffer[]) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
Expand Down
1 change: 1 addition & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,7 @@ class Parser {
if (!request) {
return -1
}
request.onResponseStarted()
}

onHeaderField (buf) {
Expand Down
4 changes: 4 additions & 0 deletions lib/core/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ class Request {
}
}

onResponseStarted () {
return this[kHandler].onResponseStarted?.()
}

onHeaders (statusCode, headers, resume, statusText) {
assert(!this.aborted)
assert(!this.completed)
Expand Down
10 changes: 10 additions & 0 deletions lib/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const {
urlIsLocal,
urlIsHttpHttpsScheme,
urlHasHttpsScheme,
clampAndCoursenConnectionTimingInfo,
simpleRangeHeaderValue,
buildContentRange
} = require('./util')
Expand Down Expand Up @@ -2098,12 +2099,21 @@ async function httpNetworkFetch (
// TODO (fix): Do we need connection here?
const { connection } = fetchParams.controller

// TODO: pass connection timing info
timingInfo.finalConnectionTimingInfo = clampAndCoursenConnectionTimingInfo(undefined, timingInfo.postRedirectStartTime, fetchParams.crossOriginIsolatedCapability)

if (connection.destroyed) {
abort(new DOMException('The operation was aborted.', 'AbortError'))
} else {
fetchParams.controller.on('terminated', abort)
this.abort = connection.abort = abort
}

timingInfo.finalNetworkRequestStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
},

onResponseStarted () {
timingInfo.finalNetworkResponseStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
},

onHeaders (status, headersList, resume, statusText) {
Expand Down
33 changes: 32 additions & 1 deletion lib/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,39 @@ function appendRequestOriginHeader (request) {
}
}

// https://w3c.github.io/hr-time/#dfn-coarsen-time
function coarsenTime (timestamp, crossOriginIsolatedCapability) {
// TODO
return timestamp
}

// https://fetch.spec.whatwg.org/#clamp-and-coarsen-connection-timing-info
function clampAndCoursenConnectionTimingInfo (connectionTimingInfo, defaultStartTime, crossOriginIsolatedCapability) {
if (!connectionTimingInfo?.startTime || connectionTimingInfo.startTime < defaultStartTime) {
return {
domainLookupStartTime: defaultStartTime,
domainLookupEndTime: defaultStartTime,
connectionStartTime: defaultStartTime,
connectionEndTime: defaultStartTime,
secureConnectionStartTime: defaultStartTime,
ALPNNegotiatedProtocol: connectionTimingInfo?.ALPNNegotiatedProtocol
}
}

return {
domainLookupStartTime: coarsenTime(connectionTimingInfo.domainLookupStartTime, crossOriginIsolatedCapability),
domainLookupEndTime: coarsenTime(connectionTimingInfo.domainLookupEndTime, crossOriginIsolatedCapability),
connectionStartTime: coarsenTime(connectionTimingInfo.connectionStartTime, crossOriginIsolatedCapability),
connectionEndTime: coarsenTime(connectionTimingInfo.connectionEndTime, crossOriginIsolatedCapability),
secureConnectionStartTime: coarsenTime(connectionTimingInfo.secureConnectionStartTime, crossOriginIsolatedCapability),
ALPNNegotiatedProtocol: connectionTimingInfo.ALPNNegotiatedProtocol
}
}

// https://w3c.github.io/hr-time/#dfn-coarsened-shared-current-time
function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
// TODO
return performance.now()
return coarsenTime(performance.now(), crossOriginIsolatedCapability)
}

// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
Expand Down Expand Up @@ -1186,6 +1216,7 @@ module.exports = {
ReadableStreamFrom,
toUSVString,
tryUpgradeRequestToAPotentiallyTrustworthyURL,
clampAndCoursenConnectionTimingInfo,
coarsenedSharedCurrentTime,
determineRequestsReferrer,
makePolicyContainer,
Expand Down
66 changes: 66 additions & 0 deletions test/fetch/resource-timing.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,69 @@ test('should include encodedBodySize in performance entry', { skip }, (t) => {

t.teardown(server.close.bind(server))
})

test('timing entries should be in order', { skip }, (t) => {
t.plan(13)
const obs = new PerformanceObserver(list => {
const [entry] = list.getEntries()

t.ok(entry.startTime > 0)
t.ok(entry.fetchStart >= entry.startTime)
t.ok(entry.domainLookupStart >= entry.fetchStart)
t.ok(entry.domainLookupEnd >= entry.domainLookupStart)
t.ok(entry.connectStart >= entry.domainLookupEnd)
t.ok(entry.connectEnd >= entry.connectStart)
t.ok(entry.requestStart >= entry.connectEnd)
t.ok(entry.responseStart >= entry.requestStart)
t.ok(entry.responseEnd >= entry.responseStart)
t.ok(entry.duration > 0)

t.ok(entry.redirectStart === 0)
t.ok(entry.redirectEnd === 0)

obs.disconnect()
performance.clearResourceTimings()
})

obs.observe({ entryTypes: ['resource'] })

const server = createServer((req, res) => {
res.end('ok')
}).listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}/redirect`)
t.strictSame('ok', await body.text())
})

t.teardown(server.close.bind(server))
})

test('redirect timing entries should be included when redirecting', { skip }, (t) => {
t.plan(4)
const obs = new PerformanceObserver(list => {
const [entry] = list.getEntries()

t.ok(entry.redirectStart >= entry.startTime)
t.ok(entry.redirectEnd >= entry.redirectStart)
t.ok(entry.connectStart >= entry.redirectEnd)

obs.disconnect()
performance.clearResourceTimings()
})

obs.observe({ entryTypes: ['resource'] })

const server = createServer((req, res) => {
if (req.url === '/redirect') {
res.statusCode = 307
res.setHeader('location', '/redirect/')
res.end()
return
}
res.end('ok')
}).listen(0, async () => {
const body = await fetch(`http://localhost:${server.address().port}/redirect`)
t.strictSame('ok', await body.text())
})

t.teardown(server.close.bind(server))
})
2 changes: 2 additions & 0 deletions types/dispatcher.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ declare namespace Dispatcher {
onError?(err: Error): void;
/** Invoked when request is upgraded either due to a `Upgrade` header or `CONNECT` method. */
onUpgrade?(statusCode: number, headers: Buffer[] | string[] | null, socket: Duplex): void;
/** Invoked when response is received, before headers have been read. **/
onResponseStarted?(): void;
/** Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. */
onHeaders?(statusCode: number, headers: Buffer[] | string[] | null, resume: () => void, statusText: string): boolean;
/** Invoked when response payload data is received. */
Expand Down