chrome-devtools-frontend
Version:
Chrome DevTools UI
345 lines (313 loc) • 14.6 kB
text/typescript
// 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);
}
}