chrome-devtools-frontend
Version:
Chrome DevTools UI
1,357 lines (1,202 loc) • 48.4 kB
text/typescript
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */
import '../../ui/components/icon_button/icon_button.js';
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.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 Bindings from '../../models/bindings/bindings.js';
import * as CPUProfile from '../../models/cpu_profile/cpu_profile.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import type * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as SettingsUI from '../../ui/legacy/components/settings_ui/settings_ui.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, nothing, render, type TemplateResult} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import {BottomUpProfileDataGridTree} from './BottomUpProfileDataGrid.js';
import {Events, HeapTimelineOverview, type IdsRangeChangedEvent, type Samples} from './HeapTimelineOverview.js';
import {type Formatter, type ProfileDataGridNode, ProfileDataGridTree} from './ProfileDataGrid.js';
import {ProfileFlameChart, ProfileFlameChartDataProvider} from './ProfileFlameChartDataProvider.js';
import {ProfileEvents, type ProfileHeader, ProfileType} from './ProfileHeader.js';
import {TopDownProfileDataGridTree} from './TopDownProfileDataGrid.js';
import {WritableProfileHeader} from './WritableProfileHeader.js';
const {repeat, ref} = Directives;
const UIStrings = {
/**
* @description The reported total size used in the selected time frame of the allocation sampling profile
* @example {3 MB} PH1
*/
selectedSizeS: 'Selected size: {PH1}',
/**
* @description Name of column header that reports the size (in terms of bytes) used for a particular part of the heap, excluding the size of the children nodes of this part of the heap
*/
selfSizeBytes: 'Self size',
/**
* @description Name of column header that reports the total size (in terms of bytes) used for a particular part of the heap
*/
totalSizeBytes: 'Total size',
/**
* @description Button text to stop profiling the heap
*/
stopHeapProfiling: 'Stop heap profiling',
/**
* @description Button text to start profiling the heap
*/
startHeapProfiling: 'Start heap profiling',
/**
* @description Progress update that the profiler is recording the contents of the heap
*/
recording: 'Recording…',
/**
* @description Icon title in Heap Profile View of a profiler tool
*/
heapProfilerIsRecording: 'Heap profiler is recording',
/**
* @description Progress update that the profiler is in the process of stopping its recording of the heap
*/
stopping: 'Stopping…',
/**
* @description Sampling category to only profile allocations happening on the heap
*/
allocationSampling: 'Allocation sampling',
/**
* @description The title for the collection of profiles that are gathered from various snapshots of the heap, using a sampling (e.g. every 1/100) technique.
*/
samplingProfiles: 'Sampling profiles',
/**
* @description Description in Heap Profile View of a profiler tool
*/
recordMemoryAllocations:
'Approximate memory allocations by sampling long operations with minimal overhead and get a breakdown by JavaScript execution stack',
/**
* @description Name of a profile
* @example {2} PH1
*/
profileD: 'Profile {PH1}',
/**
* @description Accessible text for the value in bytes in memory allocation or coverage view.
* @example {12345} PH1
*/
sBytes: '{PH1} bytes',
/**
* @description Text in CPUProfile View of a profiler tool
* @example {21.33} PH1
*/
formatPercent: '{PH1}Â %',
/**
* @description The formatted size in kilobytes, abbreviated to kB
* @example {1,021} PH1
*/
skb: '{PH1}Â kB',
/**
* @description Text for the name of something
*/
name: 'Name',
/**
* @description Tooltip of a cell that reports the size used for a particular part of the heap, excluding the size of the children nodes of this part of the heap
*/
selfSize: 'Self size',
/**
* @description Tooltip of a cell that reports the total size used for a particular part of the heap
*/
totalSize: 'Total size',
/**
* @description Text for web URLs
*/
url: 'URL',
/**
* @description Label for a checkbox in the memory panel to enable sampling heap profiler timeline.
*/
samplingHeapProfilerTimeline: 'Sampling heap profiler timeline',
/**
* @description Text in Profile View of a profiler tool
*/
profile: 'Profile',
/**
* @description Placeholder text in the search box of the JavaScript profiler tool. Users can search
*the results by the cost in milliseconds, the name of the function, or the file name.
*/
findByCostMsNameOrFile: 'Find by cost (>50ms), name or file',
/**
* @description Text for a programming function
*/
function: 'Function',
/**
* @description Title of the Profiler tool
*/
profiler: 'Profiler',
/**
* @description Aria-label for profiles view combobox in memory tool
*/
profileViewMode: 'Profile view mode',
/**
* @description Tooltip text that appears when hovering over the largeicon visibility button in the Profile View of a profiler tool
*/
focusSelectedFunction: 'Focus selected function',
/**
* @description Tooltip text that appears when hovering over the largeicon delete button in the Profile View of a profiler tool
*/
excludeSelectedFunction: 'Exclude selected function',
/**
* @description Tooltip text that appears when hovering over the largeicon refresh button in the Profile View of a profiler tool
*/
restoreAllFunctions: 'Restore all functions',
/**
* @description Text in Profile View of a profiler tool
*/
chart: 'Chart',
/**
* @description Text in Profile View of a profiler tool
*/
heavyBottomUp: 'Heavy (Bottom Up)',
/**
* @description Text for selecting different profile views in the JS profiler tool. This option is a tree view.
*/
treeTopDown: 'Tree (Top Down)',
/**
* @description Tooltip to alert developers that some parts of code in execution were not optimized.
* @example {Optimized too many times} PH1
*/
notOptimizedS: 'Not optimized: {PH1}',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/profiler/HeapProfileView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
function convertToSamplingHeapProfile(profileHeader: SamplingHeapProfileHeader):
Protocol.HeapProfiler.SamplingHeapProfile {
return (profileHeader.profile || profileHeader.protocolProfile()) as Protocol.HeapProfiler.SamplingHeapProfile;
}
export const maxLinkLength = 30;
export const enum ViewTypes {
FLAME = 'Flame',
TREE = 'Tree',
HEAVY = 'Heavy',
}
export class HeapProfileView extends UI.View.SimpleView implements UI.SearchableView.Searchable {
profileHeader: SamplingHeapProfileHeader;
readonly profileType: SamplingHeapProfileTypeBase;
adjustedTotal: number;
selectedSizeText: HTMLElement|undefined;
timestamps: number[] = [];
sizes: number[] = [];
max: number[] = [];
ordinals: number[] = [];
totalTime = 0;
lastOrdinal = 0;
readonly timelineOverview: HeapTimelineOverview = new HeapTimelineOverview();
profileInternal: CPUProfile.ProfileTreeModel.ProfileTreeModel|null = null;
searchableViewInternal!: UI.SearchableView.SearchableView;
dataGrid: UI.Widget.Widget;
viewSelectComboBox: HTMLSelectElement|undefined;
focusButton: Buttons.Button.Button|undefined;
excludeButton: Buttons.Button.Button|undefined;
resetButton: Buttons.Button.Button|undefined;
#sortColumnId = 'self';
#sortAscending = false;
#selectedNode: ProfileDataGridNode|null = null;
readonly linkifierInternal: Components.Linkifier.Linkifier = new Components.Linkifier.Linkifier(maxLinkLength);
nodeFormatter!: Formatter;
viewType!: Common.Settings.Setting<ViewTypes>;
bottomUpProfileDataGridTree?: BottomUpProfileDataGridTree|null;
topDownProfileDataGridTree?: TopDownProfileDataGridTree|null;
currentSearchResultIndex?: number;
dataProvider?: ProfileFlameChartDataProvider;
flameChart?: ProfileFlameChart;
visibleView?: UI.Widget.Widget;
searchableElement?: ProfileDataGridTree|ProfileFlameChart;
profileDataGridTree?: ProfileDataGridTree;
#isNodeSelected = false;
#isResetEnabled = false;
#selectedSize: number|null = null;
#minId: number|null = null;
#maxId: number|null = null;
#lastAppliedRange: {minId: number, maxId: number}|null = null;
#lastAppliedViewType: ViewTypes|null = null;
constructor(profileHeader: SamplingHeapProfileHeader) {
super({
title: i18nString(UIStrings.profile),
viewId: 'profile',
});
this.#setupSearchableView();
this.dataGrid = new UI.Widget.VBox();
this.profileHeader = profileHeader;
this.profileType = profileHeader.profileType();
this.initialize(new NodeFormatter(this));
const profile = new SamplingHeapProfileModel(convertToSamplingHeapProfile(profileHeader));
this.adjustedTotal = profile.total;
this.setProfile(profile);
this.#setupTimelineOverview();
}
#setupTimelineOverview(): void {
if (this.profileType.hasTemporaryView()) {
this.timelineOverview.addEventListener(Events.IDS_RANGE_CHANGED, this.onIdsRangeChanged.bind(this));
this.timelineOverview.show(this.element, this.element.firstChild);
this.timelineOverview.start();
this.profileType.addEventListener(SamplingHeapProfileType.Events.STATS_UPDATE, this.onStatsUpdate, this);
void this.profileType.once(ProfileEvents.PROFILE_COMPLETE).then(() => {
this.profileType.removeEventListener(SamplingHeapProfileType.Events.STATS_UPDATE, this.onStatsUpdate, this);
this.timelineOverview.stop();
this.timelineOverview.updateGrid();
});
}
}
#setupSearchableView(): void {
this.searchableViewInternal = new UI.SearchableView.SearchableView(this, null);
this.searchableViewInternal.setPlaceholder(i18nString(UIStrings.findByCostMsNameOrFile));
this.searchableViewInternal.show(this.element);
}
#renderDataGrid(): void {
if (!this.profileDataGridTree) {
return;
}
const onSort = (e: CustomEvent<{columnId: string, ascending: boolean}>): void => {
this.#sortColumnId = e.detail.columnId;
this.#sortAscending = e.detail.ascending;
this.sortProfile();
};
const onDeselect = (): void => {
this.#selectedNode = null;
this.nodeSelected(false);
};
// clang-format off
// eslint-disable-next-line @devtools/no-lit-render-outside-of-view
render(html`
<devtools-data-grid class="flex-auto" name=${i18nString(UIStrings.profiler)} striped autofocus resize="last"
=${onSort} =${onDeselect} .template=${html`
<table>
<tr>
<th id="self" width="120px" fixed weight="1" sortable
sort=${this.#sortColumnId === 'self' ? (this.#sortAscending ? 'ascending' : 'descending') : 'none'}>
${this.columnHeader('self')}
</th>
<th id="total" width="120px" fixed weight="1" sortable
sort=${this.#sortColumnId === 'total' ? (this.#sortAscending ? 'ascending' : 'descending') : 'none'}>
${this.columnHeader('total')}
</th>
<th id="function" weight="3" sortable disclosure
sort=${this.#sortColumnId === 'function' ? (this.#sortAscending ? 'ascending' : 'descending') : 'none'}>
${i18nString(UIStrings.function)}
</th>
</tr>
${repeat(this.profileDataGridTree.children,
(node: ProfileDataGridNode) => node.callUID,
(node: ProfileDataGridNode) => this.#renderNode(node))}
</table>`}>
</devtools-data-grid>
`, this.dataGrid.element);
// clang-format on
}
#renderNode(node: ProfileDataGridNode): TemplateResult {
const onSelect = (): void => {
this.#selectedNode = node;
this.nodeSelected(true);
};
const onContextMenu = (event: CustomEvent<UI.ContextMenu.ContextMenu>): void => {
this.populateContextMenu(event.detail, node);
};
const onExpand = (): void => {
node.expanded = true;
node.populate();
this.refresh();
};
const onCollapse = (): void => {
node.expanded = false;
this.refresh();
};
if (node.profileNode.scriptId !== '0' && !node.linkElement) {
node.linkElement = this.nodeFormatter.linkifyNode(node);
if (node.linkElement) {
(node.linkElement as HTMLElement).style.maxWidth = '75%';
}
}
// clang-format off
return html`
<tr data-uid=${node.callUID} ?selected=${this.#selectedNode === node} ?expanded=${node.expanded} =${onSelect}
=${onContextMenu} =${onExpand} =${onCollapse}>
<td data-value=${node.self} class="numeric-column ${node.searchMatchedSelfColumn ? 'highlight' : ''}"
aria-label=${`${this.nodeFormatter.formatValueAccessibleText(node.self, node)}, ${this.nodeFormatter.formatPercent(node.selfPercent, node)}`}>
<div class="profile-multiple-values">
<span>${this.nodeFormatter.formatValue(node.self, node)}</span>
<span class="percent-column">${this.nodeFormatter.formatPercent(node.selfPercent, node)}</span>
</div>
</td>
<td data-value=${node.total} class="numeric-column ${node.searchMatchedTotalColumn ? 'highlight' : ''}"
aria-label=${`${this.nodeFormatter.formatValueAccessibleText(node.total, node)}, ${this.nodeFormatter.formatPercent(node.totalPercent, node)}`}>
<div class="profile-multiple-values">
<span>${this.nodeFormatter.formatValue(node.total, node)}</span>
<span class="percent-column">${this.nodeFormatter.formatPercent(node.totalPercent, node)}</span>
</div>
</td>
<td data-value=${node.functionName} class="${node.searchMatchedFunctionColumn ? 'highlight' : ''} ${node.deoptReason ? 'not-optimized' : ''}">
${node.deoptReason ? html`
<devtools-icon name="warning-filled" class="profile-warn-marker small"
title=${i18nString(UIStrings.notOptimizedS, {PH1: node.deoptReason})}>
</devtools-icon>` : nothing}
${node.functionName}
${node.linkElement ? node.linkElement : nothing}
</td>
${node.hasChildren() ? html`
<td><table>
${node.expanded ? html`${repeat(
node.children as ProfileDataGridNode[],
child => child.callUID,
child => this.#renderNode(child))}` : nothing}
</table></td>` : nothing}
</tr>`;
// clang-format on
}
override async toolbarItems(): Promise<TemplateResult> {
const currentViewType = this.viewType.get();
const isFlame = currentViewType === ViewTypes.FLAME;
// clang-format off
return html`
<select title=${i18nString(UIStrings.profileViewMode)} aria-label=${i18nString(UIStrings.profileViewMode)}
=${this.changeView.bind(this)}
jslog=${VisualLogging.dropDown('profile-view.selected-view').track({change: true})}
${ref(e => { this.viewSelectComboBox = e as HTMLSelectElement; })}>
<option value=${ViewTypes.FLAME} ?selected=${currentViewType === ViewTypes.FLAME}>
${i18nString(UIStrings.chart)}
</option>
<option value=${ViewTypes.HEAVY} ?selected=${currentViewType === ViewTypes.HEAVY}>
${i18nString(UIStrings.heavyBottomUp)}
</option>
<option value=${ViewTypes.TREE} ?selected=${currentViewType === ViewTypes.TREE}>
${i18nString(UIStrings.treeTopDown)}
</option>
</select>
<devtools-button .data=${{
iconName: 'eye',
variant: Buttons.Button.Variant.TOOLBAR,
title: i18nString(UIStrings.focusSelectedFunction),
jslogContext: 'profile-view.focus-selected-function',
disabled: !this.#isNodeSelected,
} as Buttons.Button.ButtonData}
=${this.focusClicked.bind(this)}
?hidden=${isFlame}
${ref(e => { this.focusButton = e as Buttons.Button.Button; })}>
</devtools-button>
<devtools-button .data=${{
iconName: 'cross',
variant: Buttons.Button.Variant.TOOLBAR,
title: i18nString(UIStrings.excludeSelectedFunction),
jslogContext: 'profile-view.exclude-selected-function',
disabled: !this.#isNodeSelected,
} as Buttons.Button.ButtonData}
=${this.excludeClicked.bind(this)}
?hidden=${isFlame}
${ref(e => { this.excludeButton = e as Buttons.Button.Button; })}>
</devtools-button>
<devtools-button .data=${{
iconName: 'refresh',
variant: Buttons.Button.Variant.TOOLBAR,
title: i18nString(UIStrings.restoreAllFunctions),
jslogContext: 'profile-view.restore-all-functions',
disabled: !this.#isResetEnabled,
} as Buttons.Button.ButtonData}
=${this.resetClicked.bind(this)}
?hidden=${isFlame}
${ref(e => {this.resetButton = e as Buttons.Button.Button; })}>
</devtools-button>
<span ${ref(e => { this.selectedSizeText = e as HTMLElement; })}>
${this.#selectedSize !== null ?
i18nString(UIStrings.selectedSizeS, {PH1: i18n.ByteUtilities.bytesToString(this.#selectedSize)})
: nothing}
</span>`;
// clang-format on
}
onIdsRangeChanged(event: Common.EventTarget.EventTargetEvent<IdsRangeChangedEvent>): void {
const {minId, maxId} = event.data;
this.#selectedSize = event.data.size;
this.#minId = minId;
this.#maxId = maxId;
this.performUpdate();
}
setSelectionRange(minId: number, maxId: number): void {
const profileData = convertToSamplingHeapProfile((this.profileHeader));
const profile = new SamplingHeapProfileModel(profileData, minId, maxId);
this.adjustedTotal = profile.total;
this.setProfile(profile);
}
onStatsUpdate(event: Common.EventTarget.EventTargetEvent<Protocol.HeapProfiler.SamplingHeapProfile|null>): void {
const profile = event.data;
if (!this.totalTime) {
this.timestamps = [];
this.sizes = [];
this.max = [];
this.ordinals = [];
this.totalTime = 30000;
this.lastOrdinal = 0;
}
this.sizes.fill(0);
this.sizes.push(0);
this.timestamps.push(Date.now());
this.ordinals.push(this.lastOrdinal + 1);
for (const sample of profile?.samples ?? []) {
this.lastOrdinal = Math.max(this.lastOrdinal, sample.ordinal);
const bucket = Platform.ArrayUtilities.upperBound(this.ordinals, sample.ordinal,
Platform.ArrayUtilities.DEFAULT_COMPARATOR) -
1;
this.sizes[bucket] += sample.size;
}
this.max.push(this.sizes[this.sizes.length - 1]);
const lastTimestamp = this.timestamps[this.timestamps.length - 1];
if (lastTimestamp - this.timestamps[0] > this.totalTime) {
this.totalTime *= 2;
}
this.performUpdate();
}
columnHeader(columnId: string): Common.UIString.LocalizedString {
switch (columnId) {
case 'self':
return i18nString(UIStrings.selfSizeBytes);
case 'total':
return i18nString(UIStrings.totalSizeBytes);
}
return Common.UIString.LocalizedEmptyString;
}
createFlameChartDataProvider(): ProfileFlameChartDataProvider {
return new HeapFlameChartDataProvider((this.profile() as SamplingHeapProfileModel),
this.profileHeader.heapProfilerModel());
}
static buildPopoverTable(popoverInfo: Array<{
title: string,
value: string,
}>): Element {
const table = document.createElement('table');
for (const entry of popoverInfo) {
const row = table.createChild('tr');
row.createChild('td').textContent = entry.title;
row.createChild('td').textContent = entry.value;
}
return table;
}
setProfile(profile: CPUProfile.ProfileTreeModel.ProfileTreeModel): void {
this.profileInternal = profile;
this.bottomUpProfileDataGridTree = null;
this.topDownProfileDataGridTree = null;
this.changeView();
this.refresh();
}
profile(): CPUProfile.ProfileTreeModel.ProfileTreeModel|null {
return this.profileInternal;
}
initialize(nodeFormatter: Formatter): void {
this.nodeFormatter = nodeFormatter;
this.viewType = Common.Settings.Settings.instance().createSetting('profile-view', ViewTypes.HEAVY);
this.changeView();
if (this.flameChart) {
this.flameChart.update();
}
}
override focus(): void {
if (this.flameChart) {
this.flameChart.focus();
} else {
super.focus();
}
}
selectRange(timeLeft: number, timeRight: number): void {
if (!this.flameChart) {
return;
}
this.flameChart.selectRange(timeLeft, timeRight);
}
getBottomUpProfileDataGridTree(): ProfileDataGridTree {
if (!this.bottomUpProfileDataGridTree) {
this.bottomUpProfileDataGridTree = new BottomUpProfileDataGridTree(
this.nodeFormatter, this.searchableViewInternal,
(this.profileInternal as CPUProfile.ProfileTreeModel.ProfileTreeModel).root, this.adjustedTotal);
}
return this.bottomUpProfileDataGridTree;
}
getTopDownProfileDataGridTree(): ProfileDataGridTree {
if (!this.topDownProfileDataGridTree) {
this.topDownProfileDataGridTree = new TopDownProfileDataGridTree(
this.nodeFormatter, this.searchableViewInternal,
(this.profileInternal as CPUProfile.ProfileTreeModel.ProfileTreeModel).root, this.adjustedTotal);
}
return this.topDownProfileDataGridTree;
}
populateContextMenu(contextMenu: UI.ContextMenu.ContextMenu,
gridNode: DataGrid.DataGrid.DataGridNode<unknown>): void {
const node = (gridNode as ProfileDataGridNode);
if (node.linkElement) {
contextMenu.appendApplicableItems(node.linkElement);
}
}
override willHide(): void {
super.willHide();
this.currentSearchResultIndex = -1;
}
refresh(): void {
if (!this.profileDataGridTree) {
return;
}
this.#renderDataGrid();
}
refreshVisibleData(): void {
this.#renderDataGrid();
}
searchableView(): UI.SearchableView.SearchableView {
return this.searchableViewInternal;
}
supportsCaseSensitiveSearch(): boolean {
return true;
}
supportsWholeWordSearch(): boolean {
return false;
}
supportsRegexSearch(): boolean {
return false;
}
onSearchCanceled(): void {
if (this.searchableElement) {
this.searchableElement.onSearchCanceled();
this.refresh();
}
}
performSearch(searchConfig: UI.SearchableView.SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void {
if (this.searchableElement) {
this.searchableElement.performSearch(searchConfig, shouldJump, jumpBackwards);
this.refresh();
}
}
jumpToNextSearchResult(): void {
if (this.searchableElement) {
this.searchableElement.jumpToNextSearchResult();
this.#syncSearchSelection();
}
}
jumpToPreviousSearchResult(): void {
if (this.searchableElement) {
this.searchableElement.jumpToPreviousSearchResult();
this.#syncSearchSelection();
}
}
#syncSearchSelection(): void {
if (this.searchableElement === this.profileDataGridTree && this.profileDataGridTree) {
const searchResult = this.profileDataGridTree.searchResults[this.profileDataGridTree.searchResultIndex];
this.#selectedNode = searchResult?.profileNode || null;
let node = this.#selectedNode;
while (node?.parent) {
node.parent.expanded = true;
node = node.parent as ProfileDataGridNode;
}
this.nodeSelected(!!this.#selectedNode);
this.refresh();
}
}
linkifier(): Components.Linkifier.Linkifier {
return this.linkifierInternal;
}
ensureFlameChartCreated(): void {
if (this.flameChart) {
return;
}
this.dataProvider = this.createFlameChartDataProvider();
this.flameChart = new ProfileFlameChart(this.searchableViewInternal, this.dataProvider);
this.flameChart.addEventListener(PerfUI.FlameChart.Events.ENTRY_INVOKED, event => {
void this.onEntryInvoked(event);
});
}
async onEntryInvoked(event: Common.EventTarget.EventTargetEvent<number>): Promise<void> {
if (!this.dataProvider) {
return;
}
const entryIndex = event.data;
const node = this.dataProvider.entryNodes[entryIndex];
const debuggerModel = this.profileHeader.debuggerModel;
if (!node || !node.scriptId || !debuggerModel) {
return;
}
const script = debuggerModel.scriptForId(node.scriptId);
if (!script) {
return;
}
const location = (debuggerModel.createRawLocation(script, node.lineNumber, node.columnNumber));
const uiLocation =
await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().rawLocationToUILocation(location);
void Common.Revealer.reveal(uiLocation);
}
changeView(e?: Event): void {
if (!this.profileInternal) {
return;
}
if (e) {
const select = e.target as HTMLSelectElement;
this.viewType.set(select.value as ViewTypes);
}
this.#selectedNode = null;
this.#isNodeSelected = false;
this.performUpdate();
}
nodeSelected(selected: boolean): void {
this.#isNodeSelected = selected;
this.performUpdate();
}
focusClicked(): void {
if (!this.#selectedNode) {
return;
}
this.#isResetEnabled = true;
this.performUpdate();
this.resetButton?.focus();
if (this.profileDataGridTree) {
this.profileDataGridTree.focus(this.#selectedNode);
}
this.refresh();
this.refreshVisibleData();
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CpuProfileNodeFocused);
}
excludeClicked(): void {
const selectedNode = this.#selectedNode;
if (!selectedNode) {
return;
}
this.#isResetEnabled = true;
this.performUpdate();
this.resetButton?.focus();
this.#selectedNode = null;
this.nodeSelected(false);
if (this.profileDataGridTree) {
this.profileDataGridTree.exclude(selectedNode);
}
this.refresh();
this.refreshVisibleData();
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CpuProfileNodeExcluded);
}
resetClicked(): void {
this.viewSelectComboBox?.focus();
this.#isResetEnabled = false;
this.#selectedNode = null;
this.#isNodeSelected = false;
this.performUpdate();
if (this.profileDataGridTree) {
this.profileDataGridTree.restore();
}
this.linkifierInternal.reset();
this.refresh();
this.refreshVisibleData();
}
sortProfile(): void {
if (!this.profileDataGridTree) {
return;
}
const sortAscending = this.#sortAscending;
const sortColumnId = this.#sortColumnId;
const sortProperty = sortColumnId === 'function' ? 'functionName' : sortColumnId || '';
this.profileDataGridTree.sort(ProfileDataGridTree.propertyComparator(sortProperty, sortAscending), false);
this.refresh();
}
override performUpdate(): void {
const currentViewType = this.viewType ? this.viewType.get() : null;
if (currentViewType && currentViewType !== this.#lastAppliedViewType) {
this.searchableViewInternal.closeSearch();
if (this.visibleView) {
this.visibleView.detach();
}
switch (currentViewType) {
case ViewTypes.FLAME:
this.ensureFlameChartCreated();
this.visibleView = this.flameChart;
this.searchableElement = this.flameChart;
break;
case ViewTypes.TREE:
this.profileDataGridTree = this.getTopDownProfileDataGridTree();
this.sortProfile();
this.visibleView = this.dataGrid;
this.searchableElement = this.profileDataGridTree;
break;
case ViewTypes.HEAVY:
this.profileDataGridTree = this.getBottomUpProfileDataGridTree();
this.sortProfile();
this.visibleView = this.dataGrid;
this.searchableElement = this.profileDataGridTree;
break;
}
if (this.visibleView) {
this.visibleView.show(this.searchableViewInternal.element);
}
this.#lastAppliedViewType = currentViewType;
}
const isFlame = currentViewType === ViewTypes.FLAME;
if (this.focusButton) {
this.focusButton.hidden = isFlame;
this.focusButton.disabled = !this.#isNodeSelected;
}
if (this.excludeButton) {
this.excludeButton.hidden = isFlame;
this.excludeButton.disabled = !this.#isNodeSelected;
}
if (this.resetButton) {
this.resetButton.hidden = isFlame;
this.resetButton.disabled = !this.#isResetEnabled;
}
if (this.#selectedSize !== null && this.selectedSizeText) {
this.selectedSizeText.textContent =
i18nString(UIStrings.selectedSizeS, {PH1: i18n.ByteUtilities.bytesToString(this.#selectedSize)});
}
if (this.#minId !== null && this.#maxId !== null) {
const rangeChanged = !this.#lastAppliedRange || this.#lastAppliedRange.minId !== this.#minId ||
this.#lastAppliedRange.maxId !== this.#maxId;
if (rangeChanged) {
this.setSelectionRange(this.#minId, this.#maxId);
this.#lastAppliedRange = {minId: this.#minId, maxId: this.#maxId};
}
}
if (this.sizes.length > 0) {
const samples = ({
sizes: this.sizes,
max: this.max,
ids: this.ordinals,
timestamps: this.timestamps,
totalTime: this.totalTime,
} as Samples);
this.timelineOverview.setSamples(samples);
}
}
}
export class SamplingHeapProfileTypeBase extends
Common.ObjectWrapper.eventMixin<SamplingHeapProfileType.EventTypes, typeof ProfileType>(ProfileType) {
recording: boolean;
clearedDuringRecording: boolean;
constructor(typeId: string, description: string) {
super(typeId, description);
this.recording = false;
this.clearedDuringRecording = false;
}
override profileBeingRecorded(): SamplingHeapProfileHeader|null {
return super.profileBeingRecorded() as SamplingHeapProfileHeader | null;
}
override typeName(): string {
return 'Heap';
}
override fileExtension(): string {
return '.heapprofile';
}
override get buttonTooltip(): Common.UIString.LocalizedString {
return this.recording ? i18nString(UIStrings.stopHeapProfiling) : i18nString(UIStrings.startHeapProfiling);
}
override buttonClicked(): boolean {
if (this.recording) {
void this.stopRecordingProfile();
} else {
void this.startRecordingProfile();
}
return this.recording;
}
async startRecordingProfile(): Promise<void> {
const heapProfilerModel = UI.Context.Context.instance().flavor(SDK.HeapProfilerModel.HeapProfilerModel);
if (this.profileBeingRecorded() || !heapProfilerModel) {
return;
}
const profileHeader = new SamplingHeapProfileHeader(heapProfilerModel, this);
this.setProfileBeingRecorded(profileHeader);
this.addProfile(profileHeader);
profileHeader.updateStatus(i18nString(UIStrings.recording));
const warnings = [i18nString(UIStrings.heapProfilerIsRecording)];
UI.InspectorView.InspectorView.instance().setPanelWarnings('heap-profiler', warnings);
this.recording = true;
this.startSampling();
}
async stopRecordingProfile(): Promise<void> {
this.recording = false;
const recordedProfile = this.profileBeingRecorded();
if (!recordedProfile?.heapProfilerModel()) {
return;
}
recordedProfile.updateStatus(i18nString(UIStrings.stopping));
const profile = await this.stopSampling();
if (recordedProfile) {
console.assert(profile !== undefined);
recordedProfile.setProtocolProfile(profile as unknown as Protocol.Profiler.Profile);
recordedProfile.updateStatus('');
this.setProfileBeingRecorded(null);
}
UI.InspectorView.InspectorView.instance().setPanelWarnings('heap-profiler', []);
// If the data was cleared during the middle of the recording we no
// longer treat the profile as being completed. This means we avoid
// a change of view to the profile list.
const wasClearedDuringRecording = this.clearedDuringRecording;
this.clearedDuringRecording = false;
if (wasClearedDuringRecording) {
return;
}
this.dispatchEventToListeners(ProfileEvents.PROFILE_COMPLETE, recordedProfile);
}
override createProfileLoadedFromFile(title: string): ProfileHeader {
return new SamplingHeapProfileHeader(null, this, title);
}
override profileBeingRecordedRemoved(): void {
this.clearedDuringRecording = true;
void this.stopRecordingProfile();
}
startSampling(): void {
throw new Error('Not implemented');
}
stopSampling(): Promise<Protocol.HeapProfiler.SamplingHeapProfile> {
throw new Error('Not implemented');
}
}
let samplingHeapProfileTypeInstance: SamplingHeapProfileType;
export class SamplingHeapProfileType extends SamplingHeapProfileTypeBase {
updateTimer: number;
updateIntervalMs: number;
readonly #recordTimelineSetting: Common.Settings.Setting<boolean>;
customContentInternal: UI.UIUtils.CheckboxLabel|null = null;
constructor() {
super(SamplingHeapProfileType.TypeId, i18nString(UIStrings.allocationSampling));
if (!samplingHeapProfileTypeInstance) {
samplingHeapProfileTypeInstance = this;
}
this.updateTimer = 0;
this.updateIntervalMs = 200;
this.#recordTimelineSetting =
Common.Settings.Settings.instance().createSetting('record-sampling-heap-profiler-timeline', false);
}
static get instance(): SamplingHeapProfileType {
return samplingHeapProfileTypeInstance;
}
override get treeItemTitle(): Common.UIString.LocalizedString {
return i18nString(UIStrings.samplingProfiles);
}
override get description(): string {
// TODO(l10n): Do not concatenate localized strings.
const formattedDescription = [i18nString(UIStrings.recordMemoryAllocations)];
return formattedDescription.join('\n');
}
override hasTemporaryView(): boolean {
return this.#recordTimelineSetting.get();
}
override customContent(): Element|null {
const checkboxSetting = SettingsUI.SettingsUI.createSettingCheckbox(
i18nString(UIStrings.samplingHeapProfilerTimeline), this.#recordTimelineSetting);
this.customContentInternal = checkboxSetting;
checkboxSetting.setAttribute(
'jslog', `${VisualLogging.toggle('record-sampling-heap-profiler-timeline').track({click: true})}`);
return checkboxSetting;
}
override setCustomContentEnabled(enable: boolean): void {
if (this.customContentInternal) {
this.customContentInternal.disabled = !enable;
}
}
override startSampling(): void {
const heapProfilerModel = this.obtainRecordingProfile();
if (!heapProfilerModel) {
return;
}
void heapProfilerModel.startSampling();
if (this.#recordTimelineSetting.get()) {
this.updateTimer = window.setTimeout(() => {
void this.updateStats();
}, this.updateIntervalMs);
}
}
obtainRecordingProfile(): SDK.HeapProfilerModel.HeapProfilerModel|null {
const recordingProfile = this.profileBeingRecorded();
if (recordingProfile) {
const heapProfilerModel = recordingProfile.heapProfilerModel();
return heapProfilerModel;
}
return null;
}
override async stopSampling(): Promise<Protocol.HeapProfiler.SamplingHeapProfile> {
window.clearTimeout(this.updateTimer);
this.updateTimer = 0;
this.dispatchEventToListeners(SamplingHeapProfileType.Events.RECORDING_STOPPED);
const heapProfilerModel = this.obtainRecordingProfile();
if (!heapProfilerModel) {
throw new Error('No heap profiler model');
}
const samplingProfile = await heapProfilerModel.stopSampling();
if (!samplingProfile) {
throw new Error('No sampling profile found');
}
return samplingProfile;
}
async updateStats(): Promise<void> {
const heapProfilerModel = this.obtainRecordingProfile();
if (!heapProfilerModel) {
return;
}
const profile = await heapProfilerModel.getSamplingProfile();
if (!this.updateTimer) {
return;
}
this.dispatchEventToListeners(SamplingHeapProfileType.Events.STATS_UPDATE, profile);
this.updateTimer = window.setTimeout(() => {
void this.updateStats();
}, this.updateIntervalMs);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
static readonly TypeId = 'SamplingHeap';
}
export namespace SamplingHeapProfileType {
export const enum Events {
RECORDING_STOPPED = 'RecordingStopped',
STATS_UPDATE = 'StatsUpdate',
}
export interface EventTypes {
[Events.RECORDING_STOPPED]: void;
[Events.STATS_UPDATE]: Protocol.HeapProfiler.SamplingHeapProfile|null;
}
}
export class SamplingHeapProfileHeader extends WritableProfileHeader {
readonly heapProfilerModelInternal: SDK.HeapProfilerModel.HeapProfilerModel|null;
override protocolProfileInternal: {
head: {
callFrame: {
functionName: string,
scriptId: Protocol.Runtime.ScriptId,
url: string,
lineNumber: number,
columnNumber: number,
},
children: never[],
selfSize: number,
id: number,
},
samples: never[],
startTime: number,
endTime: number,
nodes: never[],
};
constructor(
heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null,
type: SamplingHeapProfileTypeBase,
title?: string,
) {
super(
heapProfilerModel?.debuggerModel() ?? null,
type,
title || i18nString(UIStrings.profileD, {PH1: type.nextProfileUid()}),
);
this.heapProfilerModelInternal = heapProfilerModel;
this.protocolProfileInternal = {
head: {
callFrame: {
functionName: '',
scriptId: '' as Protocol.Runtime.ScriptId,
url: '',
lineNumber: 0,
columnNumber: 0,
},
children: [],
selfSize: 0,
id: 0,
},
samples: [],
startTime: 0,
endTime: 0,
nodes: [],
};
}
protocolProfile(): Protocol.HeapProfiler.SamplingHeapProfile {
return this.protocolProfileInternal;
}
heapProfilerModel(): SDK.HeapProfilerModel.HeapProfilerModel|null {
return this.heapProfilerModelInternal;
}
override profileType(): SamplingHeapProfileTypeBase {
return super.profileType() as SamplingHeapProfileTypeBase;
}
}
export class SamplingHeapProfileNode extends CPUProfile.ProfileTreeModel.ProfileNode {
override self: number;
constructor(node: Protocol.HeapProfiler.SamplingHeapProfileNode) {
const callFrame = node.callFrame || ({
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// @ts-expect-error
functionName: node['functionName'],
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// @ts-expect-error
scriptId: node['scriptId'],
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// @ts-expect-error
url: node['url'],
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// @ts-expect-error
lineNumber: node['lineNumber'] - 1,
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// @ts-expect-error
columnNumber: node['columnNumber'] - 1,
} as Protocol.Runtime.CallFrame);
super(callFrame);
this.self = node.selfSize;
}
}
export class SamplingHeapProfileModel extends CPUProfile.ProfileTreeModel.ProfileTreeModel {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
modules: any;
constructor(profile: Protocol.HeapProfiler.SamplingHeapProfile, minOrdinal?: number, maxOrdinal?: number) {
super();
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.modules = (profile as any).modules || [];
let nodeIdToSizeMap: Map<number, number>|null = null;
if (minOrdinal || maxOrdinal) {
nodeIdToSizeMap = new Map<number, number>();
minOrdinal = minOrdinal || 0;
maxOrdinal = maxOrdinal || Infinity;
for (const sample of profile.samples) {
if (sample.ordinal < minOrdinal || sample.ordinal > maxOrdinal) {
continue;
}
const size = nodeIdToSizeMap.get(sample.nodeId) || 0;
nodeIdToSizeMap.set(sample.nodeId, size + sample.size);
}
}
this.initialize(translateProfileTree(profile.head));
function translateProfileTree(root: Protocol.HeapProfiler.SamplingHeapProfileNode): SamplingHeapProfileNode {
const resultRoot = new SamplingHeapProfileNode(root);
const sourceNodeStack = [root];
const targetNodeStack = [resultRoot];
while (sourceNodeStack.length) {
const sourceNode = (sourceNodeStack.pop() as Protocol.HeapProfiler.SamplingHeapProfileNode);
const targetNode = (targetNodeStack.pop() as SamplingHeapProfileNode);
targetNode.children = sourceNode.children.map(child => {
const targetChild = new SamplingHeapProfileNode(child);
if (nodeIdToSizeMap) {
targetChild.self = nodeIdToSizeMap.get(child.id) || 0;
}
return targetChild;
});
sourceNodeStack.push(...sourceNode.children);
targetNodeStack.push(...targetNode.children);
}
pruneEmptyBranches(resultRoot);
return resultRoot;
}
function pruneEmptyBranches(node: CPUProfile.ProfileTreeModel.ProfileNode): boolean {
node.children = node.children.filter(pruneEmptyBranches);
return Boolean(node.children.length || node.self);
}
}
}
export class NodeFormatter implements Formatter {
readonly profileView: HeapProfileView;
readonly #formattedValueCache = new Map<number, string>();
readonly #formattedValueAccessibleTextCache = new Map<number, string>();
readonly #formattedPercentCache = new Map<number, string>();
constructor(profileView: HeapProfileView) {
this.profileView = profileView;
}
formatValue(value: number): string {
let result = this.#formattedValueCache.get(value);
if (!result) {
result = i18n.ByteUtilities.bytesToString(value);
this.#formattedValueCache.set(value, result);
}
return result;
}
formatValueAccessibleText(value: number): string {
let result = this.#formattedValueAccessibleTextCache.get(value);
if (!result) {
result = i18nString(UIStrings.sBytes, {PH1: value});
this.#formattedValueAccessibleTextCache.set(value, result);
}
return result;
}
formatPercent(value: number, _node: ProfileDataGridNode): string {
let result = this.#formattedPercentCache.get(value);
if (!result) {
result = i18nString(UIStrings.formatPercent, {PH1: value.toFixed(2)});
this.#formattedPercentCache.set(value, result);
}
return result;
}
linkifyNode(node: ProfileDataGridNode): Element|null {
const heapProfilerModel = this.profileView.profileHeader.heapProfilerModel();
const target = heapProfilerModel ? heapProfilerModel.target() : null;
const options = {
className: 'profile-node-file',
};
return this.profileView.linkifier().maybeLinkifyConsoleCallFrame(target, node.profileNode.callFrame, options);
}
}
export class HeapFlameChartDataProvider extends ProfileFlameChartDataProvider {
readonly profile: CPUProfile.ProfileTreeModel.ProfileTreeModel;
readonly heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null;
constructor(profile: CPUProfile.ProfileTreeModel.ProfileTreeModel,
heapProfilerModel: SDK.HeapProfilerModel.HeapProfilerModel|null) {
super();
this.profile = profile;
this.heapProfilerModel = heapProfilerModel;
}
override minimumBoundary(): number {
return 0;
}
override totalTime(): number {
return this.profile.root.total;
}
override entryHasDeoptReason(_entryIndex: number): boolean {
return false;
}
override formatValue(value: number, _precision?: number): string {
return i18nString(UIStrings.skb, {PH1: Platform.NumberUtilities.withThousandsSeparator(value / 1e3)});
}
override calculateTimelineData(): PerfUI.FlameChart.FlameChartTimelineData {
function nodesCount(node: CPUProfile.ProfileTreeModel.ProfileNode): number {
return node.children.reduce((count, node) => count + nodesCount(node), 1);
}
const count = nodesCount(this.profile.root);
const entryNodes: CPUProfile.ProfileTreeModel.ProfileNode[] = new Array(count);
const entryLevels = new Uint16Array(count);
const entryTotalTimes = new Float32Array(count);
const entryStartTimes = new Float64Array(count);
let depth = 0;
let maxDepth = 0;
let position = 0;
let index = 0;
function addNode(node: CPUProfile.ProfileTreeModel.ProfileNode): void {
const start = position;
entryNodes[index] = node;
entryLevels[index] = depth;
entryTotalTimes[index] = node.total;
entryStartTimes[index] = position;
++index;
++depth;
node.children.forEach(addNode);
--depth;
maxDepth = Math.max(maxDepth, depth);
position = start + node.total;
}
addNode(this.profile.root);
this.maxStackDepthInternal = maxDepth + 1;
this.entryNodes = entryNodes;
this.timelineDataInternal =
PerfUI.FlameChart.FlameChartTimelineData.create({entryLevels, entryTotalTimes, entryStartTimes, groups: null});
return this.timelineDataInternal;
}
override preparePopoverElement(entryIndex: number): Element|null {
const node = this.entryNodes[entryIndex];
if (!node) {
return null;
}
const popoverInfo: Array<{
title: string,
value: string,
}> = [];
function pushRow(title: string, value: string): void {
popoverInfo.push({title, value});
}
pushRow(i18nString(UIStrings.name), UI.UIUtils.beautifyFunctionName(node.functionName));
pushRow(i18nString(UIStrings.selfSize), i18n.ByteUtilities.bytesToString(node.self));
pushRow(i18nString(UIStrings.totalSize), i18n.ByteUtilities.bytesToString(node.total));
const linkifier = new Components.Linkifier.Linkifier();
const link = linkifier.maybeLinkifyConsoleCallFrame(this.heapProfilerModel ? this.heapProfilerModel.target() : null,
node.callFrame);
if (link) {
pushRow(i18nString(UIStrings.url), link.textContent);
}
linkifier.dispose();
return HeapProfileView.buildPopoverTable(popoverInfo);
}
}