UNPKG

chrome-devtools-frontend

Version:
405 lines (354 loc) 17.5 kB
// 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 {type TraceEventHandlerName, HandlerState} from './types.js'; import {data as metaHandlerData} from './MetaHandler.js'; import * as Types from '../types/types.js'; const MILLISECONDS_TO_MICROSECONDS = 1000; const SECONDS_TO_MICROSECONDS = 1000000; // Network requests from traces are actually formed of 5 trace records. // This handler tracks all trace records based on the request ID, and // then creates a new synthetic trace event for those network requests. // // This interface, then, defines the shape of the object we intend to // keep for each request in the trace. In the finalize we will convert // these 5 types of trace records to a synthetic complete event that // represents a composite of these trace records. interface TraceEventsForNetworkRequest { willSendRequests?: Types.TraceEvents.TraceEventResourceWillSendRequest[]; sendRequests?: Types.TraceEvents.TraceEventResourceSendRequest[]; receiveResponse?: Types.TraceEvents.TraceEventResourceReceiveResponse; resourceFinish?: Types.TraceEvents.TraceEventResourceFinish; receivedData?: Types.TraceEvents.TraceEventResourceReceivedData[]; } interface NetworkRequestData { byOrigin: Map<string, { renderBlocking: Types.TraceEvents.TraceEventSyntheticNetworkRequest[], nonRenderBlocking: Types.TraceEvents.TraceEventSyntheticNetworkRequest[], all: Types.TraceEvents.TraceEventSyntheticNetworkRequest[], }>; byTime: Types.TraceEvents.TraceEventSyntheticNetworkRequest[]; } const requestMap = new Map<string, TraceEventsForNetworkRequest>(); const requestsByOrigin = new Map<string, { renderBlocking: Types.TraceEvents.TraceEventSyntheticNetworkRequest[], nonRenderBlocking: Types.TraceEvents.TraceEventSyntheticNetworkRequest[], all: Types.TraceEvents.TraceEventSyntheticNetworkRequest[], }>(); const requestsByTime: Types.TraceEvents.TraceEventSyntheticNetworkRequest[] = []; function storeTraceEventWithRequestId<K extends keyof TraceEventsForNetworkRequest>( requestId: string, key: K, value: TraceEventsForNetworkRequest[K]): void { 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] as Types.TraceEvents.TraceEventData[]; const values = value as Types.TraceEvents.TraceEventData[]; target.push(...values); } else { traceEvents[key] = value; } } function firstPositiveValueInList(entries: number[]): number { 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 = HandlerState.UNINITIALIZED; export function reset(): void { requestsByOrigin.clear(); requestMap.clear(); requestsByTime.length = 0; handlerState = HandlerState.UNINITIALIZED; } export function initialize(): void { handlerState = HandlerState.INITIALIZED; } export function handleEvent(event: Types.TraceEvents.TraceEventData): void { if (handlerState !== HandlerState.INITIALIZED) { throw new Error('Network Request handler is not initialized'); } 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; } } export async function finalize(): Promise<void> { if (handlerState !== 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 subresources, 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: Types.TraceEvents.TraceEventSyntheticNetworkRedirect[] = []; 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, ts, dur, }); } const firstSendRequest = request.sendRequests[0]; const finalSendRequest = request.sendRequests[request.sendRequests.length - 1]; const {timing} = request.receiveResponse.args.data; // No timing indicates data URLs, which we ignore. if (!timing) { continue; } // 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 ? Types.Timing.MicroSeconds(request.resourceFinish.args.data.finishTime * SECONDS_TO_MICROSECONDS) : Types.Timing.MicroSeconds(endTime); // Network duration // ======================= // Time spent on the network. const networkDuration = 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 = Types.Timing.MicroSeconds(Platform.NumberUtilities.clamp( (timing.requestTime * SECONDS_TO_MICROSECONDS - endRedirectTime), 0, Number.MAX_VALUE)); // Stalled // ======================= // The amount of time stalled 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 = Types.Timing.MicroSeconds(firstPositiveValueInList([ timing.dnsStart * MILLISECONDS_TO_MICROSECONDS, timing.connectStart * MILLISECONDS_TO_MICROSECONDS, timing.sendStart * MILLISECONDS_TO_MICROSECONDS, (request.receiveResponse.ts - endRedirectTime), ])); // Waiting // ======================= // Time from when the send finished going to when the headers were received. const waiting = Types.Timing.MicroSeconds((timing.receiveHeadersEnd - timing.sendEnd) * MILLISECONDS_TO_MICROSECONDS); // Download // ======================= // Time from receipt of headers to the finish time. const downloadStart = Types.Timing.MicroSeconds( timing.requestTime * SECONDS_TO_MICROSECONDS + timing.receiveHeadersEnd * MILLISECONDS_TO_MICROSECONDS); const download = Types.Timing.MicroSeconds(((finishTime || downloadStart) - downloadStart)); const totalTime = Types.Timing.MicroSeconds(networkDuration + processingDuration); // Collate a few values from the timing info. const dnsLookup = Types.Timing.MicroSeconds((timing.dnsEnd - timing.dnsStart) * MILLISECONDS_TO_MICROSECONDS); const ssl = Types.Timing.MicroSeconds((timing.sslEnd - timing.sslStart) * MILLISECONDS_TO_MICROSECONDS); const proxyNegotiation = Types.Timing.MicroSeconds((timing.proxyEnd - timing.proxyStart) * MILLISECONDS_TO_MICROSECONDS); const requestSent = Types.Timing.MicroSeconds((timing.sendEnd - timing.sendStart) * MILLISECONDS_TO_MICROSECONDS); const initialConnection = Types.Timing.MicroSeconds((timing.connectEnd - timing.connectStart) * MILLISECONDS_TO_MICROSECONDS); // Finally get some of the general data from the trace events. const {priority, frame, url, renderBlocking} = finalSendRequest.args.data; const {mimeType, fromCache, fromServiceWorker} = request.receiveResponse.args.data; const {encodedDataLength, decodedBodyLength} = request.resourceFinish ? request.resourceFinish.args.data : {encodedDataLength: 0, decodedBodyLength: 0}; const {receiveHeadersEnd, requestTime, sendEnd, sendStart, sslStart} = timing; const {host, protocol, pathname, search} = new URL(url); const isHttps = protocol === 'https:'; const renderProcesses = rendererProcessesByFrame.get(frame); const processInfo = renderProcesses?.get(finalSendRequest.pid); const requestingFrameUrl = processInfo ? processInfo.frame.url : ''; // Construct a synthetic trace event for this network request. const networkEvent: Types.TraceEvents.TraceEventSyntheticNetworkRequest = { args: { data: { decodedBodyLength, dnsLookup, download, encodedDataLength, finishTime, frame, fromCache, fromServiceWorker, initialConnection, host, isHttps, mimeType, networkDuration, pathname, search, priority, processingDuration, protocol, proxyNegotiation, redirectionDuration, queueing, receiveHeadersEnd: Types.Timing.MicroSeconds(receiveHeadersEnd), redirects, requestId, // In the event the property isn't set, assume non-blocking. renderBlocking: renderBlocking ? renderBlocking : 'non_blocking', requestSent, requestTime, requestingFrameUrl, sendEnd: Types.Timing.MicroSeconds(sendEnd * MILLISECONDS_TO_MICROSECONDS), sendStart: Types.Timing.MicroSeconds(sendStart * MILLISECONDS_TO_MICROSECONDS), ssl, sslStart: Types.Timing.MicroSeconds(sslStart * MILLISECONDS_TO_MICROSECONDS), stalled, statusCode: request.receiveResponse.args.data.statusCode, stackTrace: finalSendRequest.args.data.stackTrace, totalTime, url, waiting, }, }, cat: 'loading', name: 'SyntheticNetworkRequest', ph: 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, }; // 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 override the request data args based on the timestamps of the // various events in the case where the fromCache flag is set to true. // 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. if (networkEvent.args.data.fromCache) { // Zero out the network-based timing info. networkEvent.args.data.queueing = Types.Timing.MicroSeconds(0); networkEvent.args.data.waiting = Types.Timing.MicroSeconds(0); networkEvent.args.data.dnsLookup = Types.Timing.MicroSeconds(0); networkEvent.args.data.initialConnection = Types.Timing.MicroSeconds(0); networkEvent.args.data.ssl = Types.Timing.MicroSeconds(0); networkEvent.args.data.requestSent = Types.Timing.MicroSeconds(0); networkEvent.args.data.proxyNegotiation = Types.Timing.MicroSeconds(0); networkEvent.args.data.networkDuration = Types.Timing.MicroSeconds(0); // Update the stalled and download values. const endStalled = request.receiveResponse ? request.receiveResponse.ts : startTime; networkEvent.args.data.stalled = Types.Timing.MicroSeconds(endStalled - startTime); networkEvent.args.data.download = Types.Timing.MicroSeconds(endTime - endStalled); } 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 = HandlerState.FINALIZED; } export function data(): NetworkRequestData { if (handlerState !== HandlerState.FINALIZED) { throw new Error('Network Request handler is not finalized'); } return { byOrigin: new Map(requestsByOrigin), byTime: [...requestsByTime], }; } export function deps(): TraceEventHandlerName[] { return ['Meta']; }