feat(timing): introduce resource timing (#4204)

This commit is contained in:
Pavel Feldman 2020-10-21 23:25:57 -07:00 коммит произвёл GitHub
Родитель bed304b191
Коммит 8a42cdad30
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 377 добавлений и 29 удалений

Просмотреть файл

@ -8,7 +8,7 @@
},
{
"name": "firefox",
"revision": "1194",
"revision": "1196",
"download": true
},
{

Просмотреть файл

@ -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 {

Просмотреть файл

@ -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);
});