| // 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; |