mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-13 07:35:33 +03:00
feat(timing): introduce resource timing (#4204)
This commit is contained in:
parent
bed304b191
commit
8a42cdad30
@ -8,7 +8,7 @@
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1194",
|
||||
"revision": "1196",
|
||||
"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.resourceType()](#requestresourcetype)
|
||||
- [request.response()](#requestresponse)
|
||||
- [request.timing()](#requesttiming)
|
||||
- [request.url()](#requesturl)
|
||||
<!-- GEN:stop -->
|
||||
|
||||
@ -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.
|
||||
|
||||
|
@ -53,6 +53,7 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
|
||||
_failureText: string | null = null;
|
||||
private _headers: Headers;
|
||||
private _postData: Buffer | null;
|
||||
_timing: ResourceTiming;
|
||||
|
||||
static from(request: channels.RequestChannel): Request {
|
||||
return (request as any)._object;
|
||||
@ -69,6 +70,17 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
|
||||
this._redirectedFrom._redirectedTo = this;
|
||||
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
|
||||
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 {
|
||||
@ -143,6 +155,10 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
|
||||
};
|
||||
}
|
||||
|
||||
timing(): ResourceTiming {
|
||||
return this._timing;
|
||||
}
|
||||
|
||||
_finalRequest(): Request {
|
||||
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 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> {
|
||||
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<channels.ResponseChannel, channels.Re
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ResponseInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
|
||||
this._request = Request.from(this._initializer.request);
|
||||
Object.assign(this._request._timing, this._initializer.timing);
|
||||
}
|
||||
|
||||
url(): string {
|
||||
@ -272,11 +303,11 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
|
||||
}
|
||||
|
||||
request(): Request {
|
||||
return Request.from(this._initializer.request);
|
||||
return this._request;
|
||||
}
|
||||
|
||||
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('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<channels.PageChannel, channels.PageInitia
|
||||
}
|
||||
}
|
||||
|
||||
private _onRequestFailed(request: Request, failureText: string | undefined) {
|
||||
private _onRequestFailed(request: Request, responseEndTiming: number, failureText: string | undefined) {
|
||||
request._failureText = failureText || null;
|
||||
if (request._timing)
|
||||
request._timing.responseEnd = responseEndTiming;
|
||||
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) {
|
||||
frame._page = this;
|
||||
this._frames.add(frame);
|
||||
|
@ -59,6 +59,7 @@ export class ResponseDispatcher extends Dispatcher<Response, channels.ResponseIn
|
||||
status: response.status(),
|
||||
statusText: response.statusText(),
|
||||
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.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) }));
|
||||
|
@ -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<ResponseBodyResult>;
|
||||
|
@ -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
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
@ -28,6 +28,7 @@ export class FFNetworkManager {
|
||||
private _requests: Map<string, InterceptableRequest>;
|
||||
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');
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -81,6 +81,7 @@ export class Request {
|
||||
private _frame: frames.Frame;
|
||||
private _waitForResponsePromise: Promise<Response | null>;
|
||||
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<Buffer>;
|
||||
|
||||
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<Buffer> | 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<Buffer> {
|
||||
if (!this._contentPromise) {
|
||||
this._contentPromise = this._finishedPromise.then(async ({ error }) => {
|
||||
|
@ -46,6 +46,8 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||
_interceptedCallback: () => void = () => {};
|
||||
private _interceptedPromise: Promise<unknown>;
|
||||
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;
|
||||
}
|
||||
|
@ -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'));
|
||||
|
@ -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 {
|
||||
|
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 New Issue
Block a user