diff --git a/browsers.json b/browsers.json index ecbde41e53..5af39f2e3b 100644 --- a/browsers.json +++ b/browsers.json @@ -8,7 +8,7 @@ }, { "name": "firefox", - "revision": "1194", + "revision": "1196", "download": true }, { diff --git a/docs/api.md b/docs/api.md index 555ae47dee..b359b9fc4c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3815,6 +3815,7 @@ If request gets a 'redirect' response, the request is successfully finished with - [request.redirectedTo()](#requestredirectedto) - [request.resourceType()](#requestresourcetype) - [request.response()](#requestresponse) +- [request.timing()](#requesttiming) - [request.url()](#requesturl) @@ -3892,6 +3893,29 @@ ResourceType will be one of the following: `document`, `stylesheet`, `image`, `m #### request.response() - returns: <[Promise]<[null]|[Response]>> A matching [Response] object, or `null` if the response was not received due to error. +#### request.timing() +- returns: <[Object]> + - `startTime` <[number]> Request start time in milliseconds elapsed since January 1, 1970 00:00:00 UTC + - `domainLookupStart` <[number]> Time immediately before the browser starts the domain name lookup for the resource. The value is given in milliseconds relative to `startTime`, -1 if not available. + - `domainLookupEnd` <[number]> Time immediately after the browser starts the domain name lookup for the resource. The value is given in milliseconds relative to `startTime`, -1 if not available. + - `connectStart` <[number]> Time immediately before the user agent starts establishing the connection to the server to retrieve the resource. The value is given in milliseconds relative to `startTime`, -1 if not available. + - `secureConnectionStart` <[number]> immediately before the browser starts the handshake process to secure the current connection. The value is given in milliseconds relative to `startTime`, -1 if not available. + - `connectEnd` <[number]> Time immediately before the user agent starts establishing the connection to the server to retrieve the resource. The value is given in milliseconds relative to `startTime`, -1 if not available. + - `requestStart` <[number]> Time immediately before the browser starts requesting the resource from the server, cache, or local resource. The value is given in milliseconds relative to `startTime`, -1 if not available. + - `responseStart` <[number]> immediately after the browser starts requesting the resource from the server, cache, or local resource. The value is given in milliseconds relative to `startTime`, -1 if not available. + - `responseEnd` <[number]> Time immediately after the browser receives the last byte of the resource or immediately before the transport connection is closed, whichever comes first. The value is given in milliseconds relative to `startTime`, -1 if not available. +}; + +Returns resource timing information for given request. Most of the timing values become available upon the response, `responseEnd` becomes available when request finishes. Find more information at [Resource Timing API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming). + +```js +const [request] = await Promise.all([ + page.waitForEvent('requestfinished'), + page.goto(httpsServer.EMPTY_PAGE) +]); +console.log(request.timing()); +``` + #### request.url() - returns: <[string]> URL of the request. diff --git a/src/client/network.ts b/src/client/network.ts index 67593a9fa0..218a2b2fb6 100644 --- a/src/client/network.ts +++ b/src/client/network.ts @@ -53,6 +53,7 @@ export class Request extends ChannelOwner void; +export type ResourceTiming = { + startTime: number; + domainLookupStart: number; + domainLookupEnd: number; + connectStart: number; + secureConnectionStart: number; + connectEnd: number; + requestStart: number; + responseStart: number; + responseEnd: number; +}; + export class Response extends ChannelOwner { private _headers: Headers; + private _request: Request; static from(response: channels.ResponseChannel): Response { return (response as any)._object; @@ -228,6 +257,8 @@ export class Response extends ChannelOwner this.emit(Events.Page.PageError, parseError(error))); this._channel.on('popup', ({ page }) => this.emit(Events.Page.Popup, Page.from(page))); this._channel.on('request', ({ request }) => this.emit(Events.Page.Request, Request.from(request))); - this._channel.on('requestFailed', ({ request, failureText }) => this._onRequestFailed(Request.from(request), failureText)); - this._channel.on('requestFinished', ({ request }) => this.emit(Events.Page.RequestFinished, Request.from(request))); + this._channel.on('requestFailed', ({ request, failureText, responseEndTiming }) => this._onRequestFailed(Request.from(request), responseEndTiming, failureText)); + this._channel.on('requestFinished', ({ request, responseEndTiming }) => this._onRequestFinished(Request.from(request), responseEndTiming)); this._channel.on('response', ({ response }) => this.emit(Events.Page.Response, Response.from(response))); this._channel.on('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request))); this._channel.on('video', ({ relativePath }) => this.video()!._setRelativePath(relativePath)); @@ -138,11 +138,19 @@ export class Page extends ChannelOwner i page.on(Page.Events.Request, request => this._dispatchEvent('request', { request: RequestDispatcher.from(this._scope, request) })); page.on(Page.Events.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', { request: RequestDispatcher.from(this._scope, request), - failureText: request._failureText + failureText: request._failureText, + responseEndTiming: request._responseEndTiming + })); + page.on(Page.Events.RequestFinished, (request: Request) => this._dispatchEvent('requestFinished', { + request: RequestDispatcher.from(scope, request), + responseEndTiming: request._responseEndTiming })); - page.on(Page.Events.RequestFinished, request => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request) })); page.on(Page.Events.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) })); page.on(Page.Events.VideoStarted, (video: Video) => this._dispatchEvent('video', { relativePath: video._relativePath })); page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) })); diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index ce79d5171d..bbcc6c2b4e 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -758,9 +758,11 @@ export type PageRequestEvent = { export type PageRequestFailedEvent = { request: RequestChannel, failureText?: string, + responseEndTiming: number, }; export type PageRequestFinishedEvent = { request: RequestChannel, + responseEndTiming: number, }; export type PageResponseEvent = { response: ResponseChannel, @@ -2133,6 +2135,17 @@ export type RouteFulfillOptions = { }; export type RouteFulfillResult = void; +export type ResourceTiming = { + startTime: number, + domainLookupStart: number, + domainLookupEnd: number, + connectStart: number, + secureConnectionStart: number, + connectEnd: number, + requestStart: number, + responseStart: number, +}; + // ----------- Response ----------- export type ResponseInitializer = { request: RequestChannel, @@ -2143,6 +2156,7 @@ export type ResponseInitializer = { name: string, value: string, }[], + timing: ResourceTiming, }; export interface ResponseChannel extends Channel { body(params?: ResponseBodyParams, metadata?: Metadata): Promise; diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 4e306bd725..ef214aa765 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -920,10 +920,12 @@ Page: parameters: request: Request failureText: string? + responseEndTiming: number requestFinished: parameters: request: Request + responseEndTiming: number response: parameters: @@ -1789,6 +1791,17 @@ Route: isBase64: boolean? +ResourceTiming: + type: object + properties: + startTime: number + domainLookupStart: number + domainLookupEnd: number + connectStart: number + secureConnectionStart: number + connectEnd: number + requestStart: number + responseStart: number Response: type: interface @@ -1805,6 +1818,8 @@ Response: properties: name: string value: string + timing: ResourceTiming + commands: @@ -1817,7 +1832,6 @@ Response: error: string? - ConsoleMessage: type: interface diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 4b09e58b52..5f2088bb7d 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -837,6 +837,16 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { body: tOptional(tString), isBase64: tOptional(tBoolean), }); + scheme.ResourceTiming = tObject({ + startTime: tNumber, + domainLookupStart: tNumber, + domainLookupEnd: tNumber, + connectStart: tNumber, + secureConnectionStart: tNumber, + connectEnd: tNumber, + requestStart: tNumber, + responseStart: tNumber, + }); scheme.ResponseBodyParams = tOptional(tObject({})); scheme.ResponseFinishedParams = tOptional(tObject({})); scheme.BindingCallRejectParams = tObject({ diff --git a/src/server/chromium/crNetworkManager.ts b/src/server/chromium/crNetworkManager.ts index 7b4ddec722..c28dc469ab 100644 --- a/src/server/chromium/crNetworkManager.ts +++ b/src/server/chromium/crNetworkManager.ts @@ -173,7 +173,7 @@ export class CRNetworkManager { const request = this._requestIdToRequest.get(requestWillBeSentEvent.requestId); // If we connect late to the target, we could have missed the requestWillBeSent event. if (request) { - this._handleRequestRedirect(request, requestWillBeSentEvent.redirectResponse); + this._handleRequestRedirect(request, requestWillBeSentEvent.redirectResponse, requestWillBeSentEvent.timestamp); redirectedFrom = request.request; } } @@ -240,12 +240,37 @@ export class CRNetworkManager { const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId }); return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); }; - return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), getResponseBody); + const timingPayload = responsePayload.timing!; + let timing: network.ResourceTiming; + if (timingPayload) { + timing = { + startTime: (timingPayload.requestTime - request._timestamp + request._wallTime) * 1000, + domainLookupStart: timingPayload.dnsStart, + domainLookupEnd: timingPayload.dnsEnd, + connectStart: timingPayload.connectStart, + secureConnectionStart: timingPayload.sslStart, + connectEnd: timingPayload.connectEnd, + requestStart: timingPayload.sendStart, + responseStart: timingPayload.receiveHeadersEnd, + }; + } else { + timing = { + startTime: request._wallTime * 1000, + domainLookupStart: -1, + domainLookupEnd: -1, + connectStart: -1, + secureConnectionStart: -1, + connectEnd: -1, + requestStart: -1, + responseStart: -1, + }; + } + return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody); } - _handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response) { + _handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number) { const response = this._createResponse(request, responsePayload); - response._requestFinished('Response body is unavailable for redirect responses'); + response._requestFinished((timestamp - request._timestamp) * 1000, 'Response body is unavailable for redirect responses'); this._requestIdToRequest.delete(request._requestId); if (request._interceptionId) this._attemptedAuthentications.delete(request._interceptionId); @@ -275,7 +300,7 @@ export class CRNetworkManager { // event from protocol. @see https://crbug.com/883475 const response = request.request._existingResponse(); if (response) - response._requestFinished(); + response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp)); this._requestIdToRequest.delete(request._requestId); if (request._interceptionId) this._attemptedAuthentications.delete(request._interceptionId); @@ -292,7 +317,7 @@ export class CRNetworkManager { return; const response = request.request._existingResponse(); if (response) - response._requestFinished(); + response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp)); this._requestIdToRequest.delete(request._requestId); if (request._interceptionId) this._attemptedAuthentications.delete(request._interceptionId); @@ -324,6 +349,8 @@ class InterceptableRequest implements network.RouteDelegate { _interceptionId: string | null; _documentId: string | undefined; private _client: CRSession; + _timestamp: number; + _wallTime: number; constructor(options: { client: CRSession; @@ -336,6 +363,8 @@ class InterceptableRequest implements network.RouteDelegate { }) { const { client, frame, documentId, allowInterception, requestWillBeSentEvent, requestPausedEvent, redirectedFrom } = options; this._client = client; + this._timestamp = requestWillBeSentEvent.timestamp; + this._wallTime = requestWillBeSentEvent.wallTime; this._requestId = requestWillBeSentEvent.requestId; this._interceptionId = requestPausedEvent && requestPausedEvent.requestId; this._documentId = documentId; diff --git a/src/server/firefox/ffNetworkManager.ts b/src/server/firefox/ffNetworkManager.ts index 0dd62d910b..772b38ca28 100644 --- a/src/server/firefox/ffNetworkManager.ts +++ b/src/server/firefox/ffNetworkManager.ts @@ -28,6 +28,7 @@ export class FFNetworkManager { private _requests: Map; private _page: Page; private _eventListeners: RegisteredListener[]; + private _startTime = 0; constructor(session: FFSession, page: Page) { this._session = session; @@ -75,7 +76,19 @@ export class FFNetworkManager { throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`); return Buffer.from(response.base64body, 'base64'); }; - const response = new network.Response(request.request, event.status, event.statusText, event.headers, getResponseBody); + + this._startTime = event.timing.startTime; + const timing = { + startTime: this._startTime / 1000, + domainLookupStart: this._relativeTiming(event.timing.domainLookupStart), + domainLookupEnd: this._relativeTiming(event.timing.domainLookupEnd), + connectStart: this._relativeTiming(event.timing.connectStart), + secureConnectionStart: this._relativeTiming(event.timing.secureConnectionStart), + connectEnd: this._relativeTiming(event.timing.connectEnd), + requestStart: this._relativeTiming(event.timing.requestStart), + responseStart: this._relativeTiming(event.timing.responseStart), + }; + const response = new network.Response(request.request, event.status, event.statusText, event.headers, timing, getResponseBody); this._page._frameManager.requestReceivedResponse(response); } @@ -87,10 +100,10 @@ export class FFNetworkManager { // Keep redirected requests in the map for future reference as redirectedFrom. const isRedirected = response.status() >= 300 && response.status() <= 399; if (isRedirected) { - response._requestFinished('Response body is unavailable for redirect responses'); + response._requestFinished(this._relativeTiming(event.responseEndTime), 'Response body is unavailable for redirect responses'); } else { this._requests.delete(request._id); - response._requestFinished(); + response._requestFinished(this._relativeTiming(event.responseEndTime)); } this._page._frameManager.requestFinished(request.request); } @@ -102,10 +115,16 @@ export class FFNetworkManager { this._requests.delete(request._id); const response = request.request._existingResponse(); if (response) - response._requestFinished(); + response._requestFinished(-1); request.request._setFailureText(event.errorCode); this._page._frameManager.requestFailed(request.request, event.errorCode === 'NS_BINDING_ABORTED'); } + + _relativeTiming(time: number): number { + if (!time) + return -1; + return (time - this._startTime) / 1000; + } } const causeToResourceType: {[key: string]: string} = { @@ -146,7 +165,6 @@ class InterceptableRequest implements network.RouteDelegate { constructor(session: FFSession, frame: frames.Frame, redirectedFrom: InterceptableRequest | null, payload: Protocol.Network.requestWillBeSentPayload) { this._id = payload.requestId; this._session = session; - let postDataBuffer = null; if (payload.postData) postDataBuffer = Buffer.from(payload.postData, 'base64'); diff --git a/src/server/firefox/protocol.ts b/src/server/firefox/protocol.ts index fb4ff25469..ee554c466d 100644 --- a/src/server/firefox/protocol.ts +++ b/src/server/firefox/protocol.ts @@ -776,6 +776,16 @@ export module Protocol { validFrom: number; validTo: number; }; + export type ResourceTiming = { + startTime: number; + domainLookupStart: number; + domainLookupEnd: number; + connectStart: number; + secureConnectionStart: number; + connectEnd: number; + requestStart: number; + responseStart: number; + }; export type requestWillBeSentPayload = { frameId?: string; requestId: string; @@ -810,9 +820,20 @@ export module Protocol { name: string; value: string; }[]; + timing: { + startTime: number; + domainLookupStart: number; + domainLookupEnd: number; + connectStart: number; + secureConnectionStart: number; + connectEnd: number; + requestStart: number; + responseStart: number; + }; } export type requestFinishedPayload = { requestId: string; + responseEndTime: number; } export type requestFailedPayload = { requestId: string; diff --git a/src/server/helper.ts b/src/server/helper.ts index d52ec07087..4aa0faeab9 100644 --- a/src/server/helper.ts +++ b/src/server/helper.ts @@ -102,6 +102,14 @@ class Helper { progress.cleanupWhenAborted(dispose); return { promise, dispose }; } + + static secondsToRoundishMillis(value: number): number { + return ((value * 1000000) | 0) / 1000; + } + + static millisToRoundishMillis(value: number): number { + return ((value * 1000) | 0) / 1000; + } } export const helper = Helper; diff --git a/src/server/network.ts b/src/server/network.ts index cac8663b22..2939a63817 100644 --- a/src/server/network.ts +++ b/src/server/network.ts @@ -81,6 +81,7 @@ export class Request { private _frame: frames.Frame; private _waitForResponsePromise: Promise; private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {}; + _responseEndTiming = -1; constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined, url: string, resourceType: string, method: string, postData: Buffer | null, headers: types.HeadersArray) { @@ -211,6 +212,17 @@ export type RouteHandler = (route: Route, request: Request) => void; type GetResponseBodyCallback = () => Promise; +export type ResourceTiming = { + startTime: number; + domainLookupStart: number; + domainLookupEnd: number; + connectStart: number; + secureConnectionStart: number; + connectEnd: number; + requestStart: number; + responseStart: number; +}; + export class Response { private _request: Request; private _contentPromise: Promise | null = null; @@ -221,9 +233,11 @@ export class Response { private _url: string; private _headers: types.HeadersArray; private _getResponseBodyCallback: GetResponseBodyCallback; + private _timing: ResourceTiming; - constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, getResponseBodyCallback: GetResponseBodyCallback) { + constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback) { this._request = request; + this._timing = timing; this._status = status; this._statusText = statusText; this._url = request.url(); @@ -235,7 +249,8 @@ export class Response { this._request._setResponse(this); } - _requestFinished(error?: string) { + _requestFinished(responseEndTiming: number, error?: string) { + this._request._responseEndTiming = Math.max(responseEndTiming, this._timing.responseStart); this._finishedPromiseCallback({ error }); } @@ -259,6 +274,10 @@ export class Response { return this._finishedPromise.then(({ error }) => error ? new Error(error) : null); } + timing(): ResourceTiming { + return this._timing; + } + body(): Promise { if (!this._contentPromise) { this._contentPromise = this._finishedPromise.then(async ({ error }) => { diff --git a/src/server/webkit/wkInterceptableRequest.ts b/src/server/webkit/wkInterceptableRequest.ts index b73abb885c..a05a993952 100644 --- a/src/server/webkit/wkInterceptableRequest.ts +++ b/src/server/webkit/wkInterceptableRequest.ts @@ -46,6 +46,8 @@ export class WKInterceptableRequest implements network.RouteDelegate { _interceptedCallback: () => void = () => {}; private _interceptedPromise: Promise; readonly _allowInterception: boolean; + _timestamp: number; + _wallTime: number; constructor(session: WKSession, allowInterception: boolean, frame: frames.Frame, event: Protocol.Network.requestWillBeSentPayload, redirectedFrom: network.Request | null, documentId: string | undefined) { this._session = session; @@ -53,6 +55,8 @@ export class WKInterceptableRequest implements network.RouteDelegate { this._allowInterception = allowInterception; const resourceType = event.type ? event.type.toLowerCase() : (redirectedFrom ? redirectedFrom.resourceType() : 'other'); let postDataBuffer = null; + this._timestamp = event.timestamp; + this._wallTime = event.walltime * 1000; if (event.request.postData) postDataBuffer = Buffer.from(event.request.postData, 'binary'); this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, event.request.url, @@ -107,6 +111,31 @@ export class WKInterceptableRequest implements network.RouteDelegate { const response = await this._session.send('Network.getResponseBody', { requestId: this._requestId }); return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); }; - return new network.Response(this.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), getResponseBody); + const timingPayload = responsePayload.timing; + const timing: network.ResourceTiming = { + startTime: this._wallTime, + domainLookupStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.domainLookupStart) : -1, + domainLookupEnd: timingPayload ? wkMillisToRoundishMillis(timingPayload.domainLookupEnd) : -1, + connectStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.connectStart) : -1, + secureConnectionStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.secureConnectionStart) : -1, + connectEnd: timingPayload ? wkMillisToRoundishMillis(timingPayload.connectEnd) : -1, + requestStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.requestStart) : -1, + responseStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.responseStart) : -1, + }; + return new network.Response(this.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody); } } + +function wkMillisToRoundishMillis(value: number): number { + // WebKit uses -1000 for unavailable. + if (value === -1000) + return -1; + + // WebKit has a bug, instead of -1 it sends -1000 to be in ms. + if (value < 0) { + // DNS can start before request start on Mac Network Stack + return 0; + } + + return ((value * 1000) | 0) / 1000; +} diff --git a/src/server/webkit/wkPage.ts b/src/server/webkit/wkPage.ts index 3a0d8edde3..0c01a13dfa 100644 --- a/src/server/webkit/wkPage.ts +++ b/src/server/webkit/wkPage.ts @@ -878,7 +878,7 @@ export class WKPage implements PageDelegate { const request = this._requestIdToRequest.get(event.requestId); // If we connect late to the target, we could have missed the requestWillBeSent event. if (request) { - this._handleRequestRedirect(request, event.redirectResponse); + this._handleRequestRedirect(request, event.redirectResponse, event.timestamp); redirectedFrom = request.request; } } @@ -893,9 +893,9 @@ export class WKPage implements PageDelegate { this._page._frameManager.requestStarted(request.request); } - private _handleRequestRedirect(request: WKInterceptableRequest, responsePayload: Protocol.Network.Response) { + private _handleRequestRedirect(request: WKInterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number) { const response = request.createResponse(responsePayload); - response._requestFinished('Response body is unavailable for redirect responses'); + response._requestFinished(responsePayload.timing ? helper.secondsToRoundishMillis(timestamp - request._timestamp) : -1, 'Response body is unavailable for redirect responses'); this._requestIdToRequest.delete(request._requestId); this._page._frameManager.requestReceivedResponse(response); this._page._frameManager.requestFinished(request.request); @@ -942,7 +942,7 @@ export class WKPage implements PageDelegate { // event from protocol. @see https://crbug.com/883475 const response = request.request._existingResponse(); if (response) - response._requestFinished(); + response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp)); this._requestIdToRequest.delete(request._requestId); this._page._frameManager.requestFinished(request.request); } @@ -955,7 +955,7 @@ export class WKPage implements PageDelegate { return; const response = request.request._existingResponse(); if (response) - response._requestFinished(); + response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp)); this._requestIdToRequest.delete(request._requestId); request.request._setFailureText(event.errorText); this._page._frameManager.requestFailed(request.request, event.errorText.includes('cancelled')); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index bbdf8b6b67..9781f112e3 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -128,7 +128,7 @@ export function headersArrayToObject(headers: HeadersArray, lowerCase: boolean): export function monotonicTime(): number { const [seconds, nanoseconds] = process.hrtime(); - return seconds * 1000 + (nanoseconds / 1000000 | 0); + return seconds * 1000 + (nanoseconds / 1000 | 0) / 1000; } export function calculateSha1(buffer: Buffer): string { diff --git a/test/resource-timing.spec.ts b/test/resource-timing.spec.ts new file mode 100644 index 0000000000..9d06ded485 --- /dev/null +++ b/test/resource-timing.spec.ts @@ -0,0 +1,118 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, it } from './fixtures'; + +it('should work', async ({ page, server }) => { + const [request] = await Promise.all([ + page.waitForEvent('requestfinished'), + page.goto(server.EMPTY_PAGE) + ]); + const timing = request.timing(); + expect(timing.domainLookupStart).toBeGreaterThanOrEqual(0); + expect(timing.domainLookupEnd).toBeGreaterThanOrEqual(timing.domainLookupStart); + expect(timing.connectStart).toBeGreaterThanOrEqual(timing.domainLookupEnd); + expect(timing.secureConnectionStart).toBe(-1); + expect(timing.connectEnd).toBeGreaterThan(timing.secureConnectionStart); + expect(timing.requestStart).toBeGreaterThanOrEqual(timing.connectEnd); + expect(timing.responseStart).toBeGreaterThan(timing.requestStart); + expect(timing.responseEnd).toBeGreaterThanOrEqual(timing.responseStart); + expect(timing.responseEnd).toBeLessThan(10000); +}); + +it('should work for subresource', async ({ page, server, isWindows, isWebKit }) => { + const requests = []; + page.on('requestfinished', request => requests.push(request)); + await page.goto(server.PREFIX + '/one-style.html'); + expect(requests.length).toBe(2); + const timing = requests[1].timing(); + if (isWebKit && isWindows) { + // Curl does not reuse connections. + expect(timing.domainLookupStart).toBeGreaterThanOrEqual(0); + expect(timing.domainLookupEnd).toBeGreaterThanOrEqual(timing.domainLookupStart); + expect(timing.connectStart).toBeGreaterThanOrEqual(timing.domainLookupEnd); + expect(timing.secureConnectionStart).toBe(-1); + expect(timing.connectEnd).toBeGreaterThan(timing.secureConnectionStart); + } else { + expect(timing.domainLookupStart).toBe(-1); + expect(timing.domainLookupEnd).toBe(-1); + expect(timing.connectStart).toBe(-1); + expect(timing.secureConnectionStart).toBe(-1); + expect(timing.connectEnd).toBe(-1); + } + expect(timing.requestStart).toBeGreaterThanOrEqual(0); + expect(timing.responseStart).toBeGreaterThan(timing.requestStart); + expect(timing.responseEnd).toBeGreaterThanOrEqual(timing.responseStart); + expect(timing.responseEnd).toBeLessThan(10000); +}); + +it('should work for SSL', async ({ browser, httpsServer, isMac, isWebKit }) => { + const page = await browser.newPage({ ignoreHTTPSErrors: true }); + const [request] = await Promise.all([ + page.waitForEvent('requestfinished'), + page.goto(httpsServer.EMPTY_PAGE) + ]); + const timing = request.timing(); + if (!(isWebKit && isMac)) { + expect(timing.domainLookupStart).toBeGreaterThanOrEqual(0); + expect(timing.domainLookupEnd).toBeGreaterThanOrEqual(timing.domainLookupStart); + expect(timing.connectStart).toBeGreaterThanOrEqual(timing.domainLookupEnd); + expect(timing.secureConnectionStart).toBeGreaterThan(timing.connectStart); + expect(timing.connectEnd).toBeGreaterThan(timing.secureConnectionStart); + } + expect(timing.requestStart).toBeGreaterThanOrEqual(timing.connectEnd); + expect(timing.responseStart).toBeGreaterThan(timing.requestStart); + expect(timing.responseEnd).toBeGreaterThanOrEqual(timing.responseStart); + expect(timing.responseEnd).toBeLessThan(10000); + await page.close(); +}); + +it('should work for redirect', (test, { browserName }) => { + test.fixme(browserName === 'webkit', `In WebKit, redirects don't carry the timing info`); +}, async ({ page, server }) => { + server.setRedirect('/foo.html', '/empty.html'); + const responses = []; + page.on('response', response => responses.push(response)); + await page.goto(server.PREFIX + '/foo.html'); + await Promise.all(responses.map(r => r.finished())); + + expect(responses.length).toBe(2); + expect(responses[0].url()).toBe(server.PREFIX + '/foo.html'); + expect(responses[1].url()).toBe(server.PREFIX + '/empty.html'); + + const timing1 = responses[0].request().timing(); + expect(timing1.domainLookupStart).toBeGreaterThanOrEqual(0); + expect(timing1.domainLookupEnd).toBeGreaterThanOrEqual(timing1.domainLookupStart); + expect(timing1.connectStart).toBeGreaterThanOrEqual(timing1.domainLookupEnd); + expect(timing1.secureConnectionStart).toBe(-1); + expect(timing1.connectEnd).toBeGreaterThan(timing1.secureConnectionStart); + expect(timing1.requestStart).toBeGreaterThanOrEqual(timing1.connectEnd); + expect(timing1.responseStart).toBeGreaterThan(timing1.requestStart); + expect(timing1.responseEnd).toBeGreaterThanOrEqual(timing1.responseStart); + expect(timing1.responseEnd).toBeLessThan(10000); + + const timing2 = responses[1].request().timing(); + expect(timing2.domainLookupStart).toBe(-1); + expect(timing2.domainLookupEnd).toBe(-1); + expect(timing2.connectStart).toBe(-1); + expect(timing2.secureConnectionStart).toBe(-1); + expect(timing2.connectEnd).toBe(-1); + expect(timing2.requestStart).toBeGreaterThanOrEqual(0); + expect(timing2.responseStart).toBeGreaterThan(timing2.requestStart); + expect(timing2.responseEnd).toBeGreaterThanOrEqual(timing2.responseStart); + expect(timing2.responseEnd).toBeLessThan(10000); +});