Files
react-native-devtools-frontend/front_end/panels/timeline/TimelineTreeView.ts
T
Kateryna Prokopenko 6fa122c18f Type all path and URL variables in panels/timeline to Branded Types
Bug: 1299788
Change-Id: I7ec964e36b7a50e009d3062fb959dc457fceb339
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3627335
Reviewed-by: Eric Leese <leese@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Commit-Queue: Kateryna Prokopenko <kprokopenko@chromium.org>
2022-05-06 11:14:18 +00:00

1079 lines
40 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2015 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 Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as TimelineModel from '../../models/timeline_model/timeline_model.js';
import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import type {PerformanceModel} from './PerformanceModel.js';
import {TimelineRegExp} from './TimelineFilters.js';
import type {TimelineSelection} from './TimelinePanel.js';
import {TimelineUIUtils} from './TimelineUIUtils.js';
const UIStrings = {
/**
*@description Text for the performance of something
*/
performance: 'Performance',
/**
*@description Text to filter result items
*/
filter: 'Filter',
/**
*@description Time of a single activity, as opposed to the total time
*/
selfTime: 'Self Time',
/**
*@description Text for the total time of something
*/
totalTime: 'Total Time',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
activity: 'Activity',
/**
*@description Text of a DOM element in Timeline Tree View of the Performance panel
*/
selectItemForDetails: 'Select item for details.',
/**
* @description This message is presented as a tooltip when developers investigate the performance
* of a page. The tooltip alerts developers that some parts of code in execution were not optimized
* (made to run faster) and that associated timing information must be considered with this in
* mind. The placeholder text is the reason the code was not optimized.
* @example {Optimized too many times} PH1
*/
notOptimizedS: 'Not optimized: {PH1}',
/**
*@description Time in miliseconds
*@example {30.1} PH1
*/
fms: '{PH1} ms',
/**
*@description Number followed by percent sign
*@example {20} PH1
*/
percentPlaceholder: '{PH1} %',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
chromeExtensionsOverhead: '[`Chrome` extensions overhead]',
/**
* @description Text in Timeline Tree View of the Performance panel. The text is presented
* when developers investigate the performance of a page. 'V8 Runtime' labels the time
* spent in (i.e. runtime) the V8 JavaScript engine.
*/
vRuntime: '[`V8` Runtime]',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
unattributed: '[unattributed]',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
javascript: 'JavaScript',
/**
*@description Text that refers to one or a group of webpages
*/
page: 'Page',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
noGrouping: 'No Grouping',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
groupByActivity: 'Group by Activity',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
groupByCategory: 'Group by Category',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
groupByDomain: 'Group by Domain',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
groupByFrame: 'Group by Frame',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
groupBySubdomain: 'Group by Subdomain',
/**
*@description Text in Timeline Tree View of the Performance panel
*/
groupByUrl: 'Group by URL',
/**
*@description Aria-label for grouping combo box in Timeline Details View
*/
groupBy: 'Group by',
/**
*@description Aria-label for filter bar in Call Tree view
*/
filterCallTree: 'Filter call tree',
/**
*@description Aria-label for the filter bar in Bottom-Up view
*/
filterBottomup: 'Filter bottom-up',
/**
* @description Title of the sidebar pane in the Performance panel which shows the stack (call
* stack) where the program spent the most time (out of all the call stacks) while executing.
*/
heaviestStack: 'Heaviest stack',
/**
* @description Tooltip for the the Heaviest stack sidebar toggle in the Timeline Tree View of the
* Performance panel. Command to open/show the sidebar.
*/
showHeaviestStack: 'Show Heaviest stack',
/**
* @description Tooltip for the the Heaviest stack sidebar toggle in the Timeline Tree View of the
* Performance panel. Command to close/hide the sidebar.
*/
hideHeaviestStack: 'Hide Heaviest stack',
/**
* @description Screen reader announcement when the heaviest stack sidebar is shown in the Performance panel.
*/
heaviestStackShown: 'Heaviest stack sidebar shown',
/**
* @description Screen reader announcement when the heaviest stack sidebar is hidden in the Performance panel.
*/
heaviestStackHidden: 'Heaviest stack sidebar hidden',
/**
*@description Data grid name for Timeline Stack data grids
*/
timelineStack: 'Timeline Stack',
};
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineTreeView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class TimelineTreeView extends UI.Widget.VBox implements UI.SearchableView.Searchable {
modelInternal: PerformanceModel|null;
private track: TimelineModel.TimelineModel.Track|null;
private readonly tree: TimelineModel.TimelineProfileTree.Node|null;
private searchResults: TimelineModel.TimelineProfileTree.Node[];
linkifier!: Components.Linkifier.Linkifier;
dataGrid!: DataGrid.SortableDataGrid.SortableDataGrid<GridNode>;
private lastHoveredProfileNode!: TimelineModel.TimelineProfileTree.Node|null;
private textFilterInternal!: TimelineRegExp;
private taskFilter!: TimelineModel.TimelineModelFilter.ExclusiveNameFilter;
protected startTime!: number;
protected endTime!: number;
splitWidget!: UI.SplitWidget.SplitWidget;
detailsView!: UI.Widget.Widget;
private searchableView!: UI.SearchableView.SearchableView;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private currentThreadSetting?: Common.Settings.Setting<any>;
private lastSelectedNodeInternal?: TimelineModel.TimelineProfileTree.Node|null;
private textFilterUI?: UI.Toolbar.ToolbarInput;
private root?: TimelineModel.TimelineProfileTree.Node;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private currentResult?: any;
constructor() {
super();
this.modelInternal = null;
this.track = null;
this.tree = null;
this.element.classList.add('timeline-tree-view');
this.searchResults = [];
}
static eventNameForSorting(event: SDK.TracingModel.Event): string {
if (event.name === TimelineModel.TimelineModel.RecordType.JSFrame) {
const data = event.args['data'];
return data['functionName'] + '@' + (data['scriptId'] || data['url'] || '');
}
return event.name + ':@' + TimelineModel.TimelineProfileTree.eventURL(event);
}
setSearchableView(searchableView: UI.SearchableView.SearchableView): void {
this.searchableView = searchableView;
}
setModel(model: PerformanceModel|null, track: TimelineModel.TimelineModel.Track|null): void {
this.modelInternal = model;
this.track = track;
this.refreshTree();
}
getToolbarInputAccessiblePlaceHolder(): string {
return '';
}
model(): PerformanceModel|null {
return this.modelInternal;
}
init(): void {
this.linkifier = new Components.Linkifier.Linkifier();
this.taskFilter =
new TimelineModel.TimelineModelFilter.ExclusiveNameFilter([TimelineModel.TimelineModel.RecordType.Task]);
this.textFilterInternal = new TimelineRegExp();
this.currentThreadSetting = Common.Settings.Settings.instance().createSetting('timelineTreeCurrentThread', 0);
this.currentThreadSetting.addChangeListener(this.refreshTree, this);
const columns = ([] as DataGrid.DataGrid.ColumnDescriptor[]);
this.populateColumns(columns);
this.splitWidget = new UI.SplitWidget.SplitWidget(true, true, 'timelineTreeViewDetailsSplitWidget');
const mainView = new UI.Widget.VBox();
const toolbar = new UI.Toolbar.Toolbar('', mainView.element);
toolbar.makeWrappable(true);
this.populateToolbar(toolbar);
this.dataGrid = new DataGrid.SortableDataGrid.SortableDataGrid({
displayName: i18nString(UIStrings.performance),
columns,
refreshCallback: undefined,
editCallback: undefined,
deleteCallback: undefined,
});
this.dataGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this.sortingChanged, this);
this.dataGrid.element.addEventListener('mousemove', this.onMouseMove.bind(this), true);
this.dataGrid.setResizeMethod(DataGrid.DataGrid.ResizeMethod.Last);
this.dataGrid.setRowContextMenuCallback(this.onContextMenu.bind(this));
this.dataGrid.asWidget().show(mainView.element);
this.dataGrid.addEventListener(DataGrid.DataGrid.Events.SelectedNode, this.updateDetailsForSelection, this);
this.detailsView = new UI.Widget.VBox();
this.detailsView.element.classList.add('timeline-details-view', 'timeline-details-view-body');
this.splitWidget.setMainWidget(mainView);
this.splitWidget.setSidebarWidget(this.detailsView);
this.splitWidget.hideSidebar();
this.splitWidget.show(this.element);
this.splitWidget.addEventListener(UI.SplitWidget.Events.ShowModeChanged, this.onShowModeChanged, this);
this.lastSelectedNodeInternal;
}
lastSelectedNode(): TimelineModel.TimelineProfileTree.Node|null|undefined {
return this.lastSelectedNodeInternal;
}
updateContents(selection: TimelineSelection): void {
this.setRange(selection.startTime(), selection.endTime());
}
setRange(startTime: number, endTime: number): void {
this.startTime = startTime;
this.endTime = endTime;
this.refreshTree();
}
filters(): TimelineModel.TimelineModelFilter.TimelineModelFilter[] {
return [this.taskFilter, this.textFilterInternal, ...(this.modelInternal ? this.modelInternal.filters() : [])];
}
filtersWithoutTextFilter(): TimelineModel.TimelineModelFilter.TimelineModelFilter[] {
return [this.taskFilter, ...(this.modelInternal ? this.modelInternal.filters() : [])];
}
textFilter(): TimelineRegExp {
return this.textFilterInternal;
}
exposePercentages(): boolean {
return false;
}
populateToolbar(toolbar: UI.Toolbar.Toolbar): void {
const textFilterUI =
new UI.Toolbar.ToolbarInput(i18nString(UIStrings.filter), this.getToolbarInputAccessiblePlaceHolder());
textFilterUI.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, () => {
const searchQuery = textFilterUI.value();
this.textFilterInternal.setRegExp(
searchQuery ? Platform.StringUtilities.createPlainTextSearchRegex(searchQuery, 'i') : null);
this.refreshTree();
}, this);
this.textFilterUI = textFilterUI;
toolbar.appendToolbarItem(textFilterUI);
}
modelEvents(): SDK.TracingModel.Event[] {
return this.track ? this.track.syncEvents() : [];
}
onHover(_node: TimelineModel.TimelineProfileTree.Node|null): void {
}
appendContextMenuItems(_contextMenu: UI.ContextMenu.ContextMenu, _node: TimelineModel.TimelineProfileTree.Node):
void {
}
linkifyLocation(event: SDK.TracingModel.Event): Element|null {
if (!this.modelInternal) {
return null;
}
const target = this.modelInternal.timelineModel().targetByEvent(event);
if (!target) {
return null;
}
const frame = TimelineModel.TimelineProfileTree.eventStackFrame(event);
if (!frame) {
return null;
}
return this.linkifier.maybeLinkifyConsoleCallFrame(target, frame, {showColumnNumber: true, inlineFrameIndex: 0});
}
selectProfileNode(treeNode: TimelineModel.TimelineProfileTree.Node, suppressSelectedEvent: boolean): void {
const pathToRoot = [];
let node: (TimelineModel.TimelineProfileTree.Node|null)|TimelineModel.TimelineProfileTree.Node = treeNode;
for (; node; node = node.parent) {
pathToRoot.push(node);
}
for (let i = pathToRoot.length - 1; i > 0; --i) {
const gridNode = this.dataGridNodeForTreeNode(pathToRoot[i]);
if (gridNode && gridNode.dataGrid) {
gridNode.expand();
}
}
const gridNode = this.dataGridNodeForTreeNode(treeNode);
if (gridNode && gridNode.dataGrid) {
gridNode.reveal();
gridNode.select(suppressSelectedEvent);
}
}
refreshTree(): void {
this.linkifier.reset();
this.dataGrid.rootNode().removeChildren();
if (!this.modelInternal) {
this.updateDetailsForSelection();
return;
}
this.root = this.buildTree();
const children = this.root.children();
let maxSelfTime = 0;
let maxTotalTime = 0;
const totalUsedTime = this.root.totalTime - this.root.selfTime;
for (const child of children.values()) {
maxSelfTime = Math.max(maxSelfTime, child.selfTime);
maxTotalTime = Math.max(maxTotalTime, child.totalTime);
}
for (const child of children.values()) {
// Exclude the idle time off the total calculation.
const gridNode = new TreeGridNode(child, totalUsedTime, maxSelfTime, maxTotalTime, this);
this.dataGrid.insertChild(gridNode);
}
this.sortingChanged();
this.updateDetailsForSelection();
if (this.searchableView) {
this.searchableView.refreshSearch();
}
const rootNode = this.dataGrid.rootNode();
if (rootNode.children.length > 0) {
rootNode.children[0].select(/* supressSelectedEvent */ true);
}
}
buildTree(): TimelineModel.TimelineProfileTree.Node {
throw new Error('Not Implemented');
}
buildTopDownTree(doNotAggregate: boolean, groupIdCallback: ((arg0: SDK.TracingModel.Event) => string)|null):
TimelineModel.TimelineProfileTree.Node {
return new TimelineModel.TimelineProfileTree.TopDownRootNode(
this.modelEvents(), this.filters(), this.startTime, this.endTime, doNotAggregate, groupIdCallback);
}
populateColumns(columns: DataGrid.DataGrid.ColumnDescriptor[]): void {
columns.push(
({id: 'self', title: i18nString(UIStrings.selfTime), width: '120px', fixedWidth: true, sortable: true} as
DataGrid.DataGrid.ColumnDescriptor));
columns.push(
({id: 'total', title: i18nString(UIStrings.totalTime), width: '120px', fixedWidth: true, sortable: true} as
DataGrid.DataGrid.ColumnDescriptor));
columns.push(
({id: 'activity', title: i18nString(UIStrings.activity), disclosure: true, sortable: true} as
DataGrid.DataGrid.ColumnDescriptor));
}
private sortingChanged(): void {
const columnId = this.dataGrid.sortColumnId();
if (!columnId) {
return;
}
let sortFunction;
switch (columnId) {
case 'startTime':
sortFunction = compareStartTime;
break;
case 'self':
sortFunction = compareNumericField.bind(null, 'selfTime');
break;
case 'total':
sortFunction = compareNumericField.bind(null, 'totalTime');
break;
case 'activity':
sortFunction = compareName;
break;
default:
console.assert(false, 'Unknown sort field: ' + columnId);
return;
}
this.dataGrid.sortNodes(sortFunction, !this.dataGrid.isSortOrderAscending());
function compareNumericField(
field: string, a: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>,
b: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>): number {
const nodeA = (a as TreeGridNode);
const nodeB = (b as TreeGridNode);
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (nodeA.profileNode as any)[field] - (nodeB.profileNode as any)[field];
}
function compareStartTime(
a: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>,
b: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>): number {
const nodeA = (a as TreeGridNode);
const nodeB = (b as TreeGridNode);
const eventA = (nodeA.profileNode.event as SDK.TracingModel.Event);
const eventB = (nodeB.profileNode.event as SDK.TracingModel.Event);
return eventA.startTime - eventB.startTime;
}
function compareName(
a: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>,
b: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>): number {
const nodeA = (a as TreeGridNode);
const nodeB = (b as TreeGridNode);
const eventA = (nodeA.profileNode.event as SDK.TracingModel.Event);
const eventB = (nodeB.profileNode.event as SDK.TracingModel.Event);
const nameA = TimelineTreeView.eventNameForSorting(eventA);
const nameB = TimelineTreeView.eventNameForSorting(eventB);
return nameA.localeCompare(nameB);
}
}
private onShowModeChanged(): void {
if (this.splitWidget.showMode() === UI.SplitWidget.ShowMode.OnlyMain) {
return;
}
this.lastSelectedNodeInternal = undefined;
this.updateDetailsForSelection();
}
private updateDetailsForSelection(): void {
const selectedNode = this.dataGrid.selectedNode ? (this.dataGrid.selectedNode as TreeGridNode).profileNode : null;
if (selectedNode === this.lastSelectedNodeInternal) {
return;
}
this.lastSelectedNodeInternal = selectedNode;
if (this.splitWidget.showMode() === UI.SplitWidget.ShowMode.OnlyMain) {
return;
}
this.detailsView.detachChildWidgets();
this.detailsView.element.removeChildren();
if (selectedNode && this.showDetailsForNode(selectedNode)) {
return;
}
const banner = this.detailsView.element.createChild('div', 'full-widget-dimmed-banner');
UI.UIUtils.createTextChild(banner, i18nString(UIStrings.selectItemForDetails));
}
showDetailsForNode(_node: TimelineModel.TimelineProfileTree.Node): boolean {
return false;
}
private onMouseMove(event: Event): void {
const gridNode = event.target && (event.target instanceof Node) ?
(this.dataGrid.dataGridNodeFromNode((event.target as Node))) :
null;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// @ts-expect-error
const profileNode = gridNode && gridNode._profileNode;
if (profileNode === this.lastHoveredProfileNode) {
return;
}
this.lastHoveredProfileNode = profileNode;
this.onHover(profileNode);
}
private onContextMenu(
contextMenu: UI.ContextMenu.ContextMenu, eventGridNode: DataGrid.DataGrid.DataGridNode<GridNode>): void {
const gridNode = (eventGridNode as GridNode);
if (gridNode.linkElement && !contextMenu.containsTarget(gridNode.linkElement)) {
contextMenu.appendApplicableItems(gridNode.linkElement);
}
const profileNode = gridNode.profileNode;
if (profileNode) {
this.appendContextMenuItems(contextMenu, profileNode);
}
}
dataGridNodeForTreeNode(treeNode: TimelineModel.TimelineProfileTree.Node): GridNode|null {
return profileNodeToTreeGridNode.get(treeNode) || null;
}
// UI.SearchableView.Searchable implementation
searchCanceled(): void {
this.searchResults = [];
this.currentResult = 0;
}
performSearch(searchConfig: UI.SearchableView.SearchConfig, _shouldJump: boolean, _jumpBackwards?: boolean): void {
this.searchResults = [];
this.currentResult = 0;
if (!this.root) {
return;
}
const searchRegex = searchConfig.toSearchRegex();
this.searchResults = this.root.searchTree(event => TimelineUIUtils.testContentMatching(event, searchRegex.regex));
this.searchableView.updateSearchMatchesCount(this.searchResults.length);
}
jumpToNextSearchResult(): void {
if (!this.searchResults.length) {
return;
}
this.selectProfileNode(this.searchResults[this.currentResult], false);
this.currentResult = Platform.NumberUtilities.mod(this.currentResult + 1, this.searchResults.length);
}
jumpToPreviousSearchResult(): void {
if (!this.searchResults.length) {
return;
}
this.selectProfileNode(this.searchResults[this.currentResult], false);
this.currentResult = Platform.NumberUtilities.mod(this.currentResult - 1, this.searchResults.length);
}
supportsCaseSensitiveSearch(): boolean {
return true;
}
supportsRegexSearch(): boolean {
return true;
}
}
export class GridNode extends DataGrid.SortableDataGrid.SortableDataGridNode<GridNode> {
protected populated: boolean;
profileNode: TimelineModel.TimelineProfileTree.Node;
protected treeView: TimelineTreeView;
protected grandTotalTime: number;
protected maxSelfTime: number;
protected maxTotalTime: number;
linkElement: Element|null;
constructor(
profileNode: TimelineModel.TimelineProfileTree.Node, grandTotalTime: number, maxSelfTime: number,
maxTotalTime: number, treeView: TimelineTreeView) {
super(null, false);
this.populated = false;
this.profileNode = profileNode;
this.treeView = treeView;
this.grandTotalTime = grandTotalTime;
this.maxSelfTime = maxSelfTime;
this.maxTotalTime = maxTotalTime;
this.linkElement = null;
}
createCell(columnId: string): HTMLElement {
if (columnId === 'activity') {
return this.createNameCell(columnId);
}
return this.createValueCell(columnId) || super.createCell(columnId);
}
private createNameCell(columnId: string): HTMLElement {
const cell = this.createTD(columnId);
const container = cell.createChild('div', 'name-container');
const iconContainer = container.createChild('div', 'activity-icon-container');
const icon = iconContainer.createChild('div', 'activity-icon');
const name = container.createChild('div', 'activity-name');
const event = this.profileNode.event;
if (this.profileNode.isGroupNode()) {
const treeView = (this.treeView as AggregatedTimelineTreeView);
const info = treeView.displayInfoForGroupNode(this.profileNode);
name.textContent = info.name;
icon.style.backgroundColor = info.color;
if (info.icon) {
iconContainer.insertBefore(info.icon, icon);
}
} else if (event) {
const data = event.args['data'];
const deoptReason = data && data['deoptReason'];
if (deoptReason) {
container.createChild('div', 'activity-warning').title =
i18nString(UIStrings.notOptimizedS, {PH1: deoptReason});
}
name.textContent = TimelineUIUtils.eventTitle(event);
this.linkElement = this.treeView.linkifyLocation(event);
if (this.linkElement) {
container.createChild('div', 'activity-link').appendChild(this.linkElement);
}
const eventStyle = TimelineUIUtils.eventStyle(event);
const eventCategory = eventStyle.category;
UI.ARIAUtils.setAccessibleName(icon, eventCategory.title);
icon.style.backgroundColor = eventCategory.color;
}
return cell;
}
private createValueCell(columnId: string): HTMLElement|null {
if (columnId !== 'self' && columnId !== 'total' && columnId !== 'startTime') {
return null;
}
let showPercents = false;
let value: number;
let maxTime: number|undefined;
let event: SDK.TracingModel.Event|null;
switch (columnId) {
case 'startTime': {
event = this.profileNode.event;
const model = this.treeView.model();
if (!model) {
throw new Error('Unable to find model for tree view');
}
value = (event ? event.startTime : 0) - model.timelineModel().minimumRecordTime();
} break;
case 'self':
value = this.profileNode.selfTime;
maxTime = this.maxSelfTime;
showPercents = true;
break;
case 'total':
value = this.profileNode.totalTime;
maxTime = this.maxTotalTime;
showPercents = true;
break;
default:
return null;
}
const cell = this.createTD(columnId);
cell.className = 'numeric-column';
cell.setAttribute('title', i18nString(UIStrings.fms, {PH1: value.toFixed(4)}));
const textDiv = cell.createChild('div');
textDiv.createChild('span').textContent = i18nString(UIStrings.fms, {PH1: value.toFixed(1)});
if (showPercents && this.treeView.exposePercentages()) {
textDiv.createChild('span', 'percent-column').textContent =
i18nString(UIStrings.percentPlaceholder, {PH1: (value / this.grandTotalTime * 100).toFixed(1)});
}
if (maxTime) {
textDiv.classList.add('background-percent-bar');
cell.createChild('div', 'background-bar-container').createChild('div', 'background-bar').style.width =
(value * 100 / maxTime).toFixed(1) + '%';
}
return cell;
}
}
export class TreeGridNode extends GridNode {
constructor(
profileNode: TimelineModel.TimelineProfileTree.Node, grandTotalTime: number, maxSelfTime: number,
maxTotalTime: number, treeView: TimelineTreeView) {
super(profileNode, grandTotalTime, maxSelfTime, maxTotalTime, treeView);
this.setHasChildren(this.profileNode.hasChildren());
profileNodeToTreeGridNode.set(profileNode, this);
}
populate(): void {
if (this.populated) {
return;
}
this.populated = true;
if (!this.profileNode.children) {
return;
}
for (const node of this.profileNode.children().values()) {
const gridNode = new TreeGridNode(node, this.grandTotalTime, this.maxSelfTime, this.maxTotalTime, this.treeView);
this.insertChildOrdered(gridNode);
}
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/naming-convention
private static readonly gridNodeSymbol = Symbol('treeGridNode');
}
const profileNodeToTreeGridNode = new WeakMap<TimelineModel.TimelineProfileTree.Node, TreeGridNode>();
export class AggregatedTimelineTreeView extends TimelineTreeView {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected readonly groupBySetting: Common.Settings.Setting<any>;
private readonly stackView: TimelineStackView;
private readonly productByURLCache: Map<string, string>;
private readonly colorByURLCache: Map<string, string>;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private executionContextNamesByOrigin: Map<any, any>;
constructor() {
super();
this.groupBySetting = Common.Settings.Settings.instance().createSetting(
'timelineTreeGroupBy', AggregatedTimelineTreeView.GroupBy.None);
this.groupBySetting.addChangeListener(this.refreshTree.bind(this));
this.init();
this.stackView = new TimelineStackView(this);
this.stackView.addEventListener(TimelineStackView.Events.SelectionChanged, this.onStackViewSelectionChanged, this);
this.productByURLCache = new Map();
this.colorByURLCache = new Map();
this.executionContextNamesByOrigin = new Map();
}
setModel(model: PerformanceModel|null, track: TimelineModel.TimelineModel.Track|null): void {
super.setModel(model, track);
}
updateContents(selection: TimelineSelection): void {
this.updateExtensionResolver();
super.updateContents(selection);
const rootNode = this.dataGrid.rootNode();
if (rootNode.children.length) {
rootNode.children[0].select(/* suppressSelectedEvent */ true);
}
}
private updateExtensionResolver(): void {
this.executionContextNamesByOrigin = new Map();
for (const runtimeModel of SDK.TargetManager.TargetManager.instance().models(SDK.RuntimeModel.RuntimeModel)) {
for (const context of runtimeModel.executionContexts()) {
this.executionContextNamesByOrigin.set(context.origin, context.name);
}
}
}
private beautifyDomainName(this: AggregatedTimelineTreeView, name: string): string {
if (AggregatedTimelineTreeView.isExtensionInternalURL(name as Platform.DevToolsPath.UrlString)) {
name = i18nString(UIStrings.chromeExtensionsOverhead);
} else if (AggregatedTimelineTreeView.isV8NativeURL(name as Platform.DevToolsPath.UrlString)) {
name = i18nString(UIStrings.vRuntime);
} else if (name.startsWith('chrome-extension')) {
name = this.executionContextNamesByOrigin.get(name) || name;
}
return name;
}
displayInfoForGroupNode(node: TimelineModel.TimelineProfileTree.Node): {
name: string,
color: string,
icon: (Element|undefined),
} {
const categories = TimelineUIUtils.categories();
const color =
node.id ? TimelineUIUtils.eventColor((node.event as SDK.TracingModel.Event)) : categories['other'].color;
const unattributed = i18nString(UIStrings.unattributed);
const id = typeof node.id === 'symbol' ? undefined : node.id;
switch (this.groupBySetting.get()) {
case AggregatedTimelineTreeView.GroupBy.Category: {
const category = id ? categories[id] || categories['other'] : {title: unattributed, color: unattributed};
return {name: category.title, color: category.color, icon: undefined};
}
case AggregatedTimelineTreeView.GroupBy.Domain:
case AggregatedTimelineTreeView.GroupBy.Subdomain: {
const domainName = id ? this.beautifyDomainName(id) : undefined;
return {name: domainName || unattributed, color: color, icon: undefined};
}
case AggregatedTimelineTreeView.GroupBy.EventName: {
if (!node.event) {
throw new Error('Unable to find event for group by operation');
}
const name = node.event.name === TimelineModel.TimelineModel.RecordType.JSFrame ?
i18nString(UIStrings.javascript) :
TimelineUIUtils.eventTitle(node.event);
return {
name: name,
color: node.event.name === TimelineModel.TimelineModel.RecordType.JSFrame ?
TimelineUIUtils.eventStyle(node.event).category.color :
color,
icon: undefined,
};
}
case AggregatedTimelineTreeView.GroupBy.URL:
break;
case AggregatedTimelineTreeView.GroupBy.Frame: {
if (!this.modelInternal) {
throw new Error('Unable to find model for group by frame operation');
}
const frame = id ? this.modelInternal.timelineModel().pageFrameById(id as Protocol.Page.FrameId) : undefined;
const frameName = frame ? TimelineUIUtils.displayNameForFrame(frame, 80) : i18nString(UIStrings.page);
return {name: frameName, color: color, icon: undefined};
}
default:
console.assert(false, 'Unexpected grouping type');
}
return {name: id || unattributed, color: color, icon: undefined};
}
populateToolbar(toolbar: UI.Toolbar.Toolbar): void {
super.populateToolbar(toolbar);
const groupBy = AggregatedTimelineTreeView.GroupBy;
const options = [
{label: i18nString(UIStrings.noGrouping), value: groupBy.None},
{label: i18nString(UIStrings.groupByActivity), value: groupBy.EventName},
{label: i18nString(UIStrings.groupByCategory), value: groupBy.Category},
{label: i18nString(UIStrings.groupByDomain), value: groupBy.Domain},
{label: i18nString(UIStrings.groupByFrame), value: groupBy.Frame},
{label: i18nString(UIStrings.groupBySubdomain), value: groupBy.Subdomain},
{label: i18nString(UIStrings.groupByUrl), value: groupBy.URL},
];
toolbar.appendToolbarItem(
new UI.Toolbar.ToolbarSettingComboBox(options, this.groupBySetting, i18nString(UIStrings.groupBy)));
toolbar.appendSpacer();
toolbar.appendToolbarItem(this.splitWidget.createShowHideSidebarButton(
i18nString(UIStrings.showHeaviestStack), i18nString(UIStrings.hideHeaviestStack),
i18nString(UIStrings.heaviestStackShown), i18nString(UIStrings.heaviestStackHidden)));
}
private buildHeaviestStack(treeNode: TimelineModel.TimelineProfileTree.Node):
TimelineModel.TimelineProfileTree.Node[] {
console.assert(Boolean(treeNode.parent), 'Attempt to build stack for tree root');
let result: TimelineModel.TimelineProfileTree.Node[] = [];
// Do not add root to the stack, as it's the tree itself.
for (let node: TimelineModel.TimelineProfileTree.Node = treeNode; node && node.parent; node = node.parent) {
result.push(node);
}
result = result.reverse();
for (let node: TimelineModel.TimelineProfileTree.Node = treeNode;
node && node.children() && node.children().size;) {
const children = Array.from(node.children().values());
node = children.reduce((a, b) => a.totalTime > b.totalTime ? a : b);
result.push(node);
}
return result;
}
exposePercentages(): boolean {
return true;
}
private onStackViewSelectionChanged(): void {
const treeNode = this.stackView.selectedTreeNode();
if (treeNode) {
this.selectProfileNode(treeNode, true);
}
}
showDetailsForNode(node: TimelineModel.TimelineProfileTree.Node): boolean {
const stack = this.buildHeaviestStack(node);
this.stackView.setStack(stack, node);
this.stackView.show(this.detailsView.element);
return true;
}
protected groupingFunction(groupBy: string): ((arg0: SDK.TracingModel.Event) => string)|null {
const GroupBy = AggregatedTimelineTreeView.GroupBy;
switch (groupBy) {
case GroupBy.None:
return null;
case GroupBy.EventName:
return (event: SDK.TracingModel.Event): string => TimelineUIUtils.eventStyle(event).title;
case GroupBy.Category:
return (event: SDK.TracingModel.Event): string => TimelineUIUtils.eventStyle(event).category.name;
case GroupBy.Subdomain:
return this.domainByEvent.bind(this, false);
case GroupBy.Domain:
return this.domainByEvent.bind(this, true);
case GroupBy.URL:
return (event: SDK.TracingModel.Event): string => TimelineModel.TimelineProfileTree.eventURL(event) || '';
case GroupBy.Frame:
return (event: SDK.TracingModel.Event): string =>
TimelineModel.TimelineModel.TimelineData.forEvent(event).frameId || '';
default:
console.assert(false, `Unexpected aggregation setting: ${groupBy}`);
return null;
}
}
private domainByEvent(groupSubdomains: boolean, event: SDK.TracingModel.Event): string {
const url = TimelineModel.TimelineProfileTree.eventURL(event);
if (!url) {
return '';
}
if (AggregatedTimelineTreeView.isExtensionInternalURL(url)) {
return AggregatedTimelineTreeView.extensionInternalPrefix;
}
if (AggregatedTimelineTreeView.isV8NativeURL(url)) {
return AggregatedTimelineTreeView.v8NativePrefix;
}
const parsedURL = Common.ParsedURL.ParsedURL.fromString(url);
if (!parsedURL) {
return '';
}
if (parsedURL.scheme === 'chrome-extension') {
return parsedURL.scheme + '://' + parsedURL.host;
}
if (!groupSubdomains) {
return parsedURL.host;
}
if (/^[.0-9]+$/.test(parsedURL.host)) {
return parsedURL.host;
}
const domainMatch = /([^.]*\.)?[^.]*$/.exec(parsedURL.host);
return domainMatch && domainMatch[0] || '';
}
appendContextMenuItems(contextMenu: UI.ContextMenu.ContextMenu, node: TimelineModel.TimelineProfileTree.Node): void {
if (this.groupBySetting.get() !== AggregatedTimelineTreeView.GroupBy.Frame) {
return;
}
if (!node.isGroupNode()) {
return;
}
if (!this.modelInternal) {
return;
}
const frame = this.modelInternal.timelineModel().pageFrameById((node.id as Protocol.Page.FrameId));
if (!frame || !frame.ownerNode) {
return;
}
contextMenu.appendApplicableItems(frame.ownerNode);
}
private static isExtensionInternalURL(url: Platform.DevToolsPath.UrlString): boolean {
return url.startsWith(AggregatedTimelineTreeView.extensionInternalPrefix);
}
private static isV8NativeURL(url: Platform.DevToolsPath.UrlString): boolean {
return url.startsWith(AggregatedTimelineTreeView.v8NativePrefix);
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/naming-convention
private static readonly extensionInternalPrefix = 'extensions::';
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/naming-convention
private static readonly v8NativePrefix = 'native ';
}
export namespace AggregatedTimelineTreeView {
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum GroupBy {
None = 'None',
EventName = 'EventName',
Category = 'Category',
Domain = 'Domain',
Subdomain = 'Subdomain',
URL = 'URL',
Frame = 'Frame',
}
}
export class CallTreeTimelineTreeView extends AggregatedTimelineTreeView {
constructor() {
super();
this.dataGrid.markColumnAsSortedBy('total', DataGrid.DataGrid.Order.Descending);
}
getToolbarInputAccessiblePlaceHolder(): string {
return i18nString(UIStrings.filterCallTree);
}
buildTree(): TimelineModel.TimelineProfileTree.Node {
const grouping = this.groupBySetting.get();
return this.buildTopDownTree(false, this.groupingFunction(grouping));
}
}
export class BottomUpTimelineTreeView extends AggregatedTimelineTreeView {
constructor() {
super();
this.dataGrid.markColumnAsSortedBy('self', DataGrid.DataGrid.Order.Descending);
}
getToolbarInputAccessiblePlaceHolder(): string {
return i18nString(UIStrings.filterBottomup);
}
buildTree(): TimelineModel.TimelineProfileTree.Node {
return new TimelineModel.TimelineProfileTree.BottomUpRootNode(
this.modelEvents(), this.textFilter(), this.filtersWithoutTextFilter(), this.startTime, this.endTime,
this.groupingFunction(this.groupBySetting.get()));
}
}
export class TimelineStackView extends
Common.ObjectWrapper.eventMixin<TimelineStackView.EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) {
private readonly treeView: TimelineTreeView;
private readonly dataGrid: DataGrid.ViewportDataGrid.ViewportDataGrid<unknown>;
constructor(treeView: TimelineTreeView) {
super();
const header = this.element.createChild('div', 'timeline-stack-view-header');
header.textContent = i18nString(UIStrings.heaviestStack);
this.treeView = treeView;
const columns = ([
{id: 'total', title: i18nString(UIStrings.totalTime), fixedWidth: true, width: '110px'},
{id: 'activity', title: i18nString(UIStrings.activity)},
] as DataGrid.DataGrid.ColumnDescriptor[]);
this.dataGrid = new DataGrid.ViewportDataGrid.ViewportDataGrid({
displayName: i18nString(UIStrings.timelineStack),
columns,
deleteCallback: undefined,
editCallback: undefined,
refreshCallback: undefined,
});
this.dataGrid.setResizeMethod(DataGrid.DataGrid.ResizeMethod.Last);
this.dataGrid.addEventListener(DataGrid.DataGrid.Events.SelectedNode, this.onSelectionChanged, this);
this.dataGrid.asWidget().show(this.element);
}
setStack(stack: TimelineModel.TimelineProfileTree.Node[], selectedNode: TimelineModel.TimelineProfileTree.Node):
void {
const rootNode = this.dataGrid.rootNode();
rootNode.removeChildren();
let nodeToReveal: GridNode|null = null;
const totalTime = Math.max.apply(Math, stack.map(node => node.totalTime));
for (const node of stack) {
const gridNode = new GridNode(node, totalTime, totalTime, totalTime, this.treeView);
rootNode.appendChild(gridNode);
if (node === selectedNode) {
nodeToReveal = gridNode;
}
}
if (nodeToReveal) {
nodeToReveal.revealAndSelect();
}
}
selectedTreeNode(): TimelineModel.TimelineProfileTree.Node|null {
const selectedNode = this.dataGrid.selectedNode;
return selectedNode && (selectedNode as GridNode).profileNode;
}
private onSelectionChanged(): void {
this.dispatchEventToListeners(TimelineStackView.Events.SelectionChanged);
}
}
export namespace TimelineStackView {
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
SelectionChanged = 'SelectionChanged',
}
export type EventTypes = {
[Events.SelectionChanged]: void,
};
}