feat(timing): introduce resource timing (#4204)
This commit is contained in:
parent
bed304b191
commit
8a42cdad30
|
|
@ -8,7 +8,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox",
|
"name": "firefox",
|
||||||
"revision": "1194",
|
"revision": "1196",
|
||||||
"download": true
|
"download": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
24
docs/api.md
24
docs/api.md
|
|
@ -3815,6 +3815,7 @@ If request gets a 'redirect' response, the request is successfully finished with
|
||||||
- [request.redirectedTo()](#requestredirectedto)
|
- [request.redirectedTo()](#requestredirectedto)
|
||||||
- [request.resourceType()](#requestresourcetype)
|
- [request.resourceType()](#requestresourcetype)
|
||||||
- [request.response()](#requestresponse)
|
- [request.response()](#requestresponse)
|
||||||
|
- [request.timing()](#requesttiming)
|
||||||
- [request.url()](#requesturl)
|
- [request.url()](#requesturl)
|
||||||
<!-- GEN:stop -->
|
<!-- GEN:stop -->
|
||||||
|
|
||||||
|
|
@ -3892,6 +3893,29 @@ ResourceType will be one of the following: `document`, `stylesheet`, `image`, `m
|
||||||
#### request.response()
|
#### request.response()
|
||||||
- returns: <[Promise]<[null]|[Response]>> A matching [Response] object, or `null` if the response was not received due to error.
|
- 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()
|
#### request.url()
|
||||||
- returns: <[string]> URL of the request.
|
- returns: <[string]> URL of the request.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
|
||||||
_failureText: string | null = null;
|
_failureText: string | null = null;
|
||||||
private _headers: Headers;
|
private _headers: Headers;
|
||||||
private _postData: Buffer | null;
|
private _postData: Buffer | null;
|
||||||
|
_timing: ResourceTiming;
|
||||||
|
|
||||||
static from(request: channels.RequestChannel): Request {
|
static from(request: channels.RequestChannel): Request {
|
||||||
return (request as any)._object;
|
return (request as any)._object;
|
||||||
|
|
@ -69,6 +70,17 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
|
||||||
this._redirectedFrom._redirectedTo = this;
|
this._redirectedFrom._redirectedTo = this;
|
||||||
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
|
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
|
||||||
this._postData = initializer.postData ? Buffer.from(initializer.postData, 'base64') : null;
|
this._postData = initializer.postData ? Buffer.from(initializer.postData, 'base64') : null;
|
||||||
|
this._timing = {
|
||||||
|
startTime: 0,
|
||||||
|
domainLookupStart: -1,
|
||||||
|
domainLookupEnd: -1,
|
||||||
|
connectStart: -1,
|
||||||
|
secureConnectionStart: -1,
|
||||||
|
connectEnd: -1,
|
||||||
|
requestStart: -1,
|
||||||
|
responseStart: -1,
|
||||||
|
responseEnd: -1,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
url(): string {
|
url(): string {
|
||||||
|
|
@ -143,6 +155,10 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timing(): ResourceTiming {
|
||||||
|
return this._timing;
|
||||||
|
}
|
||||||
|
|
||||||
_finalRequest(): Request {
|
_finalRequest(): Request {
|
||||||
return this._redirectedTo ? this._redirectedTo._finalRequest() : this;
|
return this._redirectedTo ? this._redirectedTo._finalRequest() : this;
|
||||||
}
|
}
|
||||||
|
|
@ -214,8 +230,21 @@ export class Route extends ChannelOwner<channels.RouteChannel, channels.RouteIni
|
||||||
|
|
||||||
export type RouteHandler = (route: Route, request: Request) => void;
|
export type RouteHandler = (route: Route, request: Request) => 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<channels.ResponseChannel, channels.ResponseInitializer> {
|
export class Response extends ChannelOwner<channels.ResponseChannel, channels.ResponseInitializer> {
|
||||||
private _headers: Headers;
|
private _headers: Headers;
|
||||||
|
private _request: Request;
|
||||||
|
|
||||||
static from(response: channels.ResponseChannel): Response {
|
static from(response: channels.ResponseChannel): Response {
|
||||||
return (response as any)._object;
|
return (response as any)._object;
|
||||||
|
|
@ -228,6 +257,8 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ResponseInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ResponseInitializer) {
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer);
|
||||||
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
|
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
|
||||||
|
this._request = Request.from(this._initializer.request);
|
||||||
|
Object.assign(this._request._timing, this._initializer.timing);
|
||||||
}
|
}
|
||||||
|
|
||||||
url(): string {
|
url(): string {
|
||||||
|
|
@ -272,11 +303,11 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
|
||||||
}
|
}
|
||||||
|
|
||||||
request(): Request {
|
request(): Request {
|
||||||
return Request.from(this._initializer.request);
|
return this._request;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame(): Frame {
|
frame(): Frame {
|
||||||
return Request.from(this._initializer.request).frame();
|
return this._request.frame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,8 +125,8 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
||||||
this._channel.on('pageError', ({ error }) => this.emit(Events.Page.PageError, parseError(error)));
|
this._channel.on('pageError', ({ error }) => this.emit(Events.Page.PageError, parseError(error)));
|
||||||
this._channel.on('popup', ({ page }) => this.emit(Events.Page.Popup, Page.from(page)));
|
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('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('requestFailed', ({ request, failureText, responseEndTiming }) => this._onRequestFailed(Request.from(request), responseEndTiming, failureText));
|
||||||
this._channel.on('requestFinished', ({ request }) => this.emit(Events.Page.RequestFinished, Request.from(request)));
|
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('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('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request)));
|
||||||
this._channel.on('video', ({ relativePath }) => this.video()!._setRelativePath(relativePath));
|
this._channel.on('video', ({ relativePath }) => this.video()!._setRelativePath(relativePath));
|
||||||
|
|
@ -138,11 +138,19 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onRequestFailed(request: Request, failureText: string | undefined) {
|
private _onRequestFailed(request: Request, responseEndTiming: number, failureText: string | undefined) {
|
||||||
request._failureText = failureText || null;
|
request._failureText = failureText || null;
|
||||||
|
if (request._timing)
|
||||||
|
request._timing.responseEnd = responseEndTiming;
|
||||||
this.emit(Events.Page.RequestFailed, request);
|
this.emit(Events.Page.RequestFailed, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _onRequestFinished(request: Request, responseEndTiming: number) {
|
||||||
|
if (request._timing)
|
||||||
|
request._timing.responseEnd = responseEndTiming;
|
||||||
|
this.emit(Events.Page.RequestFinished, request);
|
||||||
|
}
|
||||||
|
|
||||||
private _onFrameAttached(frame: Frame) {
|
private _onFrameAttached(frame: Frame) {
|
||||||
frame._page = this;
|
frame._page = this;
|
||||||
this._frames.add(frame);
|
this._frames.add(frame);
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export class ResponseDispatcher extends Dispatcher<Response, channels.ResponseIn
|
||||||
status: response.status(),
|
status: response.status(),
|
||||||
statusText: response.statusText(),
|
statusText: response.statusText(),
|
||||||
headers: response.headers(),
|
headers: response.headers(),
|
||||||
|
timing: response.timing()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,13 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
|
||||||
page.on(Page.Events.Request, request => this._dispatchEvent('request', { request: RequestDispatcher.from(this._scope, request) }));
|
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', {
|
page.on(Page.Events.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', {
|
||||||
request: RequestDispatcher.from(this._scope, request),
|
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.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.VideoStarted, (video: Video) => this._dispatchEvent('video', { relativePath: video._relativePath }));
|
||||||
page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
|
page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
|
||||||
|
|
|
||||||
|
|
@ -758,9 +758,11 @@ export type PageRequestEvent = {
|
||||||
export type PageRequestFailedEvent = {
|
export type PageRequestFailedEvent = {
|
||||||
request: RequestChannel,
|
request: RequestChannel,
|
||||||
failureText?: string,
|
failureText?: string,
|
||||||
|
responseEndTiming: number,
|
||||||
};
|
};
|
||||||
export type PageRequestFinishedEvent = {
|
export type PageRequestFinishedEvent = {
|
||||||
request: RequestChannel,
|
request: RequestChannel,
|
||||||
|
responseEndTiming: number,
|
||||||
};
|
};
|
||||||
export type PageResponseEvent = {
|
export type PageResponseEvent = {
|
||||||
response: ResponseChannel,
|
response: ResponseChannel,
|
||||||
|
|
@ -2133,6 +2135,17 @@ export type RouteFulfillOptions = {
|
||||||
};
|
};
|
||||||
export type RouteFulfillResult = void;
|
export type RouteFulfillResult = void;
|
||||||
|
|
||||||
|
export type ResourceTiming = {
|
||||||
|
startTime: number,
|
||||||
|
domainLookupStart: number,
|
||||||
|
domainLookupEnd: number,
|
||||||
|
connectStart: number,
|
||||||
|
secureConnectionStart: number,
|
||||||
|
connectEnd: number,
|
||||||
|
requestStart: number,
|
||||||
|
responseStart: number,
|
||||||
|
};
|
||||||
|
|
||||||
// ----------- Response -----------
|
// ----------- Response -----------
|
||||||
export type ResponseInitializer = {
|
export type ResponseInitializer = {
|
||||||
request: RequestChannel,
|
request: RequestChannel,
|
||||||
|
|
@ -2143,6 +2156,7 @@ export type ResponseInitializer = {
|
||||||
name: string,
|
name: string,
|
||||||
value: string,
|
value: string,
|
||||||
}[],
|
}[],
|
||||||
|
timing: ResourceTiming,
|
||||||
};
|
};
|
||||||
export interface ResponseChannel extends Channel {
|
export interface ResponseChannel extends Channel {
|
||||||
body(params?: ResponseBodyParams, metadata?: Metadata): Promise<ResponseBodyResult>;
|
body(params?: ResponseBodyParams, metadata?: Metadata): Promise<ResponseBodyResult>;
|
||||||
|
|
|
||||||
|
|
@ -920,10 +920,12 @@ Page:
|
||||||
parameters:
|
parameters:
|
||||||
request: Request
|
request: Request
|
||||||
failureText: string?
|
failureText: string?
|
||||||
|
responseEndTiming: number
|
||||||
|
|
||||||
requestFinished:
|
requestFinished:
|
||||||
parameters:
|
parameters:
|
||||||
request: Request
|
request: Request
|
||||||
|
responseEndTiming: number
|
||||||
|
|
||||||
response:
|
response:
|
||||||
parameters:
|
parameters:
|
||||||
|
|
@ -1789,6 +1791,17 @@ Route:
|
||||||
isBase64: boolean?
|
isBase64: boolean?
|
||||||
|
|
||||||
|
|
||||||
|
ResourceTiming:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
startTime: number
|
||||||
|
domainLookupStart: number
|
||||||
|
domainLookupEnd: number
|
||||||
|
connectStart: number
|
||||||
|
secureConnectionStart: number
|
||||||
|
connectEnd: number
|
||||||
|
requestStart: number
|
||||||
|
responseStart: number
|
||||||
|
|
||||||
Response:
|
Response:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
@ -1805,6 +1818,8 @@ Response:
|
||||||
properties:
|
properties:
|
||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
|
timing: ResourceTiming
|
||||||
|
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
|
|
||||||
|
|
@ -1817,7 +1832,6 @@ Response:
|
||||||
error: string?
|
error: string?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ConsoleMessage:
|
ConsoleMessage:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -837,6 +837,16 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
body: tOptional(tString),
|
body: tOptional(tString),
|
||||||
isBase64: tOptional(tBoolean),
|
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.ResponseBodyParams = tOptional(tObject({}));
|
||||||
scheme.ResponseFinishedParams = tOptional(tObject({}));
|
scheme.ResponseFinishedParams = tOptional(tObject({}));
|
||||||
scheme.BindingCallRejectParams = tObject({
|
scheme.BindingCallRejectParams = tObject({
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ export class CRNetworkManager {
|
||||||
const request = this._requestIdToRequest.get(requestWillBeSentEvent.requestId);
|
const request = this._requestIdToRequest.get(requestWillBeSentEvent.requestId);
|
||||||
// If we connect late to the target, we could have missed the requestWillBeSent event.
|
// If we connect late to the target, we could have missed the requestWillBeSent event.
|
||||||
if (request) {
|
if (request) {
|
||||||
this._handleRequestRedirect(request, requestWillBeSentEvent.redirectResponse);
|
this._handleRequestRedirect(request, requestWillBeSentEvent.redirectResponse, requestWillBeSentEvent.timestamp);
|
||||||
redirectedFrom = request.request;
|
redirectedFrom = request.request;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -240,12 +240,37 @@ export class CRNetworkManager {
|
||||||
const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId });
|
const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId });
|
||||||
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
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);
|
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);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
if (request._interceptionId)
|
if (request._interceptionId)
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
|
|
@ -275,7 +300,7 @@ export class CRNetworkManager {
|
||||||
// event from protocol. @see https://crbug.com/883475
|
// event from protocol. @see https://crbug.com/883475
|
||||||
const response = request.request._existingResponse();
|
const response = request.request._existingResponse();
|
||||||
if (response)
|
if (response)
|
||||||
response._requestFinished();
|
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
if (request._interceptionId)
|
if (request._interceptionId)
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
|
|
@ -292,7 +317,7 @@ export class CRNetworkManager {
|
||||||
return;
|
return;
|
||||||
const response = request.request._existingResponse();
|
const response = request.request._existingResponse();
|
||||||
if (response)
|
if (response)
|
||||||
response._requestFinished();
|
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
if (request._interceptionId)
|
if (request._interceptionId)
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
|
|
@ -324,6 +349,8 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
_interceptionId: string | null;
|
_interceptionId: string | null;
|
||||||
_documentId: string | undefined;
|
_documentId: string | undefined;
|
||||||
private _client: CRSession;
|
private _client: CRSession;
|
||||||
|
_timestamp: number;
|
||||||
|
_wallTime: number;
|
||||||
|
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
client: CRSession;
|
client: CRSession;
|
||||||
|
|
@ -336,6 +363,8 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
}) {
|
}) {
|
||||||
const { client, frame, documentId, allowInterception, requestWillBeSentEvent, requestPausedEvent, redirectedFrom } = options;
|
const { client, frame, documentId, allowInterception, requestWillBeSentEvent, requestPausedEvent, redirectedFrom } = options;
|
||||||
this._client = client;
|
this._client = client;
|
||||||
|
this._timestamp = requestWillBeSentEvent.timestamp;
|
||||||
|
this._wallTime = requestWillBeSentEvent.wallTime;
|
||||||
this._requestId = requestWillBeSentEvent.requestId;
|
this._requestId = requestWillBeSentEvent.requestId;
|
||||||
this._interceptionId = requestPausedEvent && requestPausedEvent.requestId;
|
this._interceptionId = requestPausedEvent && requestPausedEvent.requestId;
|
||||||
this._documentId = documentId;
|
this._documentId = documentId;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export class FFNetworkManager {
|
||||||
private _requests: Map<string, InterceptableRequest>;
|
private _requests: Map<string, InterceptableRequest>;
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
private _eventListeners: RegisteredListener[];
|
private _eventListeners: RegisteredListener[];
|
||||||
|
private _startTime = 0;
|
||||||
|
|
||||||
constructor(session: FFSession, page: Page) {
|
constructor(session: FFSession, page: Page) {
|
||||||
this._session = session;
|
this._session = session;
|
||||||
|
|
@ -75,7 +76,19 @@ export class FFNetworkManager {
|
||||||
throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`);
|
throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`);
|
||||||
return Buffer.from(response.base64body, 'base64');
|
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);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,10 +100,10 @@ export class FFNetworkManager {
|
||||||
// Keep redirected requests in the map for future reference as redirectedFrom.
|
// Keep redirected requests in the map for future reference as redirectedFrom.
|
||||||
const isRedirected = response.status() >= 300 && response.status() <= 399;
|
const isRedirected = response.status() >= 300 && response.status() <= 399;
|
||||||
if (isRedirected) {
|
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 {
|
} else {
|
||||||
this._requests.delete(request._id);
|
this._requests.delete(request._id);
|
||||||
response._requestFinished();
|
response._requestFinished(this._relativeTiming(event.responseEndTime));
|
||||||
}
|
}
|
||||||
this._page._frameManager.requestFinished(request.request);
|
this._page._frameManager.requestFinished(request.request);
|
||||||
}
|
}
|
||||||
|
|
@ -102,10 +115,16 @@ export class FFNetworkManager {
|
||||||
this._requests.delete(request._id);
|
this._requests.delete(request._id);
|
||||||
const response = request.request._existingResponse();
|
const response = request.request._existingResponse();
|
||||||
if (response)
|
if (response)
|
||||||
response._requestFinished();
|
response._requestFinished(-1);
|
||||||
request.request._setFailureText(event.errorCode);
|
request.request._setFailureText(event.errorCode);
|
||||||
this._page._frameManager.requestFailed(request.request, event.errorCode === 'NS_BINDING_ABORTED');
|
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} = {
|
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) {
|
constructor(session: FFSession, frame: frames.Frame, redirectedFrom: InterceptableRequest | null, payload: Protocol.Network.requestWillBeSentPayload) {
|
||||||
this._id = payload.requestId;
|
this._id = payload.requestId;
|
||||||
this._session = session;
|
this._session = session;
|
||||||
|
|
||||||
let postDataBuffer = null;
|
let postDataBuffer = null;
|
||||||
if (payload.postData)
|
if (payload.postData)
|
||||||
postDataBuffer = Buffer.from(payload.postData, 'base64');
|
postDataBuffer = Buffer.from(payload.postData, 'base64');
|
||||||
|
|
|
||||||
|
|
@ -776,6 +776,16 @@ export module Protocol {
|
||||||
validFrom: number;
|
validFrom: number;
|
||||||
validTo: 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 = {
|
export type requestWillBeSentPayload = {
|
||||||
frameId?: string;
|
frameId?: string;
|
||||||
requestId: string;
|
requestId: string;
|
||||||
|
|
@ -810,9 +820,20 @@ export module Protocol {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
|
timing: {
|
||||||
|
startTime: number;
|
||||||
|
domainLookupStart: number;
|
||||||
|
domainLookupEnd: number;
|
||||||
|
connectStart: number;
|
||||||
|
secureConnectionStart: number;
|
||||||
|
connectEnd: number;
|
||||||
|
requestStart: number;
|
||||||
|
responseStart: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export type requestFinishedPayload = {
|
export type requestFinishedPayload = {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
|
responseEndTime: number;
|
||||||
}
|
}
|
||||||
export type requestFailedPayload = {
|
export type requestFailedPayload = {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,14 @@ class Helper {
|
||||||
progress.cleanupWhenAborted(dispose);
|
progress.cleanupWhenAborted(dispose);
|
||||||
return { promise, 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;
|
export const helper = Helper;
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ export class Request {
|
||||||
private _frame: frames.Frame;
|
private _frame: frames.Frame;
|
||||||
private _waitForResponsePromise: Promise<Response | null>;
|
private _waitForResponsePromise: Promise<Response | null>;
|
||||||
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
|
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
|
||||||
|
_responseEndTiming = -1;
|
||||||
|
|
||||||
constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined,
|
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) {
|
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<Buffer>;
|
type GetResponseBodyCallback = () => Promise<Buffer>;
|
||||||
|
|
||||||
|
export type ResourceTiming = {
|
||||||
|
startTime: number;
|
||||||
|
domainLookupStart: number;
|
||||||
|
domainLookupEnd: number;
|
||||||
|
connectStart: number;
|
||||||
|
secureConnectionStart: number;
|
||||||
|
connectEnd: number;
|
||||||
|
requestStart: number;
|
||||||
|
responseStart: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class Response {
|
export class Response {
|
||||||
private _request: Request;
|
private _request: Request;
|
||||||
private _contentPromise: Promise<Buffer> | null = null;
|
private _contentPromise: Promise<Buffer> | null = null;
|
||||||
|
|
@ -221,9 +233,11 @@ export class Response {
|
||||||
private _url: string;
|
private _url: string;
|
||||||
private _headers: types.HeadersArray;
|
private _headers: types.HeadersArray;
|
||||||
private _getResponseBodyCallback: GetResponseBodyCallback;
|
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._request = request;
|
||||||
|
this._timing = timing;
|
||||||
this._status = status;
|
this._status = status;
|
||||||
this._statusText = statusText;
|
this._statusText = statusText;
|
||||||
this._url = request.url();
|
this._url = request.url();
|
||||||
|
|
@ -235,7 +249,8 @@ export class Response {
|
||||||
this._request._setResponse(this);
|
this._request._setResponse(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_requestFinished(error?: string) {
|
_requestFinished(responseEndTiming: number, error?: string) {
|
||||||
|
this._request._responseEndTiming = Math.max(responseEndTiming, this._timing.responseStart);
|
||||||
this._finishedPromiseCallback({ error });
|
this._finishedPromiseCallback({ error });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,6 +274,10 @@ export class Response {
|
||||||
return this._finishedPromise.then(({ error }) => error ? new Error(error) : null);
|
return this._finishedPromise.then(({ error }) => error ? new Error(error) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timing(): ResourceTiming {
|
||||||
|
return this._timing;
|
||||||
|
}
|
||||||
|
|
||||||
body(): Promise<Buffer> {
|
body(): Promise<Buffer> {
|
||||||
if (!this._contentPromise) {
|
if (!this._contentPromise) {
|
||||||
this._contentPromise = this._finishedPromise.then(async ({ error }) => {
|
this._contentPromise = this._finishedPromise.then(async ({ error }) => {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||||
_interceptedCallback: () => void = () => {};
|
_interceptedCallback: () => void = () => {};
|
||||||
private _interceptedPromise: Promise<unknown>;
|
private _interceptedPromise: Promise<unknown>;
|
||||||
readonly _allowInterception: boolean;
|
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) {
|
constructor(session: WKSession, allowInterception: boolean, frame: frames.Frame, event: Protocol.Network.requestWillBeSentPayload, redirectedFrom: network.Request | null, documentId: string | undefined) {
|
||||||
this._session = session;
|
this._session = session;
|
||||||
|
|
@ -53,6 +55,8 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||||
this._allowInterception = allowInterception;
|
this._allowInterception = allowInterception;
|
||||||
const resourceType = event.type ? event.type.toLowerCase() : (redirectedFrom ? redirectedFrom.resourceType() : 'other');
|
const resourceType = event.type ? event.type.toLowerCase() : (redirectedFrom ? redirectedFrom.resourceType() : 'other');
|
||||||
let postDataBuffer = null;
|
let postDataBuffer = null;
|
||||||
|
this._timestamp = event.timestamp;
|
||||||
|
this._wallTime = event.walltime * 1000;
|
||||||
if (event.request.postData)
|
if (event.request.postData)
|
||||||
postDataBuffer = Buffer.from(event.request.postData, 'binary');
|
postDataBuffer = Buffer.from(event.request.postData, 'binary');
|
||||||
this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, event.request.url,
|
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 });
|
const response = await this._session.send('Network.getResponseBody', { requestId: this._requestId });
|
||||||
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -878,7 +878,7 @@ export class WKPage implements PageDelegate {
|
||||||
const request = this._requestIdToRequest.get(event.requestId);
|
const request = this._requestIdToRequest.get(event.requestId);
|
||||||
// If we connect late to the target, we could have missed the requestWillBeSent event.
|
// If we connect late to the target, we could have missed the requestWillBeSent event.
|
||||||
if (request) {
|
if (request) {
|
||||||
this._handleRequestRedirect(request, event.redirectResponse);
|
this._handleRequestRedirect(request, event.redirectResponse, event.timestamp);
|
||||||
redirectedFrom = request.request;
|
redirectedFrom = request.request;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -893,9 +893,9 @@ export class WKPage implements PageDelegate {
|
||||||
this._page._frameManager.requestStarted(request.request);
|
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);
|
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._requestIdToRequest.delete(request._requestId);
|
||||||
this._page._frameManager.requestReceivedResponse(response);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
this._page._frameManager.requestFinished(request.request);
|
this._page._frameManager.requestFinished(request.request);
|
||||||
|
|
@ -942,7 +942,7 @@ export class WKPage implements PageDelegate {
|
||||||
// event from protocol. @see https://crbug.com/883475
|
// event from protocol. @see https://crbug.com/883475
|
||||||
const response = request.request._existingResponse();
|
const response = request.request._existingResponse();
|
||||||
if (response)
|
if (response)
|
||||||
response._requestFinished();
|
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._page._frameManager.requestFinished(request.request);
|
this._page._frameManager.requestFinished(request.request);
|
||||||
}
|
}
|
||||||
|
|
@ -955,7 +955,7 @@ export class WKPage implements PageDelegate {
|
||||||
return;
|
return;
|
||||||
const response = request.request._existingResponse();
|
const response = request.request._existingResponse();
|
||||||
if (response)
|
if (response)
|
||||||
response._requestFinished();
|
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
request.request._setFailureText(event.errorText);
|
request.request._setFailureText(event.errorText);
|
||||||
this._page._frameManager.requestFailed(request.request, event.errorText.includes('cancelled'));
|
this._page._frameManager.requestFailed(request.request, event.errorText.includes('cancelled'));
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ export function headersArrayToObject(headers: HeadersArray, lowerCase: boolean):
|
||||||
|
|
||||||
export function monotonicTime(): number {
|
export function monotonicTime(): number {
|
||||||
const [seconds, nanoseconds] = process.hrtime();
|
const [seconds, nanoseconds] = process.hrtime();
|
||||||
return seconds * 1000 + (nanoseconds / 1000000 | 0);
|
return seconds * 1000 + (nanoseconds / 1000 | 0) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateSha1(buffer: Buffer): string {
|
export function calculateSha1(buffer: Buffer): string {
|
||||||
|
|
|
||||||
118
test/resource-timing.spec.ts
Normal file
118
test/resource-timing.spec.ts
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue