UNPKG

chrome-devtools-frontend

Version:
444 lines (393 loc) • 16.6 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 {data as metaHandlerData} from './MetaHandler.js'; import {KnownEventName, KNOWN_EVENTS, type TraceEventHandlerName, HandlerState} from './types.js'; import * as Types from '../types/types.js'; const processes = new Map<Types.TraceEvents.ProcessID, RendererProcess>(); const traceEventToNode = new Map<RendererTraceEvent, RendererEventNode>(); const allRendererEvents: RendererTraceEvent[] = []; let nodeIdCount = 0; const makeRendererEventNodeId = (): RendererEventNodeId => (++nodeIdCount) as RendererEventNodeId; let handlerState = HandlerState.UNINITIALIZED; const makeRendererProcess = (): RendererProcess => ({ url: null, isOnMainFrame: false, threads: new Map(), }); const makeRendererThread = (): RendererThread => ({ name: null, events: [], }); const makeEmptyRendererEventTree = (): RendererEventTree => ({ nodes: new Map(), roots: new Set(), maxDepth: 0, }); const makeEmptyRendererEventNode = (event: RendererTraceEvent, id: RendererEventNodeId): RendererEventNode => ({ event, id, parentId: null, childrenIds: new Set(), depth: 0, }); const getOrCreateRendererProcess = (processes: Map<Types.TraceEvents.ProcessID, RendererProcess>, pid: Types.TraceEvents.ProcessID): RendererProcess => { return Platform.MapUtilities.getWithDefault(processes, pid, makeRendererProcess); }; const getOrCreateRendererThread = (process: RendererProcess, tid: Types.TraceEvents.ThreadID): RendererThread => { return Platform.MapUtilities.getWithDefault(process.threads, tid, makeRendererThread); }; export function reset(): void { processes.clear(); traceEventToNode.clear(); allRendererEvents.length = 0; nodeIdCount = -1; handlerState = HandlerState.UNINITIALIZED; } export function initialize(): void { if (handlerState !== HandlerState.UNINITIALIZED) { throw new Error('Renderer Handler was not reset'); } handlerState = HandlerState.INITIALIZED; } export function handleEvent(event: Types.TraceEvents.TraceEventData): void { if (handlerState !== HandlerState.INITIALIZED) { throw new Error('Renderer Handler is not initialized'); } if (Types.TraceEvents.isTraceEventInstant(event) || Types.TraceEvents.isTraceEventComplete(event)) { const process = getOrCreateRendererProcess(processes, event.pid); const thread = getOrCreateRendererThread(process, event.tid); thread.events.push(event); allRendererEvents.push(event); } } export async function finalize(): Promise<void> { if (handlerState !== HandlerState.INITIALIZED) { throw new Error('Renderer Handler is not initialized'); } const {mainFrameId, rendererProcessesByFrame, threadsInProcess} = metaHandlerData(); assignMeta(processes, mainFrameId, rendererProcessesByFrame, threadsInProcess); sanitizeProcesses(processes); buildHierarchy(processes, {filter: KNOWN_EVENTS}); sanitizeThreads(processes); handlerState = HandlerState.FINALIZED; } export function data(): RendererHandlerData { if (handlerState !== HandlerState.FINALIZED) { throw new Error('Renderer Handler is not finalized'); } return { processes: new Map(processes), traceEventToNode: new Map(traceEventToNode), allRendererEvents: [...allRendererEvents], }; } /** * Steps through all the renderer processes we've located so far in the meta * handler, obtaining their URL, checking whether they are the main frame, and * collecting each one of their threads' name. This meta handler's data is * assigned to the renderer handler's data. */ export function assignMeta( processes: Map<Types.TraceEvents.ProcessID, RendererProcess>, mainFrameId: string, rendererProcessesByFrame: Map<string, Map<Types.TraceEvents.ProcessID, {frame: Types.TraceEvents.TraceFrame}>>, threadsInProcess: Map<Types.TraceEvents.ProcessID, Map<Types.TraceEvents.ThreadID, Types.TraceEvents.TraceEventThreadName>>): void { assignOrigin(processes, mainFrameId, rendererProcessesByFrame); assignIsMainFrame(processes, mainFrameId, rendererProcessesByFrame); assignThreadName(processes, rendererProcessesByFrame, threadsInProcess); } /** * Assigns origins to all threads in all processes. * @see assignMeta */ export function assignOrigin( processes: Map<Types.TraceEvents.ProcessID, RendererProcess>, mainFrameId: string, rendererProcessesByFrame: Map<string, Map<Types.TraceEvents.ProcessID, {frame: Types.TraceEvents.TraceFrame}>>): void { for (const [frameId, renderProcessesByPid] of rendererProcessesByFrame) { for (const [pid, processInfo] of renderProcessesByPid) { const process = getOrCreateRendererProcess(processes, pid); // Sometimes a single process is responsible with rendering multiple // frames at the same time. For example, see https://crbug.com/1334563. // When this happens, we'd still like to assign a single url per process // so: 1) use the first frame rendered by this process as the url source // and 2) if there's a more "important" frame found, us its url instead. if (process.url === null /* first frame */ || frameId === mainFrameId /* more important frame */) { // If we are here, it's because we care about this process and the URL. But before we store it, we check if it is a valid URL by trying to create a URL object. If it isn't, we won't set it, and this process will be filtered out later. try { new URL(processInfo.frame.url); process.url = processInfo.frame.url; } catch (e) { process.url = null; } } } } } /** * Assigns whether or not a thread is the main frame to all threads in all processes. * @see assignMeta */ export function assignIsMainFrame( processes: Map<Types.TraceEvents.ProcessID, RendererProcess>, mainFrameId: string, rendererProcessesByFrame: Map<string, Map<Types.TraceEvents.ProcessID, {frame: Types.TraceEvents.TraceFrame}>>): void { for (const [frameId, renderProcessesByPid] of rendererProcessesByFrame) { for (const [pid] of renderProcessesByPid) { const process = getOrCreateRendererProcess(processes, pid); // We have this go in one direction; once a renderer has been flagged as // being on the main frame, we don't unset it to false if were to show up // in a subframe. Equally, if we already saw this renderer in a subframe, // but it becomes the main frame, the flag would get updated. if (frameId === mainFrameId) { process.isOnMainFrame = true; } } } } /** * Assigns the thread name to all threads in all processes. * @see assignMeta */ export function assignThreadName( processes: Map<Types.TraceEvents.ProcessID, RendererProcess>, rendererProcessesByFrame: Map<string, Map<Types.TraceEvents.ProcessID, {frame: Types.TraceEvents.TraceFrame}>>, threadsInProcess: Map<Types.TraceEvents.ProcessID, Map<Types.TraceEvents.ThreadID, Types.TraceEvents.TraceEventThreadName>>): void { for (const [, renderProcessesByPid] of rendererProcessesByFrame) { for (const [pid] of renderProcessesByPid) { const process = getOrCreateRendererProcess(processes, pid); for (const [tid, threadInfo] of threadsInProcess.get(pid) ?? []) { const thread = getOrCreateRendererThread(process, tid); thread.name = threadInfo?.args.name ?? `${tid}`; } } } } /** * Removes unneeded trace data opportunistically stored while handling events. * This currently does the following: * - Deletes processes with an unkonwn origin. */ export function sanitizeProcesses(processes: Map<Types.TraceEvents.ProcessID, RendererProcess>): void { for (const [pid, process] of processes) { // If the process had no url, or if it had a malformed url that could not be // parsed for some reason, or if it's an "about:" origin, delete it. // This is done because we don't really care about processes for which we // can't provide actionable insights to the user (e.g. about:blank pages). if (process.url === null) { processes.delete(pid); continue; } const asUrl = new URL(process.url); if (asUrl.protocol === 'about:') { processes.delete(pid); } } } /** * Removes unneeded trace data opportunistically stored while handling events. * This currently does the following: * - Deletes threads with no roots. */ export function sanitizeThreads(processes: Map<Types.TraceEvents.ProcessID, RendererProcess>): void { for (const [, process] of processes) { for (const [tid, thread] of process.threads) { // If the thread has no roots, also delete it. Otherwise, there's going to // be space taken, even though nothing is rendered in the track manager. if (!thread.tree?.roots.size) { process.threads.delete(tid); } } } } /** * Creates a hierarchical structure from the trace events. Each thread in each * process will contribute to their own individual hierarchy. * * The trace data comes in as a contiguous array of events, against which we * make a couple of assumptions: * * 1. Events are temporally-ordered in terms of start time (though they're * not necessarily ordered as such in the data stream). * 2. If event B's start and end times are within event A's time boundaries * we assume that A is the parent of B. * * Therefore we expect to reformulate something like: * * [ Task A ][ Task B ][ Task C ][ Task D ][ Task E ] * * Into something hierarchically-arranged like below: * * |------------- Task A -------------||-- Task E --| * |-- Task B --||-- Task D --| * |- Task C -| */ export function buildHierarchy( processes: Map<Types.TraceEvents.ProcessID, RendererProcess>, options: {filter: {has: (name: KnownEventName) => boolean}}): void { for (const [, process] of processes) { for (const [, thread] of process.threads) { // Step 1. Massage the data. Helpers.Trace.sortTraceEventsInPlace(thread.events); // Step 2. Build the tree. thread.tree = treify(thread.events, options); } } } /** * Builds a hierarchy of the trace events in a particular thread of a * particular process, assuming that they're sorted, by iterating through all of * the events in order. * * The approach is analogous to how a parser would be implemented. A stack * maintains local context. A scanner peeks and pops from the data stream. * Various "tokens" (events) are treated as "whitespace" (ignored). * * The tree starts out empty and is populated as the hierarchy is built. The * nodes are also assumed to be created empty, with no known parent or children. * * Complexity: O(n), where n = number of events */ export function treify( events: RendererTraceEvent[], options: {filter: {has: (name: KnownEventName) => boolean}}): RendererEventTree { const stack = []; // Reset the node id counter for every new renderer. nodeIdCount = -1; const tree = makeEmptyRendererEventTree(); for (let i = 0; i < events.length; i++) { const event = events[i]; // If the current event should not be part of the tree, then simply proceed // with the next event. if (!options.filter.has(event.name as KnownEventName)) { continue; } const duration = event.dur || 0; const nodeId = makeRendererEventNodeId(); const node = makeEmptyRendererEventNode(event, nodeId); // If the parent stack is empty, then the current event is a root. Create a // node for it, mark it as a root, then proceed with the next event. if (stack.length === 0) { tree.nodes.set(nodeId, node); tree.roots.add(nodeId); event.selfTime = Types.Timing.MicroSeconds(duration); stack.push(node); tree.maxDepth = Math.max(tree.maxDepth, stack.length); traceEventToNode.set(event, node); continue; } const parentNode = stack.at(-1); if (parentNode === undefined) { throw new Error('Impossible: no parent node found in the stack'); } const parentEvent = parentNode.event; const begin = event.ts; const parentBegin = parentEvent.ts; const parentDuration = parentEvent.dur || 0; const end = begin + duration; const parentEnd = parentBegin + parentDuration; // Check the relationship between the parent event at the top of the stack, // and the current event being processed. There are only 4 distinct // possiblities, only 2 of them actually valid, given the assumed sorting: // 1. Current event starts before the parent event, ends whenever. (invalid) // 2. Current event starts after the parent event, ends whenever. (valid) // 3. Current event starts during the parent event, ends after. (invalid) // 4. Current event starts and ends during the parent event. (valid) // 1. If the current event starts before the parent event, then the data is // not sorted properly, messed up some way, or this logic is incomplete. const startsBeforeParent = begin < parentBegin; if (startsBeforeParent) { throw new Error('Impossible: current event starts before the parent event'); } // 2. If the current event starts after the parent event, then it's a new // parent. Pop, then handle current event again. const startsAfterParent = begin >= parentEnd; if (startsAfterParent) { stack.pop(); i--; // The last created node has been discarded, so discard this id. nodeIdCount--; continue; } // 3. If the current event starts during the parent event, but ends after // it, then the data is messed up some way. const endsAfterParent = end > parentEnd; if (endsAfterParent) { throw new Error('Impossible: current event starts during the parent event'); } // 4. The only remaining case is the common case, where the current event is // contained within the parent event. Create a node for the current // event, establish the parent/child relationship, then proceed with the // next event. tree.nodes.set(nodeId, node); node.depth = stack.length; node.parentId = parentNode.id; parentNode.childrenIds.add(nodeId); event.selfTime = Types.Timing.MicroSeconds(duration); if (parentEvent.selfTime !== undefined) { parentEvent.selfTime = Types.Timing.MicroSeconds(parentEvent.selfTime - (event.dur || 0)); } stack.push(node); tree.maxDepth = Math.max(tree.maxDepth, stack.length); traceEventToNode.set(event, node); } return tree; } export const FORCED_LAYOUT_EVENT_NAMES = new Set([ KnownEventName.Layout, ]); export const FORCED_RECALC_STYLE_EVENTS = new Set([ KnownEventName.RecalculateStyles, KnownEventName.UpdateLayoutTree, ]); export function deps(): TraceEventHandlerName[] { return ['Meta', 'Samples']; } export interface RendererHandlerData { processes: Map<Types.TraceEvents.ProcessID, RendererProcess>; traceEventToNode: Map<RendererTraceEvent, RendererEventNode>; allRendererEvents: RendererTraceEvent[]; } export interface RendererProcess { // In an ideal world this would be modelled as a URL, but URLs cannot be sent // between the main thread and workers, so we have to store it as a string. url: string|null; isOnMainFrame: boolean; threads: Map<Types.TraceEvents.ThreadID, RendererThread>; } export interface RendererThread { name: string|null; events: RendererTraceEvent[]; tree?: RendererEventTree; } interface RendererEventData { selfTime: Types.Timing.MicroSeconds; initiator: RendererTraceEvent; parent?: RendererTraceEvent; hotFunctionsStackTraces: Types.TraceEvents.TraceEventCallFrame[][]; } export type RendererTraceEvent = Types.TraceEvents.TraceEventRendererData&Partial<RendererEventData>; export interface RendererEventTree { nodes: Map<RendererEventNodeId, RendererEventNode>; roots: Set<RendererEventNodeId>; maxDepth: number; } export interface RendererEventNode { event: RendererTraceEvent; depth: number; id: RendererEventNodeId; parentId?: RendererEventNodeId|null; childrenIds: Set<RendererEventNodeId>; } class RendererEventNodeIdTag { /* eslint-disable-next-line no-unused-private-class-members */ readonly #tag: (symbol|undefined); } export type RendererEventNodeId = number&RendererEventNodeIdTag;