UNPKG

chrome-devtools-frontend

Version:
321 lines (292 loc) • 11 kB
// Copyright 2023 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 type * as Platform from '../../../core/platform/platform.js'; import * as Types from '../types/types.js'; import {eventIsInBounds} from './Timing.js'; let nodeIdCount = 0; export const makeTraceEntryNodeId = (): TraceEntryNodeId => (++nodeIdCount) as TraceEntryNodeId; export const makeEmptyTraceEntryTree = (): TraceEntryTree => ({ roots: new Set(), maxDepth: 0, }); export const makeEmptyTraceEntryNode = (entry: Types.Events.Event, id: TraceEntryNodeId): TraceEntryNode => ({ entry, id, parent: null, children: [], depth: 0, }); export interface TraceEntryTree { roots: Set<TraceEntryNode>; maxDepth: number; } /** Node in the graph that defines all parent/child relationships. */ export interface TraceEntryNode { entry: Types.Events.Event; depth: number; selfTime?: Types.Timing.Micro; id: TraceEntryNodeId; parent: TraceEntryNode|null; children: TraceEntryNode[]; } export type TraceEntryNodeId = Platform.Brand.Brand<number, 'traceEntryNodeIdTag'>; /** * Builds a hierarchy of the entries (trace events and profile calls) 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(entries: Types.Events.Event[], options?: { filter: {has: (name: Types.Events.Name) => boolean}, }): {tree: TraceEntryTree, entryToNode: Map<Types.Events.Event, TraceEntryNode>} { // As we construct the tree, store a map of each entry to its node. This // means if you are iterating over a list of RendererEntry events you can // easily look up that node in the tree. const entryToNode = new Map<Types.Events.Event, TraceEntryNode>(); const stack = []; // Reset the node id counter for every new renderer. nodeIdCount = -1; const tree = makeEmptyTraceEntryTree(); for (let i = 0; i < entries.length; i++) { const event = entries[i]; // If the current event should not be part of the tree, then simply proceed // with the next event. if (options && !options.filter.has(event.name as Types.Events.Name)) { continue; } const duration = event.dur || 0; const nodeId = makeTraceEntryNodeId(); const node = makeEmptyTraceEntryNode(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.roots.add(node); node.selfTime = Types.Timing.Micro(duration); stack.push(node); tree.maxDepth = Math.max(tree.maxDepth, stack.length); entryToNode.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.entry; 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 // possibilities, 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, for example a // profile call was sampled too late after its start, ignore the // problematic event. const endsAfterParent = end > parentEnd; if (endsAfterParent) { continue; } // 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. node.depth = stack.length; node.parent = parentNode; parentNode.children.push(node); node.selfTime = Types.Timing.Micro(duration); if (parentNode.selfTime !== undefined) { parentNode.selfTime = Types.Timing.Micro(parentNode.selfTime - (event.dur || 0)); } stack.push(node); tree.maxDepth = Math.max(tree.maxDepth, stack.length); entryToNode.set(event, node); } return {tree, entryToNode}; } /** * Iterates events in a tree hierarchically, from top to bottom, * calling back on every event's start and end in the order * as it traverses down and then up the tree. * * For example, given this tree, the following callbacks * are expected to be made in the following order * |---------------A---------------| * |------B------||-------D------| * |---C---| * * 1. Start A * 3. Start B * 4. Start C * 5. End C * 6. End B * 7. Start D * 8. End D * 9. End A * */ export function walkTreeFromEntry( entryToNode: Map<Types.Events.Event, TraceEntryNode>, rootEntry: Types.Events.Event, onEntryStart: (entry: Types.Events.Event) => void, onEntryEnd: (entry: Types.Events.Event) => void, ): void { const startNode = entryToNode.get(rootEntry); if (!startNode) { return; } walkTreeByNode(entryToNode, startNode, onEntryStart, onEntryEnd); } /** * Given a Helpers.TreeHelpers.RendererTree, this will iterates events in hierarchically, visiting * each root node and working from top to bottom, calling back on every event's * start and end in the order as it traverses down and then up the tree. * * For example, given this tree, the following callbacks * are expected to be made in the following order * |------------- Task A -------------||-- Task E --| * |-- Task B --||-- Task D --| * |- Task C -| * * 1. Start A * 3. Start B * 4. Start C * 5. End C * 6. End B * 7. Start D * 8. End D * 9. End A * 10. Start E * 11. End E * */ export function walkEntireTree( entryToNode: Map<Types.Events.Event, TraceEntryNode>, tree: TraceEntryTree, onEntryStart: (entry: Types.Events.Event) => void, onEntryEnd: (entry: Types.Events.Event) => void, traceWindowToInclude?: Types.Timing.TraceWindowMicro, minDuration?: Types.Timing.Micro, ): void { for (const rootNode of tree.roots) { walkTreeByNode(entryToNode, rootNode, onEntryStart, onEntryEnd, traceWindowToInclude, minDuration); } } function walkTreeByNode( entryToNode: Map<Types.Events.Event, TraceEntryNode>, rootNode: TraceEntryNode, onEntryStart: (entry: Types.Events.Event) => void, onEntryEnd: (entry: Types.Events.Event) => void, traceWindowToInclude?: Types.Timing.TraceWindowMicro, minDuration?: Types.Timing.Micro, ): void { if (traceWindowToInclude && !treeNodeIsInWindow(rootNode, traceWindowToInclude)) { // If this node is not within the provided window, we can skip it. We also // can skip all its children too, as we know they won't be in the window if // their parent is not. return; } if (typeof minDuration !== 'undefined') { const duration = Types.Timing.Micro( rootNode.entry.ts + Types.Timing.Micro(rootNode.entry.dur ?? 0), ); if (duration < minDuration) { return; } } onEntryStart(rootNode.entry); for (const child of rootNode.children) { walkTreeByNode(entryToNode, child, onEntryStart, onEntryEnd, traceWindowToInclude, minDuration); } onEntryEnd(rootNode.entry); } /** * Returns true if the provided node is partially or fully within the trace * window. The entire node does not have to fit inside the window, but it does * have to partially intersect it. */ function treeNodeIsInWindow(node: TraceEntryNode, traceWindow: Types.Timing.TraceWindowMicro): boolean { return eventIsInBounds(node.entry, traceWindow); } /** * Determines if the given events, which are assumed to be ordered can * be organized into tree structures. * This condition is met if there is *not* a pair of async events * e1 and e2 where: * * e1.startTime < e2.startTime && e1.endTime > e2.startTime && e1.endTime < e2.endTime. * or, graphically: * |------- e1 ------| * |------- e2 --------| * * Because a parent-child relationship cannot be made from the example * above, a tree cannot be made from the set of events. * * Sync events from the same thread are tree-able by definition. * * Note that this will also return true if multiple trees can be * built, for example if none of the events overlap with each other. */ export function canBuildTreesFromEvents(events: readonly Types.Events.Event[]): boolean { const stack: Types.Events.Event[] = []; for (const event of events) { const startTime = event.ts; const endTime = event.ts + (event.dur ?? 0); let parent = stack.at(-1); if (parent === undefined) { stack.push(event); continue; } let parentEndTime = parent.ts + (parent.dur ?? 0); // Discard events that are not parents for this event. The parent // is one whose end time is after this event start time. while (stack.length && startTime >= parentEndTime) { stack.pop(); parent = stack.at(-1); if (parent === undefined) { break; } parentEndTime = parent.ts + (parent.dur ?? 0); } if (stack.length && endTime > parentEndTime) { // If such an event exists but its end time is before this // event's end time, then a tree cannot be made using this // events. return false; } stack.push(event); } return true; }