UNPKG

chrome-devtools-frontend

Version:
513 lines (456 loc) • 21.9 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 * as Helpers from '../helpers/helpers.js'; import * as Types from '../types/types.js'; // We track the renderer processes we see in each frame on the way through the trace. const rendererProcessesByFrameId: FrameProcessData = new Map(); // We will often want to key data by Frame IDs, and commonly we'll care most // about the main frame's ID, so we store and expose that. let mainFrameId = ''; let mainFrameURL = ''; const framesByProcessId = new Map<Types.Events.ProcessID, Map<string, Types.Events.TraceFrame>>(); // We will often want to key data by the browser process, GPU process and top // level renderer IDs, so keep a track on those. let browserProcessId: Types.Events.ProcessID = Types.Events.ProcessID(-1); let browserThreadId: Types.Events.ThreadID = Types.Events.ThreadID(-1); let gpuProcessId: Types.Events.ProcessID = Types.Events.ProcessID(-1); let gpuThreadId: Types.Events.ThreadID = Types.Events.ThreadID(-1); let viewportRect: DOMRect|null = null; let devicePixelRatio: number|null = null; const processNames = new Map<Types.Events.ProcessID, Types.Events.ProcessName>(); const topLevelRendererIds = new Set<Types.Events.ProcessID>(); const traceBounds: Types.Timing.TraceWindowMicro = { min: Types.Timing.Micro(Number.POSITIVE_INFINITY), max: Types.Timing.Micro(Number.NEGATIVE_INFINITY), range: Types.Timing.Micro(Number.POSITIVE_INFINITY), }; /** * These represent the user navigating. Values such as First Contentful Paint, * etc, are relative to the navigation. * * We store navigation events both by the frame and navigation ID. This means * when we need to look them up, we can use whichever ID we have. * * Note that these Maps will have the same values in them; these are just keyed * differently to make look-ups easier. * * We also additionally maintain an array of only navigations that occurred on * the main frame. In many places in the UI we only care about highlighting * main frame navigations, so calculating this list here is better than * filtering either of the below maps over and over again at the UI layer. */ const navigationsByFrameId = new Map<string, Types.Events.NavigationStart[]>(); const navigationsByNavigationId = new Map<string, Types.Events.NavigationStart>(); const finalDisplayUrlByNavigationId = new Map<string, string>(); const mainFrameNavigations: Types.Events.NavigationStart[] = []; // Represents all the threads in the trace, organized by process. This is mostly for internal // bookkeeping so that during the finalize pass we can obtain the main and browser thread IDs. const threadsInProcess = new Map<Types.Events.ProcessID, Map<Types.Events.ThreadID, Types.Events.ThreadName>>(); let traceStartedTimeFromTracingStartedEvent = Types.Timing.Micro(-1); const eventPhasesOfInterestForTraceBounds = new Set([ Types.Events.Phase.BEGIN, Types.Events.Phase.END, Types.Events.Phase.COMPLETE, Types.Events.Phase.INSTANT, ]); // Tracks if the trace is a generic trace, which here means that it did not come from athe DevTools Performance Panel recording. // We assume a trace is generic, and mark it as not generic if we see any of: // - TracingStartedInPage // - TracingStartedInBrowser // - TracingSessionIdForWorker // These are all events which indicate this is a Chrome browser trace. let traceIsGeneric = true; const CHROME_WEB_TRACE_EVENTS = new Set([ Types.Events.Name.TRACING_STARTED_IN_PAGE, Types.Events.Name.TRACING_SESSION_ID_FOR_WORKER, Types.Events.Name.TRACING_STARTED_IN_BROWSER, ]); export function reset(): void { navigationsByFrameId.clear(); navigationsByNavigationId.clear(); finalDisplayUrlByNavigationId.clear(); processNames.clear(); mainFrameNavigations.length = 0; browserProcessId = Types.Events.ProcessID(-1); browserThreadId = Types.Events.ThreadID(-1); gpuProcessId = Types.Events.ProcessID(-1); gpuThreadId = Types.Events.ThreadID(-1); viewportRect = null; topLevelRendererIds.clear(); threadsInProcess.clear(); rendererProcessesByFrameId.clear(); framesByProcessId.clear(); traceBounds.min = Types.Timing.Micro(Number.POSITIVE_INFINITY); traceBounds.max = Types.Timing.Micro(Number.NEGATIVE_INFINITY); traceBounds.range = Types.Timing.Micro(Number.POSITIVE_INFINITY); traceStartedTimeFromTracingStartedEvent = Types.Timing.Micro(-1); traceIsGeneric = true; } function updateRendererProcessByFrame(event: Types.Events.Event, frame: Types.Events.TraceFrame): void { const framesInProcessById = Platform.MapUtilities.getWithDefault(framesByProcessId, frame.processId, () => new Map()); framesInProcessById.set(frame.frame, frame); const rendererProcessInFrame = Platform.MapUtilities.getWithDefault( rendererProcessesByFrameId, frame.frame, () => new Map< Types.Events.ProcessID, Array<{frame: Types.Events.TraceFrame, window: Types.Timing.TraceWindowMicro}>>()); const rendererProcessInfo = Platform.MapUtilities.getWithDefault(rendererProcessInFrame, frame.processId, () => { return []; }); const lastProcessData = rendererProcessInfo.at(-1); // Only store a new entry if the URL changed, otherwise it's just // redundant information. if (lastProcessData && lastProcessData.frame.url === frame.url) { return; } // For now we store the time of the event as the min. In the finalize we step // through each of these windows and update their max and range values. rendererProcessInfo.push({ frame, window: { min: event.ts, max: Types.Timing.Micro(0), range: Types.Timing.Micro(0), }, }); } export function handleEvent(event: Types.Events.Event): void { if (traceIsGeneric && CHROME_WEB_TRACE_EVENTS.has(event.name as Types.Events.Name)) { traceIsGeneric = false; } if (Types.Events.isProcessName(event)) { processNames.set(event.pid, event); } // If there is a timestamp (which meta events do not have), and the event does // not end with ::UMA then it, and the event is in the set of valid phases, // then it should be included for the purposes of calculating the trace bounds. // The UMA events in particular seem to be reported on page unloading, which // often extends the bounds of the trace unhelpfully. if (event.ts !== 0 && !event.name.endsWith('::UMA') && eventPhasesOfInterestForTraceBounds.has(event.ph)) { traceBounds.min = Types.Timing.Micro(Math.min(event.ts, traceBounds.min)); const eventDuration = event.dur ?? Types.Timing.Micro(0); traceBounds.max = Types.Timing.Micro(Math.max(event.ts + eventDuration, traceBounds.max)); } if (Types.Events.isProcessName(event) && (event.args.name === 'Browser' || event.args.name === 'HeadlessBrowser')) { browserProcessId = event.pid; return; } if (Types.Events.isProcessName(event) && (event.args.name === 'Gpu' || event.args.name === 'GPU Process')) { gpuProcessId = event.pid; return; } if (Types.Events.isThreadName(event) && event.args.name === 'CrGpuMain') { gpuThreadId = event.tid; return; } if (Types.Events.isThreadName(event) && event.args.name === 'CrBrowserMain') { browserThreadId = event.tid; } if (Types.Events.isMainFrameViewport(event) && viewportRect === null) { const rectAsArray = event.args.data.viewport_rect; const viewportX = rectAsArray[0]; const viewportY = rectAsArray[1]; const viewportWidth = rectAsArray[2]; const viewportHeight = rectAsArray[5]; viewportRect = new DOMRect(viewportX, viewportY, viewportWidth, viewportHeight); devicePixelRatio = event.args.data.dpr; } // The TracingStartedInBrowser event includes the data on which frames are // in scope at the start of the trace. We use this to identify the frame with // no parent, i.e. the top level frame. if (Types.Events.isTracingStartedInBrowser(event)) { traceStartedTimeFromTracingStartedEvent = event.ts; if (!event.args.data) { throw new Error('No frames found in trace data'); } for (const frame of (event.args.data.frames ?? [])) { updateRendererProcessByFrame(event, frame); if (!frame.parent) { topLevelRendererIds.add(frame.processId); } /** * The code here uses a few different methods to try to determine the main frame. * The ideal is that the frames have two flags present: * * 1. isOutermostMainFrame (added in April 2024 - crrev.com/c/5424783) * 2. isInPrimaryMainFrame (added in June 2024 - crrev.com/c/5595033) * * The frame where both of these are set to `true` is the main frame. The * reason we need both of these flags to have 100% confidence is because * with the introduction of MPArch and pre-rendering, we can have other * frames that are the outermost frame, but are not the primary process. * Relying on isOutermostMainFrame in isolation caused the engine to * incorrectly identify the wrong frame as main (see crbug.com/343873756). * * See https://source.chromium.org/chromium/chromium/src/+/main:docs/frame_trees.md * for a bit more context on FrameTrees in Chromium. * * To avoid breaking entirely for traces pre-June 2024 that don't have * both of these flags, we will fallback to less accurate methods: * * 1. If we have isOutermostMainFrame, we will use that * (and accept we might get it wrong) * 2. If we don't have isOutermostMainFrame, we fallback to finding a * frame that has a URL, but doesn't have a parent. This is a crude * guess at the main frame...but better than nothing and is historically * how DevTools identified the main frame. */ const traceHasPrimaryMainFrameFlag = 'isInPrimaryMainFrame' in frame; const traceHasOutermostMainFrameFlag = 'isOutermostMainFrame' in frame; if (traceHasPrimaryMainFrameFlag && traceHasOutermostMainFrameFlag) { // Ideal situation: identify the main frame as the one that has both these flags set to true. if (frame.isInPrimaryMainFrame && frame.isOutermostMainFrame) { mainFrameId = frame.frame; mainFrameURL = frame.url; } } else if (traceHasOutermostMainFrameFlag) { // Less ideal: "guess" at the main thread by using this flag. if (frame.isOutermostMainFrame) { mainFrameId = frame.frame; mainFrameURL = frame.url; } // Worst case: guess by seeing if the frame doesn't have a parent, and does have a URL. } else if (!frame.parent && frame.url) { mainFrameId = frame.frame; mainFrameURL = frame.url; } } return; } // FrameCommittedInBrowser events tell us information about each frame // and we use these to track how long each individual renderer is active // for. We track all renderers here (top level and those in frames), but // for convenience we also populate a set of top level renderer IDs. if (Types.Events.isFrameCommittedInBrowser(event)) { const frame = event.args.data; if (!frame) { return; } updateRendererProcessByFrame(event, frame); if (frame.parent) { return; } topLevelRendererIds.add(frame.processId); return; } if (Types.Events.isCommitLoad(event)) { const frameData = event.args.data; if (!frameData) { return; } const {frame, name, url} = frameData; updateRendererProcessByFrame(event, {processId: event.pid, frame, name, url}); return; } // Track all threads based on the process & thread IDs. if (Types.Events.isThreadName(event)) { const threads = Platform.MapUtilities.getWithDefault(threadsInProcess, event.pid, () => new Map()); threads.set(event.tid, event); return; } // Track all navigation events. Note that there can be navigation start events // but where the documentLoaderURL is empty. As far as the trace rendering is // concerned, these events are noise so we filter them out here. // (The filtering of empty URLs is done in the isNavigationStart check) if (Types.Events.isNavigationStart(event) && event.args.data) { const navigationId = event.args.data.navigationId; if (navigationsByNavigationId.has(navigationId)) { // We have only ever seen this situation once, in crbug.com/1503982, where the user ran: // window.location.href = 'javascript:console.log("foo")' // In this situation two identical navigationStart events are emitted with the same data, URL and ID. // So, in this situation we drop/ignore any subsequent navigations if we have already seen that ID. return; } navigationsByNavigationId.set(navigationId, event); finalDisplayUrlByNavigationId.set(navigationId, event.args.data.documentLoaderURL); const frameId = event.args.frame; const existingFrameNavigations = navigationsByFrameId.get(frameId) || []; existingFrameNavigations.push(event); navigationsByFrameId.set(frameId, existingFrameNavigations); if (frameId === mainFrameId) { mainFrameNavigations.push(event); } return; } // Update `finalDisplayUrlByNavigationId` to reflect the latest redirect for each navigation. if (Types.Events.isResourceSendRequest(event)) { if (event.args.data.resourceType !== 'Document') { return; } const maybeNavigationId = event.args.data.requestId; const navigation = navigationsByNavigationId.get(maybeNavigationId); if (!navigation) { return; } finalDisplayUrlByNavigationId.set(maybeNavigationId, event.args.data.url); return; } // Update `finalDisplayUrlByNavigationId` to reflect history API navigations. if (Types.Events.isDidCommitSameDocumentNavigation(event)) { if (event.args.render_frame_host.frame_type !== 'PRIMARY_MAIN_FRAME') { return; } const navigation = mainFrameNavigations.at(-1); const key = navigation?.args.data?.navigationId ?? ''; finalDisplayUrlByNavigationId.set(key, event.args.url); return; } } export async function finalize(): Promise<void> { // We try to set the minimum time by finding the event with the smallest // timestamp. However, if we also got a timestamp from the // TracingStartedInBrowser event, we should always use that. // But in some traces (for example, CPU profiles) we do not get that event, // hence why we need to check we got a timestamp from it before setting it. if (traceStartedTimeFromTracingStartedEvent >= 0) { traceBounds.min = traceStartedTimeFromTracingStartedEvent; } traceBounds.range = Types.Timing.Micro(traceBounds.max - traceBounds.min); // If we go from foo.com to example.com we will get a new renderer, and // therefore the "top level renderer" will have a different PID as it has // changed. Here we step through each renderer process and updated its window // bounds, such that we end up with the time ranges in the trace for when // each particular renderer started and stopped being the main renderer // process. for (const [, processWindows] of rendererProcessesByFrameId) { // Sort the windows by time; we cannot assume by default they arrive via // events in time order. Because we set the window bounds per-process based // on the time of the current + next window, we need them sorted in ASC // order. const processWindowValues = [...processWindows.values()].flat().sort((a, b) => { return a.window.min - b.window.min; }); for (let i = 0; i < processWindowValues.length; i++) { const currentWindow = processWindowValues[i]; const nextWindow = processWindowValues[i + 1]; // For the last window we set its max to be positive infinity. // TODO: Move the trace bounds handler into meta so we can clamp first and last windows. if (!nextWindow) { currentWindow.window.max = Types.Timing.Micro(traceBounds.max); currentWindow.window.range = Types.Timing.Micro(traceBounds.max - currentWindow.window.min); } else { currentWindow.window.max = Types.Timing.Micro(nextWindow.window.min - 1); currentWindow.window.range = Types.Timing.Micro(currentWindow.window.max - currentWindow.window.min); } } } // Frame ids which we didn't register using either the TracingStartedInBrowser or // the FrameCommittedInBrowser events are considered noise, so we filter them out, as well // as the navigations that belong to such frames. for (const [frameId, navigations] of navigationsByFrameId) { // The frames in the rendererProcessesByFrameId map come only from the // TracingStartedInBrowser and FrameCommittedInBrowser events, so we can use it as point // of comparison to determine if a frameId should be discarded. if (rendererProcessesByFrameId.has(frameId)) { continue; } navigationsByFrameId.delete(frameId); for (const navigation of navigations) { if (!navigation.args.data) { continue; } navigationsByNavigationId.delete(navigation.args.data.navigationId); } } // Sometimes in traces the TracingStartedInBrowser event can give us an // incorrect initial URL for the main frame's URL - about:blank or the URL of // the previous page. This doesn't matter too much except we often use this // URL as the visual name of the trace shown to the user (e.g. in the history // dropdown). We can be more accurate by finding the first main frame // navigation, and using its URL, if we have it. // However, to avoid doing this in a case where the first navigation is far // into the trace's lifecycle, we only do this in situations where the first // navigation happened very soon (0.5 seconds) after the trace started // recording. const firstMainFrameNav = mainFrameNavigations.at(0); const firstNavTimeThreshold = Helpers.Timing.secondsToMicro(Types.Timing.Seconds(0.5)); if (firstMainFrameNav) { const navigationIsWithinThreshold = firstMainFrameNav.ts - traceBounds.min < firstNavTimeThreshold; if (firstMainFrameNav.args.data?.isOutermostMainFrame && firstMainFrameNav.args.data?.documentLoaderURL && navigationIsWithinThreshold) { mainFrameURL = firstMainFrameNav.args.data.documentLoaderURL; } } } export interface MetaHandlerData { traceIsGeneric: boolean; traceBounds: Types.Timing.TraceWindowMicro; browserProcessId: Types.Events.ProcessID; processNames: Map<Types.Events.ProcessID, Types.Events.ProcessName>; browserThreadId: Types.Events.ThreadID; gpuProcessId: Types.Events.ProcessID; navigationsByFrameId: Map<string, Types.Events.NavigationStart[]>; navigationsByNavigationId: Map<string, Types.Events.NavigationStart>; /** * The user-visible URL displayed to users in the address bar. * This captures: * - resolving all redirects * - history API pushState * * Given no redirects or history API usages, this is just the navigation event's documentLoaderURL. * * Note: empty string special case denotes the duration of the trace between the start * and the first navigation. If there is no history API navigation during this time, * there will be no value for empty string. **/ finalDisplayUrlByNavigationId: Map<string, string>; threadsInProcess: Map<Types.Events.ProcessID, Map<Types.Events.ThreadID, Types.Events.ThreadName>>; mainFrameId: string; mainFrameURL: string; /** * A frame can have multiple renderer processes, at the same time, * a renderer process can have multiple URLs. This map tracks the * processes active on a given frame, with the time window in which * they were active. Because a renderer process might have multiple * URLs, each process in each frame has an array of windows, with an * entry for each URL it had. */ rendererProcessesByFrame: FrameProcessData; topLevelRendererIds: Set<Types.Events.ProcessID>; frameByProcessId: Map<Types.Events.ProcessID, Map<string, Types.Events.TraceFrame>>; mainFrameNavigations: Types.Events.NavigationStart[]; gpuThreadId?: Types.Events.ThreadID; viewportRect?: DOMRect; devicePixelRatio?: number; } // Each frame has a single render process at a given time but it can have // multiple render processes during a trace, for example if a navigation // occurred in the frame. This map tracks the process that was active for // each frame at each point in time. Also, because a process can be // assigned to multiple URLs, there is a window for each URL a process // was assigned. // // Note that different sites always end up in different render // processes, however two different URLs can point to the same site. // For example: https://google.com and https://maps.google.com point to // the same site. // Read more about this in // https://developer.chrome.com/articles/renderingng-architecture/#threads // and https://web.dev/same-site-same-origin/ export type FrameProcessData = Map<string, Map<Types.Events.ProcessID, Array<{frame: Types.Events.TraceFrame, window: Types.Timing.TraceWindowMicro}>>>; export function data(): MetaHandlerData { return { traceBounds: {...traceBounds}, browserProcessId, browserThreadId, processNames, gpuProcessId, gpuThreadId: gpuThreadId === Types.Events.ThreadID(-1) ? undefined : gpuThreadId, viewportRect: viewportRect || undefined, devicePixelRatio: devicePixelRatio ?? undefined, mainFrameId, mainFrameURL, navigationsByFrameId, navigationsByNavigationId, finalDisplayUrlByNavigationId, threadsInProcess, rendererProcessesByFrame: rendererProcessesByFrameId, topLevelRendererIds, frameByProcessId: framesByProcessId, mainFrameNavigations, traceIsGeneric, }; }