Skip to content
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.onMessageBegin()
}

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 @@ -263,6 +263,10 @@ class Request {
}
}

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

onHeaders (statusCode, headers, resume, statusText) {
assert(!this.aborted)
assert(!this.completed)
Expand Down
14 changes: 13 additions & 1 deletion lib/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const {
isomorphicEncode,
urlIsLocal,
urlIsHttpHttpsScheme,
urlHasHttpsScheme
urlHasHttpsScheme,
clampAndCoursenConnectionTimingInfo
} = require('./util')
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
const assert = require('assert')
Expand Down Expand Up @@ -1970,6 +1971,9 @@ 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 {
Expand All @@ -1978,6 +1982,14 @@ async function httpNetworkFetch (
}
},

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

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

onHeaders (status, headersList, resume, statusText) {
if (status < 200) {
return
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 @@ -1030,6 +1060,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))
})