UNPKG

chrome-devtools-frontend

Version:
345 lines (313 loc) • 14.6 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 * as Platform from '../../core/platform/platform.js'; import * as Trace from '../../models/trace/trace.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import { entryIsVisibleInTimeline, } from './CompatibilityTracksAppender.js'; /** * This class can take in a thread that has been generated by the * RendererHandler and apply certain actions to it in order to modify what is * shown to the user. These actions can be automatically applied by DevTools or * applied by the user. * * Once actions are applied, the invisibleEntries() method will return the * entries that are invisible, and this is the list of entries that should be * removed before rendering the resulting thread on the timeline. */ export class EntriesFilter { #parsedTrace: Trace.Handlers.Types.ParsedTrace; // Track the set of invisible entries. #invisibleEntries: Trace.Types.Events.Event[] = []; // List of entries whose children are hidden. This list is used to // keep track of entries that should be identified in the UI as "expandable", // since they can be clicked to reveal their hidden children. #expandableEntries: Trace.Types.Events.Event[] = []; // Cache for descendants of entry that have already been gathered. The descendants // will never change so we can avoid running the potentially expensive search again. #entryToDescendantsMap = new Map<Trace.Helpers.TreeHelpers.TraceEntryNode, Trace.Types.Events.Event[]>(); constructor(parsedTrace: Trace.Handlers.Types.ParsedTrace) { this.#parsedTrace = parsedTrace; } #getEntryNode(entry: Trace.Types.Events.Event): Trace.Helpers.TreeHelpers.TraceEntryNode|undefined { // The entry might be either from the Samples handler or Renderer handler. So we need to check both handlers to find // the EntryNode. return this.#parsedTrace.Samples.entryToNode.get(entry) ?? this.#parsedTrace.Renderer.entryToNode.get(entry); } /** * Checks which actions can be applied on an entry. This allows us to only show possible actions in the Context Menu. * For example, if an entry has no children, COLLAPSE_FUNCTION will not change the FlameChart, therefore there is no need to show this action as an option. */ findPossibleActions(entry: Trace.Types.Events.Event): PerfUI.FlameChart.PossibleFilterActions { const entryNode = this.#getEntryNode(entry); if (!entryNode) { // Invalid node was given, return no possible actions. return { [PerfUI.FlameChart.FilterAction.MERGE_FUNCTION]: false, [PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION]: false, [PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS]: false, [PerfUI.FlameChart.FilterAction.RESET_CHILDREN]: false, [PerfUI.FlameChart.FilterAction.UNDO_ALL_ACTIONS]: false, }; } const entryParent = entryNode.parent; const allVisibleDescendants = this.#findAllDescendantsOfNode(entryNode).filter(descendant => !this.#invisibleEntries.includes(descendant)); const allVisibleRepeatingDescendants = this.#findAllRepeatingDescendantsOfNext(entryNode).filter( descendant => !this.#invisibleEntries.includes(descendant)); const allInVisibleDescendants = this.#findAllDescendantsOfNode(entryNode).filter(descendant => this.#invisibleEntries.includes(descendant)); // If there are children to hide, indicate action as possible const possibleActions: PerfUI.FlameChart.PossibleFilterActions = { [PerfUI.FlameChart.FilterAction.MERGE_FUNCTION]: entryParent !== null, [PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION]: allVisibleDescendants.length > 0, [PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS]: allVisibleRepeatingDescendants.length > 0, [PerfUI.FlameChart.FilterAction.RESET_CHILDREN]: allInVisibleDescendants.length > 0, [PerfUI.FlameChart.FilterAction.UNDO_ALL_ACTIONS]: this.#invisibleEntries.length > 0, }; return possibleActions; } /** * Returns the amount of entry descendants that belong to the hidden entries array. * */ findHiddenDescendantsAmount(entry: Trace.Types.Events.Event): number { const entryNode = this.#getEntryNode(entry); if (!entryNode) { return 0; } const allDescendants = this.#findAllDescendantsOfNode(entryNode); return allDescendants.filter(descendant => this.invisibleEntries().includes(descendant)).length; } /** * Returns the set of entries that are invisible given the set of applied actions. */ invisibleEntries(): Trace.Types.Events.Event[] { return this.#invisibleEntries; } /** * Sets hidden and expandable. Called when a trace with modifications is loaded and some entries are set as hidden and expandable. * Both arrays are set together because if there is one, the other must be present too. */ setHiddenAndExpandableEntries( invisibleEntries: Trace.Types.Events.Event[], expandableEntries: Trace.Types.Events.Event[]): void { this.#invisibleEntries.push(...invisibleEntries); this.#expandableEntries.push(...expandableEntries); } entryIsInvisible(entry: Trace.Types.Events.Event): boolean { return this.#invisibleEntries.includes(entry); } /** * Returns the array of entries that have a sign indicating that entries below are hidden, * and so that they can be "expanded" to reveal their hidden children. */ expandableEntries(): Trace.Types.Events.Event[] { return this.#expandableEntries; } /** * Applies an action to hide entries or removes entries * from hidden entries array depending on the action. */ applyFilterAction(action: PerfUI.FlameChart.UserFilterAction): Trace.Types.Events.Event[] { // We apply new user action to the set of all entries, and mark // any that should be hidden by adding them to this set. // Another approach would be to use splice() to remove items from the // array, but doing this would be a mutation of the arry for every hidden // event. Instead, we add entries to this set and return it as an array at the end. const entriesToHide = new Set<Trace.Types.Events.Event>(); switch (action.type) { case PerfUI.FlameChart.FilterAction.MERGE_FUNCTION: { // The entry that was clicked on is merged into its parent. All its // children remain visible, so we just have to hide the entry that was // selected. entriesToHide.add(action.entry); // If parent node exists, add it to expandableEntries, so it would be possible to uncollapse its children. const actionNode = this.#getEntryNode(action.entry) || null; const parentNode = actionNode && this.#firstVisibleParentNodeForEntryNode(actionNode); if (parentNode) { this.#addExpandableEntry(parentNode.entry); } break; } case PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION: { // The entry itself remains visible, but all of its descendants are hidden. const entryNode = this.#getEntryNode(action.entry); if (!entryNode) { // Invalid node was given, just ignore and move on. break; } const allDescendants = this.#findAllDescendantsOfNode(entryNode); allDescendants.forEach(descendant => entriesToHide.add(descendant)); this.#addExpandableEntry(action.entry); break; } case PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS: { const entryNode = this.#getEntryNode(action.entry); if (!entryNode) { // Invalid node was given, just ignore and move on. break; } const allRepeatingDescendants = this.#findAllRepeatingDescendantsOfNext(entryNode); allRepeatingDescendants.forEach(descendant => entriesToHide.add(descendant)); if (entriesToHide.size > 0) { this.#addExpandableEntry(action.entry); } break; } case PerfUI.FlameChart.FilterAction.UNDO_ALL_ACTIONS: { this.#invisibleEntries = []; this.#expandableEntries = []; break; } case PerfUI.FlameChart.FilterAction.RESET_CHILDREN: { this.#makeEntryChildrenVisible(action.entry); break; } default: Platform.assertNever(action.type, `Unknown EntriesFilter action: ${action.type}`); } this.#invisibleEntries.push(...entriesToHide); return this.#invisibleEntries; } /** * Add an entry to the array of entries that have a sign indicating that entries below are hidden. * Also, remove all of the child entries of the new expandable entry from the expandable array. Do that because * to draw the initiator from the closest visible entry, we need to get the closest entry that is * marked as expandable and we do not want to get some that are hidden. */ #addExpandableEntry(entry: Trace.Types.Events.Event): void { this.#expandableEntries.push(entry); const entryNode = this.#getEntryNode(entry); if (!entryNode) { // Invalid node was given, just ignore and move on. return; } const allDescendants = this.#findAllDescendantsOfNode(entryNode); if (allDescendants.length > 0) { this.#expandableEntries = this.#expandableEntries.filter(entry => { return !allDescendants.includes(entry); }); } } firstVisibleParentEntryForEntry(entry: Trace.Types.Events.Event): Trace.Types.Events.Event|null { const node = this.#getEntryNode(entry); if (!node) { return null; } const parent = this.#firstVisibleParentNodeForEntryNode(node); return parent ? parent.entry : null; } // The direct parent might be hidden by other actions, therefore we look for the next visible parent. #firstVisibleParentNodeForEntryNode(node: Trace.Helpers.TreeHelpers.TraceEntryNode): Trace.Helpers.TreeHelpers.TraceEntryNode|null { let parent = node.parent; while ((parent && this.#invisibleEntries.includes(parent.entry)) || (parent && !entryIsVisibleInTimeline(parent.entry))) { parent = parent.parent; } return parent; } #findAllDescendantsOfNode(root: Trace.Helpers.TreeHelpers.TraceEntryNode): Trace.Types.Events.Event[] { const cachedDescendants = this.#entryToDescendantsMap.get(root); if (cachedDescendants) { return cachedDescendants; } const descendants: Trace.Types.Events.Event[] = []; // Walk through all the descendants, starting at the root node. const children: Trace.Helpers.TreeHelpers.TraceEntryNode[] = [...root.children]; while (children.length > 0) { const childNode = children.shift(); if (childNode) { descendants.push(childNode.entry); const childNodeCachedDescendants = this.#entryToDescendantsMap.get(childNode); // If the descendants of a child are cached, get them from the cache instead of iterating through them again if (childNodeCachedDescendants) { descendants.push(...childNodeCachedDescendants); } else { children.push(...childNode.children); } } } this.#entryToDescendantsMap.set(root, descendants); return descendants; } #findAllRepeatingDescendantsOfNext(root: Trace.Helpers.TreeHelpers.TraceEntryNode): Trace.Types.Events.Event[] { // Walk through all the ancestors, starting at the root node. const children: Trace.Helpers.TreeHelpers.TraceEntryNode[] = [...root.children]; const repeatingNodes: Trace.Types.Events.Event[] = []; const rootIsProfileCall = Trace.Types.Events.isProfileCall(root.entry); while (children.length > 0) { const childNode = children.shift(); if (childNode) { const childIsProfileCall = Trace.Types.Events.isProfileCall(childNode.entry); if (/* Handle SyntheticProfileCalls */ rootIsProfileCall && childIsProfileCall) { const rootNodeEntry = root.entry as Trace.Types.Events.SyntheticProfileCall; const childNodeEntry = childNode.entry as Trace.Types.Events.SyntheticProfileCall; if (Trace.Helpers.SamplesIntegrator.SamplesIntegrator.framesAreEqual( rootNodeEntry.callFrame, childNodeEntry.callFrame)) { repeatingNodes.push(childNode.entry); } } /* Handle Renderer events */ else if (!rootIsProfileCall && !childIsProfileCall) { if (root.entry.name === childNode.entry.name) { repeatingNodes.push(childNode.entry); } } children.push(...childNode.children); } } return repeatingNodes; } /** * If an entry was selected from a link instead of clicking on it, * it might be in the invisible entries array. * If it is, reveal it by resetting clidren the closest expandable entry, */ revealEntry(entry: Trace.Types.Events.Event): void { const entryNode = this.#getEntryNode(entry); if (!entryNode) { // Invalid node was given, just ignore and move on. return; } let closestExpandableParent = entryNode; while (closestExpandableParent.parent && !this.#expandableEntries.includes(closestExpandableParent.entry)) { closestExpandableParent = closestExpandableParent.parent; } this.#makeEntryChildrenVisible(closestExpandableParent.entry); } /** * Removes all of the entry children from the * invisible entries array to make them visible. */ #makeEntryChildrenVisible(entry: Trace.Types.Events.Event): void { const entryNode = this.#getEntryNode(entry); if (!entryNode) { // Invalid node was given, just ignore and move on. return; } const descendants = this.#findAllDescendantsOfNode(entryNode); /** * Filter out all descendant of the node * from the invisible entries list. */ this.#invisibleEntries = this.#invisibleEntries.filter(entry => { if (descendants.includes(entry)) { return false; } return true; }); /** * Filter out all descentants and entry from expandable entries * list to not show that some entries below those are hidden. */ this.#expandableEntries = this.#expandableEntries.filter(iterEntry => { if (descendants.includes(iterEntry) || iterEntry === entry) { return false; } return true; }); } isEntryExpandable(event: Trace.Types.Events.Event): boolean { return this.#expandableEntries.includes(event); } }