@quick-game/cli
Version:
Command line interface for rapid qg development
361 lines • 18.8 kB
JavaScript
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Platform from '../../../core/platform/platform.js';
import { data as metaHandlerData } from './MetaHandler.js';
import * as Helpers from '../helpers/helpers.js';
import * as Types from '../types/types.js';
const MILLISECONDS_TO_MICROSECONDS = 1000;
const SECONDS_TO_MICROSECONDS = 1000000;
const requestMap = new Map();
const requestsByOrigin = new Map();
const requestsByTime = [];
function storeTraceEventWithRequestId(requestId, key, value) {
if (!requestMap.has(requestId)) {
requestMap.set(requestId, {});
}
const traceEvents = requestMap.get(requestId);
if (!traceEvents) {
throw new Error(`Unable to locate trace events for request ID ${requestId}`);
}
if (Array.isArray(traceEvents[key])) {
const target = traceEvents[key];
const values = value;
target.push(...values);
}
else {
traceEvents[key] = value;
}
}
function firstPositiveValueInList(entries) {
for (const entry of entries) {
if (entry > 0) {
return entry;
}
}
// In the event we don't find a positive value, we return 0 so as to
// be a mathematical noop. It's typically not correct to return – say –
// a -1 here because it would affect the calculation of stats below.
return 0;
}
let handlerState = 1 /* HandlerState.UNINITIALIZED */;
export function reset() {
requestsByOrigin.clear();
requestMap.clear();
requestsByTime.length = 0;
handlerState = 1 /* HandlerState.UNINITIALIZED */;
}
export function initialize() {
handlerState = 2 /* HandlerState.INITIALIZED */;
}
export function handleEvent(event) {
if (handlerState !== 2 /* HandlerState.INITIALIZED */) {
throw new Error('Network Request handler is not initialized');
}
if (Types.TraceEvents.isTraceEventResourceChangePriority(event)) {
storeTraceEventWithRequestId(event.args.data.requestId, 'changePriority', event);
return;
}
if (Types.TraceEvents.isTraceEventResourceWillSendRequest(event)) {
storeTraceEventWithRequestId(event.args.data.requestId, 'willSendRequests', [event]);
return;
}
if (Types.TraceEvents.isTraceEventResourceSendRequest(event)) {
storeTraceEventWithRequestId(event.args.data.requestId, 'sendRequests', [event]);
return;
}
if (Types.TraceEvents.isTraceEventResourceReceiveResponse(event)) {
storeTraceEventWithRequestId(event.args.data.requestId, 'receiveResponse', event);
return;
}
if (Types.TraceEvents.isTraceEventResourceReceivedData(event)) {
storeTraceEventWithRequestId(event.args.data.requestId, 'receivedData', [event]);
return;
}
if (Types.TraceEvents.isTraceEventResourceFinish(event)) {
storeTraceEventWithRequestId(event.args.data.requestId, 'resourceFinish', event);
return;
}
if (Types.TraceEvents.isTraceEventResourceMarkAsCached(event)) {
storeTraceEventWithRequestId(event.args.data.requestId, 'resourceMarkAsCached', event);
return;
}
}
export async function finalize() {
if (handlerState !== 2 /* HandlerState.INITIALIZED */) {
throw new Error('Network Request handler is not initialized');
}
const { rendererProcessesByFrame } = metaHandlerData();
for (const [requestId, request] of requestMap.entries()) {
// If we have an incomplete set of events here, we choose to drop the network
// request rather than attempt to synthesize the missing data.
if (!request.sendRequests || !request.receiveResponse) {
continue;
}
// In the data we may get multiple willSendRequests and sendRequests, which
// will indicate that there are redirects for a given (sub)resource. In the
// case of a navigation, e.g., example.com/ we will get willSendRequests,
// and we should use these to calculate time spent in redirects.
// In the case of sub-resources, however, e.g., example.com/foo.js we will
// *only* get sendRequests, and we use these instead of willSendRequests
// to detect the time in redirects. We always use the sendRequest for the
// url, priority etc since it contains those values, but we use the
// willSendRequest (if it exists) to calculate the timestamp and durations
// of redirects.
const redirects = [];
for (let i = 0; i < request.sendRequests.length - 1; i++) {
const sendRequest = request.sendRequests[i];
const nextSendRequest = request.sendRequests[i + 1];
// Use the willSendRequests as the source for redirects if possible.
// We default to those of the sendRequests, however, since willSendRequest
// is not guaranteed to be present in the data for every request.
let ts = sendRequest.ts;
let dur = Types.Timing.MicroSeconds(nextSendRequest.ts - sendRequest.ts);
if (request.willSendRequests && request.willSendRequests[i] && request.willSendRequests[i + 1]) {
const willSendRequest = request.willSendRequests[i];
const nextWillSendRequest = request.willSendRequests[i + 1];
ts = willSendRequest.ts;
dur = Types.Timing.MicroSeconds(nextWillSendRequest.ts - willSendRequest.ts);
}
redirects.push({
url: sendRequest.args.data.url,
priority: sendRequest.args.data.priority,
requestMethod: sendRequest.args.data.requestMethod,
ts,
dur,
});
}
// If a ResourceFinish event with an encoded data length is received,
// then the resource was not cached; it was fetched before it was
// requested, e.g. because it was pushed in this navigation.
const isPushedResource = request.resourceFinish?.args.data.encodedDataLength !== 0;
// This works around crbug.com/998397, which reports pushed resources, and resources served by a service worker as disk cached.
const isDiskCached = request.receiveResponse.args.data.fromCache &&
!request.receiveResponse.args.data.fromServiceWorker && !isPushedResource;
// If the request contains a resourceMarkAsCached event, it was served from memory cache.
const isMemoryCached = request.resourceMarkAsCached !== undefined;
// The timing data returned is from the original (uncached) request, which
// means that if we leave the above network record data as-is when the
// request came from either the disk cache or memory cache, our calculations
// will be incorrect.
//
// Here we add a flag so when we calculate the timestamps of the various
// events, we can overwrite them.
// These timestamps may not be perfect (indeed they don't always match
// the Network CDP domain exactly, which is likely an artifact of the way
// the data is routed on the backend), but they're the closest we have.
const isCached = isMemoryCached || isDiskCached;
const timing = request.receiveResponse.args.data.timing;
// If a non-cached request has no |timing| indicates data URLs, we ignore it.
if (!timing && !isCached) {
continue;
}
const firstSendRequest = request.sendRequests[0];
const finalSendRequest = request.sendRequests[request.sendRequests.length - 1];
const initialPriority = finalSendRequest.args.data.priority;
let finalPriority = initialPriority;
if (request.changePriority) {
finalPriority = request.changePriority.args.data.priority;
}
// Start time
// =======================
// The time where the request started, which is either the first willSendRequest
// event if there is one, or, if there is not, the sendRequest.
const startTime = (request.willSendRequests && request.willSendRequests.length) ?
Types.Timing.MicroSeconds(request.willSendRequests[0].ts) :
Types.Timing.MicroSeconds(firstSendRequest.ts);
// End redirect time
// =======================
// It's possible that when we start requesting data we will receive redirections.
// Here we note the time of the *last* willSendRequest / sendRequest event,
// which is used later on in the calculations for time queueing etc.
const endRedirectTime = (request.willSendRequests && request.willSendRequests.length) ?
Types.Timing.MicroSeconds(request.willSendRequests[request.willSendRequests.length - 1].ts) :
Types.Timing.MicroSeconds(finalSendRequest.ts);
// Finish time and end time
// =======================
// The finish time and the end time are subtly different.
// - Finish time: records the point at which the network stack stopped receiving the data
// - End time: the timestamp of the finish event itself (if one exists)
//
// The end time, then, will be slightly after the finish time.
const endTime = request.resourceFinish ? request.resourceFinish.ts : endRedirectTime;
const finishTime = request.resourceFinish?.args.data.finishTime ?
Types.Timing.MicroSeconds(request.resourceFinish.args.data.finishTime * SECONDS_TO_MICROSECONDS) :
Types.Timing.MicroSeconds(endTime);
// Network duration
// =======================
// Time spent on the network.
const networkDuration = isCached ? Types.Timing.MicroSeconds(0) :
Types.Timing.MicroSeconds((finishTime || endRedirectTime) - endRedirectTime);
// Processing duration
// =======================
// Time spent from start to end.
const processingDuration = Types.Timing.MicroSeconds(endTime - (finishTime || endTime));
// Redirection duration
// =======================
// Time between the first willSendRequest / sendRequest and last. This we place in *front* of the
// queueing, since the queueing time that we know about from the trace data is only the last request,
// i.e., the one that occurs after all the redirects.
const redirectionDuration = Types.Timing.MicroSeconds(endRedirectTime - startTime);
// Queueing
// =======================
// The amount of time queueing is the time between the request's start time to the requestTime
// arg recorded in the receiveResponse event. In the cases where the recorded start time is larger
// that the requestTime we set queueing time to zero.
const queueing = isCached ?
Types.Timing.MicroSeconds(0) :
Types.Timing.MicroSeconds(Platform.NumberUtilities.clamp((timing.requestTime * SECONDS_TO_MICROSECONDS - endRedirectTime), 0, Number.MAX_VALUE));
// Stalled
// =======================
// If the request is cached, the amount of time stalled is the time between the start time and
// receiving a response.
// Otherwise it is whichever positive number comes first from the following timing info:
// DNS start, Connection start, Send Start, or the time duration between our start time and
// receiving a response.
const stalled = isCached ? Types.Timing.MicroSeconds(request.receiveResponse.ts - startTime) :
Types.Timing.MicroSeconds(firstPositiveValueInList([
timing.dnsStart * MILLISECONDS_TO_MICROSECONDS,
timing.connectStart * MILLISECONDS_TO_MICROSECONDS,
timing.sendStart * MILLISECONDS_TO_MICROSECONDS,
(request.receiveResponse.ts - endRedirectTime),
]));
// Sending HTTP request
// =======================
// Time when the HTTP request is sent.
const sendStartTime = isCached ?
startTime :
Types.Timing.MicroSeconds(timing.requestTime * SECONDS_TO_MICROSECONDS + timing.sendStart * MILLISECONDS_TO_MICROSECONDS);
// Waiting
// =======================
// Time from when the send finished going to when the headers were received.
const waiting = isCached ?
Types.Timing.MicroSeconds(0) :
Types.Timing.MicroSeconds((timing.receiveHeadersEnd - timing.sendEnd) * MILLISECONDS_TO_MICROSECONDS);
// Download
// =======================
// Time from receipt of headers to the finish time.
const downloadStart = isCached ?
startTime :
Types.Timing.MicroSeconds(timing.requestTime * SECONDS_TO_MICROSECONDS + timing.receiveHeadersEnd * MILLISECONDS_TO_MICROSECONDS);
const download = isCached ? Types.Timing.MicroSeconds(endTime - request.receiveResponse.ts) :
Types.Timing.MicroSeconds(((finishTime || downloadStart) - downloadStart));
const totalTime = Types.Timing.MicroSeconds(networkDuration + processingDuration);
// Collect a few values from the timing info.
// If the Network request is cached, we zero out them.
const dnsLookup = isCached ?
Types.Timing.MicroSeconds(0) :
Types.Timing.MicroSeconds((timing.dnsEnd - timing.dnsStart) * MILLISECONDS_TO_MICROSECONDS);
const ssl = isCached ? Types.Timing.MicroSeconds(0) :
Types.Timing.MicroSeconds((timing.sslEnd - timing.sslStart) * MILLISECONDS_TO_MICROSECONDS);
const proxyNegotiation = isCached ?
Types.Timing.MicroSeconds(0) :
Types.Timing.MicroSeconds((timing.proxyEnd - timing.proxyStart) * MILLISECONDS_TO_MICROSECONDS);
const requestSent = isCached ?
Types.Timing.MicroSeconds(0) :
Types.Timing.MicroSeconds((timing.sendEnd - timing.sendStart) * MILLISECONDS_TO_MICROSECONDS);
const initialConnection = isCached ?
Types.Timing.MicroSeconds(0) :
Types.Timing.MicroSeconds((timing.connectEnd - timing.connectStart) * MILLISECONDS_TO_MICROSECONDS);
// Finally get some of the general data from the trace events.
const { frame, url, renderBlocking } = finalSendRequest.args.data;
const { encodedDataLength, decodedBodyLength } = request.resourceFinish ? request.resourceFinish.args.data : { encodedDataLength: 0, decodedBodyLength: 0 };
const { host, protocol, pathname, search } = new URL(url);
const isHttps = protocol === 'https:';
const requestingFrameUrl = Helpers.Trace.activeURLForFrameAtTime(frame, finalSendRequest.ts, rendererProcessesByFrame) || '';
// Construct a synthetic trace event for this network request.
const networkEvent = {
args: {
data: {
// All data we create from trace events should be added to |syntheticData|.
syntheticData: {
dnsLookup,
download,
downloadStart,
finishTime,
initialConnection,
isDiskCached,
isHttps,
isMemoryCached,
isPushedResource,
networkDuration,
processingDuration,
proxyNegotiation,
queueing,
redirectionDuration,
requestSent,
sendStartTime,
ssl,
stalled,
totalTime,
waiting,
},
// All fields below are from TraceEventsForNetworkRequest.
decodedBodyLength,
encodedDataLength,
frame,
fromServiceWorker: request.receiveResponse.args.data.fromServiceWorker,
host,
mimeType: request.receiveResponse.args.data.mimeType,
pathname,
priority: finalPriority,
initialPriority,
protocol,
redirects,
// In the event the property isn't set, assume non-blocking.
renderBlocking: renderBlocking ? renderBlocking : 'non_blocking',
requestId,
requestingFrameUrl,
requestMethod: finalSendRequest.args.data.requestMethod,
search,
statusCode: request.receiveResponse.args.data.statusCode,
stackTrace: finalSendRequest.args.data.stackTrace,
timing,
url,
},
},
cat: 'loading',
name: 'SyntheticNetworkRequest',
ph: "X" /* Types.TraceEvents.Phase.COMPLETE */,
dur: Types.Timing.MicroSeconds(endTime - startTime),
tdur: Types.Timing.MicroSeconds(endTime - startTime),
ts: Types.Timing.MicroSeconds(startTime),
tts: Types.Timing.MicroSeconds(startTime),
pid: finalSendRequest.pid,
tid: finalSendRequest.tid,
};
const requests = Platform.MapUtilities.getWithDefault(requestsByOrigin, host, () => {
return {
renderBlocking: [],
nonRenderBlocking: [],
all: [],
};
});
// For ease of rendering we sometimes want to differentiate between
// render-blocking and non-render-blocking, so we divide the data here.
if (networkEvent.args.data.renderBlocking === 'non_blocking') {
requests.nonRenderBlocking.push(networkEvent);
}
else {
requests.renderBlocking.push(networkEvent);
}
// However, there are also times where we just want to loop through all
// the captured requests, so here we store all of them together.
requests.all.push(networkEvent);
requestsByTime.push(networkEvent);
}
handlerState = 3 /* HandlerState.FINALIZED */;
}
export function data() {
if (handlerState !== 3 /* HandlerState.FINALIZED */) {
throw new Error('Network Request handler is not finalized');
}
return {
byOrigin: new Map(requestsByOrigin),
byTime: [...requestsByTime],
};
}
export function deps() {
return ['Meta'];
}
//# sourceMappingURL=NetworkRequestsHandler.js.map