UNPKG

chrome-devtools-frontend

Version:
1,257 lines (1,158 loc) • 39.9 kB
// 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 '../common/common.js'; import * as Components from '../components/components.js'; import * as DataGrid from '../data_grid/data_grid.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import * as SDK from '../sdk/sdk.js'; import * as TimelineModel from '../timeline_model/timeline_model.js'; import * as UI from '../ui/ui.js'; import {PerformanceModel} from './PerformanceModel.js'; // eslint-disable-line no-unused-vars import {TimelineRegExp} from './TimelineFilters.js'; import {TimelineSelection} from './TimelinePanel.js'; // eslint-disable-line no-unused-vars import {TimelineUIUtils} from './TimelineUIUtils.js'; export 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 */ 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 Header text content in Timeline Tree View of the Performance panel */ heaviestStack: 'Heaviest stack', /** *@description Data grid name for Timeline Stack data grids */ timelineStack: 'Timeline Stack', }; const str_ = i18n.i18n.registerUIStrings('timeline/TimelineTreeView.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); /** * @implements {UI.SearchableView.Searchable} */ export class TimelineTreeView extends UI.Widget.VBox { constructor() { super(); /** @type {?PerformanceModel} */ this._model = null; /** @type {?TimelineModel.TimelineModel.Track} */ this._track = null; /** @type {?TimelineModel.TimelineProfileTree.Node} */ this._tree = null; this.element.classList.add('timeline-tree-view'); /** @type {!Array<!TimelineModel.TimelineProfileTree.Node>} */ this._searchResults = []; /** @type {!Components.Linkifier.Linkifier} */ this.linkifier; /** @type {!DataGrid.SortableDataGrid.SortableDataGrid<!GridNode>} */ this.dataGrid; /** @type {?TimelineModel.TimelineProfileTree.Node} */ this._lastHoveredProfileNode; /** @type {!TimelineRegExp} */ this._textFilter; /** @type {!TimelineModel.TimelineModelFilter.ExclusiveNameFilter} */ this._taskFilter; /** @type {number} */ this._startTime; /** @type {number} */ this._endTime; /** @type {!UI.SplitWidget.SplitWidget} */ this.splitWidget; /** @type {!UI.Widget.Widget} */ this.detailsView; /** @type {!UI.SearchableView.SearchableView} */ this._searchableView; } /** * @param {!SDK.TracingModel.Event} event * @return {string} */ static eventNameForSorting(event) { 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); } /** * @param {!UI.SearchableView.SearchableView} searchableView */ setSearchableView(searchableView) { this._searchableView = searchableView; } /** * @param {?PerformanceModel} model * @param {?TimelineModel.TimelineModel.Track} track */ setModel(model, track) { this._model = model; this._track = track; this.refreshTree(); } /** * @return {string} */ getToolbarInputAccessiblePlaceHolder() { return ''; } /** * @protected * @return {?PerformanceModel} model */ model() { return this._model; } /** * @protected */ init() { this.linkifier = new Components.Linkifier.Linkifier(); this._taskFilter = new TimelineModel.TimelineModelFilter.ExclusiveNameFilter([TimelineModel.TimelineModel.RecordType.Task]); this._textFilter = new TimelineRegExp(); this._currentThreadSetting = Common.Settings.Settings.instance().createSetting('timelineTreeCurrentThread', 0); this._currentThreadSetting.addChangeListener(this.refreshTree, this); const columns = /** @type {!Array<!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); 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); /** @type {?TimelineModel.TimelineProfileTree.Node|undefined} */ this._lastSelectedNode; } /** * @protected * @return {?TimelineModel.TimelineProfileTree.Node|undefined} */ lastSelectedNode() { return this._lastSelectedNode; } /** * @param {!TimelineSelection} selection */ updateContents(selection) { this.setRange(selection.startTime(), selection.endTime()); } /** * @param {number} startTime * @param {number} endTime */ setRange(startTime, endTime) { this._startTime = startTime; this._endTime = endTime; this.refreshTree(); } /** * @protected * @return {!Array<!TimelineModel.TimelineModelFilter.TimelineModelFilter>} */ filters() { return [this._taskFilter, this._textFilter, ...(this._model ? this._model.filters() : [])]; } /** * @protected * @return {!Array<!TimelineModel.TimelineModelFilter.TimelineModelFilter>} */ filtersWithoutTextFilter() { return [this._taskFilter, ...(this._model ? this._model.filters() : [])]; } /** * @protected * @return {!TimelineRegExp} */ textFilter() { return this._textFilter; } /** * @return {boolean} */ _exposePercentages() { return false; } /** * @protected * @param {!UI.Toolbar.Toolbar} toolbar */ populateToolbar(toolbar) { const textFilterUI = new UI.Toolbar.ToolbarInput(i18nString(UIStrings.filter), this.getToolbarInputAccessiblePlaceHolder()); textFilterUI.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, () => { const searchQuery = textFilterUI.value(); this._textFilter.setRegExp(searchQuery ? createPlainTextSearchRegex(searchQuery, 'i') : null); this.refreshTree(); }, this); this._textFilterUI = textFilterUI; toolbar.appendToolbarItem(textFilterUI); } /** * @return {!Array<!SDK.TracingModel.Event>} */ _modelEvents() { return this._track ? this._track.syncEvents() : []; } /** * @param {?TimelineModel.TimelineProfileTree.Node} node */ _onHover(node) { } /** * @param {!UI.ContextMenu.ContextMenu} contextMenu * @param {!TimelineModel.TimelineProfileTree.Node} node */ _appendContextMenuItems(contextMenu, node) { } /** * @param {!SDK.TracingModel.Event} event * @return {?Element} */ _linkifyLocation(event) { if (!this._model) { return null; } const target = this._model.timelineModel().targetByEvent(event); if (!target) { return null; } const frame = TimelineModel.TimelineProfileTree.eventStackFrame(event); if (!frame) { return null; } return this.linkifier.maybeLinkifyConsoleCallFrame(target, frame); } /** * @param {!TimelineModel.TimelineProfileTree.Node} treeNode * @param {boolean} suppressSelectedEvent */ selectProfileNode(treeNode, suppressSelectedEvent) { const pathToRoot = []; /** @type {?TimelineModel.TimelineProfileTree.Node} */ let 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); } } /** * @protected */ refreshTree() { this.linkifier.reset(); this.dataGrid.rootNode().removeChildren(); if (!this._model) { 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); } } /** * @return {!TimelineModel.TimelineProfileTree.Node} */ _buildTree() { throw new Error('Not Implemented'); } /** * @protected * @param {boolean} doNotAggregate * @param {?function(!SDK.TracingModel.Event):string} groupIdCallback * @return {!TimelineModel.TimelineProfileTree.Node} */ buildTopDownTree(doNotAggregate, groupIdCallback) { return new TimelineModel.TimelineProfileTree.TopDownRootNode( this._modelEvents(), this.filters(), this._startTime, this._endTime, doNotAggregate, groupIdCallback); } /** * @protected * @param {!Array<!DataGrid.DataGrid.ColumnDescriptor>} columns */ populateColumns(columns) { columns.push( /** @type {!DataGrid.DataGrid.ColumnDescriptor} */ ( {id: 'self', title: i18nString(UIStrings.selfTime), width: '120px', fixedWidth: true, sortable: true})); columns.push( /** @type {!DataGrid.DataGrid.ColumnDescriptor} */ ( {id: 'total', title: i18nString(UIStrings.totalTime), width: '120px', fixedWidth: true, sortable: true})); columns.push(/** @type {!DataGrid.DataGrid.ColumnDescriptor} */ ( {id: 'activity', title: i18nString(UIStrings.activity), disclosure: true, sortable: true})); } _sortingChanged() { 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()); /** * @param {string} field * @param {!DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>} a * @param {!DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>} b * @return {number} */ function compareNumericField(field, a, b) { const nodeA = /** @type {!TreeGridNode} */ (a); const nodeB = /** @type {!TreeGridNode} */ (b); return /** @type {*} */ (nodeA._profileNode)[field] - /** @type {*} */ (nodeB._profileNode)[field]; } /** * @param {!DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>} a * @param {!DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>} b * @return {number} */ function compareStartTime(a, b) { const nodeA = /** @type {!TreeGridNode} */ (a); const nodeB = /** @type {!TreeGridNode} */ (b); const eventA = /** @type {!SDK.TracingModel.Event} */ (nodeA._profileNode.event); const eventB = /** @type {!SDK.TracingModel.Event} */ (nodeB._profileNode.event); return eventA.startTime - eventB.startTime; } /** * @param {!DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>} a * @param {!DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>} b * @return {number} */ function compareName(a, b) { const nodeA = /** @type {!TreeGridNode} */ (a); const nodeB = /** @type {!TreeGridNode} */ (b); const eventA = /** @type {!SDK.TracingModel.Event} */ (nodeA._profileNode.event); const eventB = /** @type {!SDK.TracingModel.Event} */ (nodeB._profileNode.event); const nameA = TimelineTreeView.eventNameForSorting(eventA); const nameB = TimelineTreeView.eventNameForSorting(eventB); return nameA.localeCompare(nameB); } } _onShowModeChanged() { if (this.splitWidget.showMode() === UI.SplitWidget.ShowMode.OnlyMain) { return; } this._lastSelectedNode = undefined; this._updateDetailsForSelection(); } _updateDetailsForSelection() { const selectedNode = this.dataGrid.selectedNode ? /** @type {!TreeGridNode} */ (this.dataGrid.selectedNode)._profileNode : null; if (selectedNode === this._lastSelectedNode) { return; } this._lastSelectedNode = 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)); } /** * @param {!TimelineModel.TimelineProfileTree.Node} node * @return {boolean} */ _showDetailsForNode(node) { return false; } /** * @param {!Event} event */ _onMouseMove(event) { const gridNode = event.target && (event.target instanceof Node) ? /** @type {?TreeGridNode} */ (this.dataGrid.dataGridNodeFromNode(/** @type {!Node} */ (event.target))) : null; const profileNode = gridNode && gridNode._profileNode; if (profileNode === this._lastHoveredProfileNode) { return; } this._lastHoveredProfileNode = profileNode; this._onHover(profileNode); } /** * @param {!UI.ContextMenu.ContextMenu} contextMenu * @param {!DataGrid.DataGrid.DataGridNode<!GridNode>} eventGridNode */ _onContextMenu(contextMenu, eventGridNode) { const gridNode = /** @type {!GridNode} */ (eventGridNode); if (gridNode._linkElement && !contextMenu.containsTarget(gridNode._linkElement)) { contextMenu.appendApplicableItems(gridNode._linkElement); } const profileNode = gridNode._profileNode; if (profileNode) { this._appendContextMenuItems(contextMenu, profileNode); } } /** * @param {!TimelineModel.TimelineProfileTree.Node} treeNode * @protected * @return {?GridNode} */ dataGridNodeForTreeNode(treeNode) { return profileNodeToTreeGridNode.get(treeNode) || null; } // UI.SearchableView.Searchable implementation /** * @override */ searchCanceled() { this._searchResults = []; this._currentResult = 0; } /** * @override * @param {!UI.SearchableView.SearchConfig} searchConfig * @param {boolean} shouldJump * @param {boolean=} jumpBackwards */ performSearch(searchConfig, shouldJump, jumpBackwards) { this._searchResults = []; this._currentResult = 0; if (!this._root) { return; } const searchRegex = searchConfig.toSearchRegex(); this._searchResults = this._root.searchTree(event => TimelineUIUtils.testContentMatching(event, searchRegex)); this._searchableView.updateSearchMatchesCount(this._searchResults.length); } /** * @override */ jumpToNextSearchResult() { if (!this._searchResults.length) { return; } this.selectProfileNode(this._searchResults[this._currentResult], false); this._currentResult = Platform.NumberUtilities.mod(this._currentResult + 1, this._searchResults.length); } /** * @override */ jumpToPreviousSearchResult() { if (!this._searchResults.length) { return; } this.selectProfileNode(this._searchResults[this._currentResult], false); this._currentResult = Platform.NumberUtilities.mod(this._currentResult - 1, this._searchResults.length); } /** * @override * @return {boolean} */ supportsCaseSensitiveSearch() { return true; } /** * @override * @return {boolean} */ supportsRegexSearch() { return true; } } /** * @extends {DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>} */ export class GridNode extends DataGrid.SortableDataGrid.SortableDataGridNode { /** * @param {!TimelineModel.TimelineProfileTree.Node} profileNode * @param {number} grandTotalTime * @param {number} maxSelfTime * @param {number} maxTotalTime * @param {!TimelineTreeView} treeView */ constructor(profileNode, grandTotalTime, maxSelfTime, maxTotalTime, treeView) { super(null, false); this._populated = false; this._profileNode = profileNode; this._treeView = treeView; this._grandTotalTime = grandTotalTime; this._maxSelfTime = maxSelfTime; this._maxTotalTime = maxTotalTime; this._linkElement = null; } /** * @override * @param {string} columnId * @return {!HTMLElement} */ createCell(columnId) { if (columnId === 'activity') { return this._createNameCell(columnId); } return this._createValueCell(columnId) || super.createCell(columnId); } /** * @param {string} columnId * @return {!HTMLElement} */ _createNameCell(columnId) { 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 = /** @type {!AggregatedTimelineTreeView} */ (this._treeView); 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; } /** * @param {string} columnId * @return {?HTMLElement} */ _createValueCell(columnId) { if (columnId !== 'self' && columnId !== 'total' && columnId !== 'startTime') { return null; } let showPercents = false; /** @type {number} */ let value; /** @type {number|undefined} */ let maxTime; /** @type {?SDK.TracingModel.Event} */ let event; switch (columnId) { case 'startTime': event = this._profileNode.event; if (!this._treeView._model) { throw new Error('Unable to find model for tree view'); } value = (event ? event.startTime : 0) - this._treeView._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'; 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 { /** * @param {!TimelineModel.TimelineProfileTree.Node} profileNode * @param {number} grandTotalTime * @param {number} maxSelfTime * @param {number} maxTotalTime * @param {!TimelineTreeView} treeView */ constructor(profileNode, grandTotalTime, maxSelfTime, maxTotalTime, treeView) { super(profileNode, grandTotalTime, maxSelfTime, maxTotalTime, treeView); this.setHasChildren(this._profileNode.hasChildren()); profileNodeToTreeGridNode.set(profileNode, this); } /** * @override */ populate() { 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); } } } TreeGridNode._gridNodeSymbol = Symbol('treeGridNode'); /** @type {!WeakMap<!TimelineModel.TimelineProfileTree.Node, !TreeGridNode>} */ const profileNodeToTreeGridNode = new WeakMap(); export class AggregatedTimelineTreeView extends TimelineTreeView { 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); /** @type {!Map<string, string>} */ this._productByURLCache = new Map(); /** @type {!Map<string, string>} */ this._colorByURLCache = new Map(); this._executionContextNamesByOrigin = new Map(); } /** * @override * @param {?PerformanceModel} model * @param {?TimelineModel.TimelineModel.Track} track */ setModel(model, track) { super.setModel(model, track); } /** * @override * @param {!TimelineSelection} selection */ updateContents(selection) { this._updateExtensionResolver(); super.updateContents(selection); const rootNode = this.dataGrid.rootNode(); if (rootNode.children.length) { rootNode.children[0].select(/* suppressSelectedEvent */ true); } } _updateExtensionResolver() { this._executionContextNamesByOrigin = new Map(); for (const runtimeModel of SDK.SDKModel.TargetManager.instance().models(SDK.RuntimeModel.RuntimeModel)) { for (const context of runtimeModel.executionContexts()) { this._executionContextNamesByOrigin.set(context.origin, context.name); } } } /** * @param {string} name * @return {string} * @this {AggregatedTimelineTreeView} */ _beautifyDomainName(name) { if (AggregatedTimelineTreeView._isExtensionInternalURL(name)) { name = i18nString(UIStrings.chromeExtensionsOverhead); } else if (AggregatedTimelineTreeView._isV8NativeURL(name)) { name = i18nString(UIStrings.vRuntime); } else if (name.startsWith('chrome-extension')) { name = this._executionContextNamesByOrigin.get(name) || name; } return name; } /** * @param {!TimelineModel.TimelineProfileTree.Node} node * @return {!{name: string, color: string, icon: (!Element|undefined)}} */ _displayInfoForGroupNode(node) { const categories = TimelineUIUtils.categories(); const color = node.id ? TimelineUIUtils.eventColor(/** @type {!SDK.TracingModel.Event} */ (node.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._model) { throw new Error('Unable to find model for group by frame operation'); } const frame = id ? this._model.timelineModel().pageFrameById(id) : 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}; } /** * @override * @param {!UI.Toolbar.Toolbar} toolbar */ populateToolbar(toolbar) { 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.heaviestStack))); } /** * @param {!TimelineModel.TimelineProfileTree.Node} treeNode * @return {!Array<!TimelineModel.TimelineProfileTree.Node>} */ _buildHeaviestStack(treeNode) { console.assert(Boolean(treeNode.parent), 'Attempt to build stack for tree root'); let result = []; // Do not add root to the stack, as it's the tree itself. for (let node = treeNode; node && node.parent; node = node.parent) { result.push(node); } result = result.reverse(); for (let 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; } /** * @override * @return {boolean} */ _exposePercentages() { return true; } _onStackViewSelectionChanged() { const treeNode = this._stackView.selectedTreeNode(); if (treeNode) { this.selectProfileNode(treeNode, true); } } /** * @override * @param {!TimelineModel.TimelineProfileTree.Node} node * @return {boolean} */ _showDetailsForNode(node) { const stack = this._buildHeaviestStack(node); this._stackView.setStack(stack, node); this._stackView.show(this.detailsView.element); return true; } /** * @param {!AggregatedTimelineTreeView.GroupBy} groupBy * @return {?function(!SDK.TracingModel.Event):string} */ _groupingFunction(groupBy) { const GroupBy = AggregatedTimelineTreeView.GroupBy; switch (groupBy) { case GroupBy.None: return null; case GroupBy.EventName: return event => TimelineUIUtils.eventStyle(event).title; case GroupBy.Category: return event => 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 => TimelineModel.TimelineProfileTree.eventURL(event) || ''; case GroupBy.Frame: return event => TimelineModel.TimelineModel.TimelineData.forEvent(event).frameId; default: console.assert(false, `Unexpected aggregation setting: ${groupBy}`); return null; } } /** * @param {boolean} groupSubdomains * @param {!SDK.TracingModel.Event} event * @return {string} */ _domainByEvent(groupSubdomains, event) { 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] || ''; } /** * @override * @param {!UI.ContextMenu.ContextMenu} contextMenu * @param {!TimelineModel.TimelineProfileTree.Node} node */ _appendContextMenuItems(contextMenu, node) { if (this._groupBySetting.get() !== AggregatedTimelineTreeView.GroupBy.Frame) { return; } if (!node.isGroupNode()) { return; } if (!this._model) { return; } const frame = this._model.timelineModel().pageFrameById(/** @type {string} */ (node.id)); if (!frame || !frame.ownerNode) { return; } contextMenu.appendApplicableItems(frame.ownerNode); } /** * @param {string} url * @return {boolean} */ static _isExtensionInternalURL(url) { return url.startsWith(AggregatedTimelineTreeView._extensionInternalPrefix); } /** * @param {string} url * @return {boolean} */ static _isV8NativeURL(url) { return url.startsWith(AggregatedTimelineTreeView._v8NativePrefix); } } AggregatedTimelineTreeView._extensionInternalPrefix = 'extensions::'; AggregatedTimelineTreeView._v8NativePrefix = 'native '; /** * @enum {string} */ AggregatedTimelineTreeView.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); } /** * @override * @return {string} */ getToolbarInputAccessiblePlaceHolder() { return i18nString(UIStrings.filterCallTree); } /** * @override * @return {!TimelineModel.TimelineProfileTree.Node} */ _buildTree() { 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); } /** * @override * @return {string} */ getToolbarInputAccessiblePlaceHolder() { return i18nString(UIStrings.filterBottomup); } /** * @override * @return {!TimelineModel.TimelineProfileTree.Node} */ _buildTree() { return new TimelineModel.TimelineProfileTree.BottomUpRootNode( this._modelEvents(), this.textFilter(), this.filtersWithoutTextFilter(), this._startTime, this._endTime, this._groupingFunction(this._groupBySetting.get())); } } export class TimelineStackView extends UI.Widget.VBox { /** * @param {!TimelineTreeView} treeView */ constructor(treeView) { super(); const header = this.element.createChild('div', 'timeline-stack-view-header'); header.textContent = i18nString(UIStrings.heaviestStack); this._treeView = treeView; const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([ {id: 'total', title: i18nString(UIStrings.totalTime), fixedWidth: true, width: '110px'}, {id: 'activity', title: i18nString(UIStrings.activity)} ]); 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); } /** * @param {!Array<!TimelineModel.TimelineProfileTree.Node>} stack * @param {!TimelineModel.TimelineProfileTree.Node} selectedNode */ setStack(stack, selectedNode) { const rootNode = this._dataGrid.rootNode(); rootNode.removeChildren(); let nodeToReveal = 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(); } } /** * @return {?TimelineModel.TimelineProfileTree.Node} */ selectedTreeNode() { const selectedNode = this._dataGrid.selectedNode; return selectedNode && /** @type {!GridNode} */ (selectedNode)._profileNode; } _onSelectionChanged() { this.dispatchEventToListeners(TimelineStackView.Events.SelectionChanged); } } /** @enum {symbol} */ TimelineStackView.Events = { SelectionChanged: Symbol('SelectionChanged') };