blob: c0a3288b31db9ec02b41a030bd380bb6f6cb0008 [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @implements {Profiler.ProfileType.DataDisplayDelegate}
* @implements {UI.Searchable}
* @unrestricted
*/
Profiler.HeapSnapshotView = class extends UI.SimpleView {
/**
* @param {!Profiler.ProfileType.DataDisplayDelegate} dataDisplayDelegate
* @param {!Profiler.HeapProfileHeader} profile
*/
constructor(dataDisplayDelegate, profile) {
super(Common.UIString('Heap Snapshot'));
this.element.classList.add('heap-snapshot-view');
this._profile = profile;
this._linkifier = new Components.Linkifier();
const profileType = profile.profileType();
profileType.addEventListener(Profiler.HeapSnapshotProfileType.SnapshotReceived, this._onReceiveSnapshot, this);
profileType.addEventListener(Profiler.ProfileType.Events.RemoveProfileHeader, this._onProfileHeaderRemoved, this);
const isHeapTimeline = profileType.id === Profiler.TrackingHeapSnapshotProfileType.TypeId;
if (isHeapTimeline) {
this._createOverview();
}
this._parentDataDisplayDelegate = dataDisplayDelegate;
this._searchableView = new UI.SearchableView(this);
this._searchableView.show(this.element);
this._splitWidget = new UI.SplitWidget(false, true, 'heapSnapshotSplitViewState', 200, 200);
this._splitWidget.show(this._searchableView.element);
const heapProfilerModel = profile.heapProfilerModel();
this._containmentDataGrid = new Profiler.HeapSnapshotContainmentDataGrid(heapProfilerModel, this);
this._containmentDataGrid.addEventListener(DataGrid.DataGrid.Events.SelectedNode, this._selectionChanged, this);
this._containmentWidget = this._containmentDataGrid.asWidget();
this._containmentWidget.setMinimumSize(50, 25);
this._statisticsView = new Profiler.HeapSnapshotStatisticsView();
this._constructorsDataGrid = new Profiler.HeapSnapshotConstructorsDataGrid(heapProfilerModel, this);
this._constructorsDataGrid.addEventListener(DataGrid.DataGrid.Events.SelectedNode, this._selectionChanged, this);
this._constructorsWidget = this._constructorsDataGrid.asWidget();
this._constructorsWidget.setMinimumSize(50, 25);
this._diffDataGrid = new Profiler.HeapSnapshotDiffDataGrid(heapProfilerModel, this);
this._diffDataGrid.addEventListener(DataGrid.DataGrid.Events.SelectedNode, this._selectionChanged, this);
this._diffWidget = this._diffDataGrid.asWidget();
this._diffWidget.setMinimumSize(50, 25);
if (isHeapTimeline) {
this._allocationDataGrid = new Profiler.AllocationDataGrid(heapProfilerModel, this);
this._allocationDataGrid.addEventListener(
DataGrid.DataGrid.Events.SelectedNode, this._onSelectAllocationNode, this);
this._allocationWidget = this._allocationDataGrid.asWidget();
this._allocationWidget.setMinimumSize(50, 25);
this._allocationStackView = new Profiler.HeapAllocationStackView(heapProfilerModel);
this._allocationStackView.setMinimumSize(50, 25);
this._tabbedPane = new UI.TabbedPane();
}
this._retainmentDataGrid = new Profiler.HeapSnapshotRetainmentDataGrid(heapProfilerModel, this);
this._retainmentWidget = this._retainmentDataGrid.asWidget();
this._retainmentWidget.setMinimumSize(50, 21);
this._retainmentWidget.element.classList.add('retaining-paths-view');
let splitWidgetResizer;
if (this._allocationStackView) {
this._tabbedPane = new UI.TabbedPane();
this._tabbedPane.appendTab('retainers', Common.UIString('Retainers'), this._retainmentWidget);
this._tabbedPane.appendTab('allocation-stack', Common.UIString('Allocation stack'), this._allocationStackView);
splitWidgetResizer = this._tabbedPane.headerElement();
this._objectDetailsView = this._tabbedPane;
} else {
const retainmentViewHeader = createElementWithClass('div', 'heap-snapshot-view-resizer');
const retainingPathsTitleDiv = retainmentViewHeader.createChild('div', 'title');
const retainingPathsTitle = retainingPathsTitleDiv.createChild('span');
retainingPathsTitle.textContent = Common.UIString('Retainers');
splitWidgetResizer = retainmentViewHeader;
this._objectDetailsView = new UI.VBox();
this._objectDetailsView.element.appendChild(retainmentViewHeader);
this._retainmentWidget.show(this._objectDetailsView.element);
}
this._splitWidget.hideDefaultResizer();
this._splitWidget.installResizer(splitWidgetResizer);
this._retainmentDataGrid.addEventListener(
DataGrid.DataGrid.Events.SelectedNode, this._inspectedObjectChanged, this);
this._retainmentDataGrid.reset();
this._perspectives = [];
this._comparisonPerspective = new Profiler.HeapSnapshotView.ComparisonPerspective();
this._perspectives.push(new Profiler.HeapSnapshotView.SummaryPerspective());
if (profile.profileType() !== Profiler.ProfileTypeRegistry.instance.trackingHeapSnapshotProfileType) {
this._perspectives.push(this._comparisonPerspective);
}
this._perspectives.push(new Profiler.HeapSnapshotView.ContainmentPerspective());
if (this._allocationWidget) {
this._perspectives.push(new Profiler.HeapSnapshotView.AllocationPerspective());
}
this._perspectives.push(new Profiler.HeapSnapshotView.StatisticsPerspective());
this._perspectiveSelect = new UI.ToolbarComboBox(this._onSelectedPerspectiveChanged.bind(this), ls`Perspective`);
this._updatePerspectiveOptions();
this._baseSelect = new UI.ToolbarComboBox(this._changeBase.bind(this), ls`Base snapshot`);
this._baseSelect.setVisible(false);
this._updateBaseOptions();
this._filterSelect = new UI.ToolbarComboBox(this._changeFilter.bind(this), ls`Filter`);
this._filterSelect.setVisible(false);
this._updateFilterOptions();
this._classNameFilter = new UI.ToolbarInput(ls`Class filter`);
this._classNameFilter.setVisible(false);
this._constructorsDataGrid.setNameFilter(this._classNameFilter);
this._diffDataGrid.setNameFilter(this._classNameFilter);
this._selectedSizeText = new UI.ToolbarText();
this._popoverHelper = new UI.PopoverHelper(this.element, this._getPopoverRequest.bind(this));
this._popoverHelper.setDisableOnClick(true);
this._popoverHelper.setHasPadding(true);
this.element.addEventListener('scroll', this._popoverHelper.hidePopover.bind(this._popoverHelper), true);
this._currentPerspectiveIndex = 0;
this._currentPerspective = this._perspectives[0];
this._currentPerspective.activate(this);
this._dataGrid = this._currentPerspective.masterGrid(this);
this._populate();
this._searchThrottler = new Common.Throttler(0);
for (const existingProfile of this._profiles()) {
existingProfile.addEventListener(Profiler.ProfileHeader.Events.ProfileTitleChanged, this._updateControls, this);
}
}
_createOverview() {
const profileType = this._profile.profileType();
this._trackingOverviewGrid = new Profiler.HeapTimelineOverview();
this._trackingOverviewGrid.addEventListener(
Profiler.HeapTimelineOverview.IdsRangeChanged, this._onIdsRangeChanged.bind(this));
if (!this._profile.fromFile() && profileType.profileBeingRecorded() === this._profile) {
profileType.addEventListener(
Profiler.TrackingHeapSnapshotProfileType.HeapStatsUpdate, this._onHeapStatsUpdate, this);
profileType.addEventListener(
Profiler.TrackingHeapSnapshotProfileType.TrackingStopped, this._onStopTracking, this);
this._trackingOverviewGrid.start();
}
}
_onStopTracking() {
this._profile.profileType().removeEventListener(
Profiler.TrackingHeapSnapshotProfileType.HeapStatsUpdate, this._onHeapStatsUpdate, this);
this._profile.profileType().removeEventListener(
Profiler.TrackingHeapSnapshotProfileType.TrackingStopped, this._onStopTracking, this);
if (this._trackingOverviewGrid) {
this._trackingOverviewGrid.stop();
}
}
/**
* @param {!Common.Event} event
*/
_onHeapStatsUpdate(event) {
const samples = event.data;
if (samples) {
this._trackingOverviewGrid.setSamples(event.data);
}
}
/**
* @return {!UI.SearchableView}
*/
searchableView() {
return this._searchableView;
}
/**
* @override
* @param {?Profiler.ProfileHeader} profile
* @return {?UI.Widget}
*/
showProfile(profile) {
return this._parentDataDisplayDelegate.showProfile(profile);
}
/**
* @override
* @param {!Protocol.HeapProfiler.HeapSnapshotObjectId} snapshotObjectId
* @param {string} perspectiveName
*/
showObject(snapshotObjectId, perspectiveName) {
if (snapshotObjectId <= this._profile.maxJSObjectId) {
this.selectLiveObject(perspectiveName, snapshotObjectId);
} else {
this._parentDataDisplayDelegate.showObject(snapshotObjectId, perspectiveName);
}
}
/**
* @override
* @param {number} nodeIndex
* @return {!Promise<?Element>}
*/
async linkifyObject(nodeIndex) {
const heapProfilerModel = this._profile.heapProfilerModel();
// heapProfilerModel is null if snapshot was loaded from file
if (!heapProfilerModel) {
return null;
}
const location = await this._profile.getLocation(nodeIndex);
if (!location) {
return null;
}
const debuggerModel = heapProfilerModel.runtimeModel().debuggerModel();
const rawLocation = debuggerModel.createRawLocationByScriptId(
String(location.scriptId), location.lineNumber, location.columnNumber);
if (!rawLocation) {
return null;
}
const sourceURL = rawLocation.script() && rawLocation.script().sourceURL;
return sourceURL && this._linkifier ? this._linkifier.linkifyRawLocation(rawLocation, sourceURL) : null;
}
async _populate() {
const heapSnapshotProxy = await this._profile._loadPromise;
this._retrieveStatistics(heapSnapshotProxy);
this._dataGrid.setDataSource(heapSnapshotProxy);
if (this._profile.profileType().id === Profiler.TrackingHeapSnapshotProfileType.TypeId &&
this._profile.fromFile()) {
const samples = await heapSnapshotProxy.getSamples();
if (samples) {
console.assert(samples.timestamps.length);
const profileSamples = new Profiler.HeapTimelineOverview.Samples();
profileSamples.sizes = samples.sizes;
profileSamples.ids = samples.lastAssignedIds;
profileSamples.timestamps = samples.timestamps;
profileSamples.max = samples.sizes;
profileSamples.totalTime = Math.max(samples.timestamps.peekLast(), 10000);
this._trackingOverviewGrid.setSamples(profileSamples);
}
}
const list = this._profiles();
const profileIndex = list.indexOf(this._profile);
this._baseSelect.setSelectedIndex(Math.max(0, profileIndex - 1));
if (this._trackingOverviewGrid) {
this._trackingOverviewGrid.updateGrid();
}
}
/**
* @param {!Profiler.HeapSnapshotProxy} heapSnapshotProxy
* @return {!Promise<!HeapSnapshotModel.Statistics>}
*/
async _retrieveStatistics(heapSnapshotProxy) {
const statistics = await heapSnapshotProxy.getStatistics();
this._statisticsView.setTotal(statistics.total);
this._statisticsView.addRecord(statistics.code, Common.UIString('Code'), '#f77');
this._statisticsView.addRecord(statistics.strings, Common.UIString('Strings'), '#5e5');
this._statisticsView.addRecord(statistics.jsArrays, Common.UIString('JS Arrays'), '#7af');
this._statisticsView.addRecord(statistics.native, Common.UIString('Typed Arrays'), '#fc5');
this._statisticsView.addRecord(statistics.system, Common.UIString('System Objects'), '#98f');
return statistics;
}
/**
* @param {!Common.Event} event
*/
_onIdsRangeChanged(event) {
const minId = event.data.minId;
const maxId = event.data.maxId;
this._selectedSizeText.setText(Common.UIString('Selected size: %s', Number.bytesToString(event.data.size)));
if (this._constructorsDataGrid.snapshot) {
this._constructorsDataGrid.setSelectionRange(minId, maxId);
}
}
/**
* @override
* @return {!Array<!UI.ToolbarItem>}
*/
syncToolbarItems() {
const result = [this._perspectiveSelect, this._classNameFilter];
if (this._profile.profileType() !== Profiler.ProfileTypeRegistry.instance.trackingHeapSnapshotProfileType) {
result.push(this._baseSelect, this._filterSelect);
}
result.push(this._selectedSizeText);
return result;
}
/**
* @override
*/
willHide() {
this._currentSearchResultIndex = -1;
this._popoverHelper.hidePopover();
}
/**
* @override
* @return {boolean}
*/
supportsCaseSensitiveSearch() {
return true;
}
/**
* @override
* @return {boolean}
*/
supportsRegexSearch() {
return false;
}
/**
* @override
*/
searchCanceled() {
this._currentSearchResultIndex = -1;
this._searchResults = [];
}
/**
* @param {?Profiler.HeapSnapshotGridNode} node
*/
_selectRevealedNode(node) {
if (node) {
node.select();
}
}
/**
* @override
* @param {!UI.SearchableView.SearchConfig} searchConfig
* @param {boolean} shouldJump
* @param {boolean=} jumpBackwards
*/
performSearch(searchConfig, shouldJump, jumpBackwards) {
const nextQuery = new HeapSnapshotModel.SearchConfig(
searchConfig.query.trim(), searchConfig.caseSensitive, searchConfig.isRegex, shouldJump,
jumpBackwards || false);
this._searchThrottler.schedule(this._performSearch.bind(this, nextQuery));
}
/**
* @param {!HeapSnapshotModel.SearchConfig} nextQuery
* @return {!Promise}
*/
async _performSearch(nextQuery) {
// Call searchCanceled since it will reset everything we need before doing a new search.
this.searchCanceled();
if (!this._currentPerspective.supportsSearch()) {
return;
}
this.currentQuery = nextQuery;
const query = nextQuery.query.trim();
if (!query) {
return;
}
if (query.charAt(0) === '@') {
const snapshotNodeId = parseInt(query.substring(1), 10);
if (isNaN(snapshotNodeId)) {
return;
}
const node = await this._dataGrid.revealObjectByHeapSnapshotId(String(snapshotNodeId));
this._selectRevealedNode(node);
return;
}
this._searchResults = await this._profile._snapshotProxy.search(this.currentQuery, this._dataGrid.nodeFilter());
this._searchableView.updateSearchMatchesCount(this._searchResults.length);
if (this._searchResults.length) {
this._currentSearchResultIndex = nextQuery.jumpBackwards ? this._searchResults.length - 1 : 0;
}
await this._jumpToSearchResult(this._currentSearchResultIndex);
}
/**
* @override
*/
jumpToNextSearchResult() {
if (!this._searchResults.length) {
return;
}
this._currentSearchResultIndex = (this._currentSearchResultIndex + 1) % this._searchResults.length;
this._searchThrottler.schedule(this._jumpToSearchResult.bind(this, this._currentSearchResultIndex));
}
/**
* @override
*/
jumpToPreviousSearchResult() {
if (!this._searchResults.length) {
return;
}
this._currentSearchResultIndex =
(this._currentSearchResultIndex + this._searchResults.length - 1) % this._searchResults.length;
this._searchThrottler.schedule(this._jumpToSearchResult.bind(this, this._currentSearchResultIndex));
}
/**
* @param {number} searchResultIndex
* @return {!Promise}
*/
async _jumpToSearchResult(searchResultIndex) {
this._searchableView.updateCurrentMatchIndex(searchResultIndex);
if (searchResultIndex === -1) {
return;
}
const node = await this._dataGrid.revealObjectByHeapSnapshotId(String(this._searchResults[searchResultIndex]));
this._selectRevealedNode(node);
}
refreshVisibleData() {
if (!this._dataGrid) {
return;
}
let child = this._dataGrid.rootNode().children[0];
while (child) {
child.refresh();
child = child.traverseNextNode(false, null, true);
}
}
_changeBase() {
if (this._baseProfile === this._profiles()[this._baseSelect.selectedIndex()]) {
return;
}
this._baseProfile = this._profiles()[this._baseSelect.selectedIndex()];
const dataGrid = /** @type {!Profiler.HeapSnapshotDiffDataGrid} */ (this._dataGrid);
// Change set base data source only if main data source is already set.
if (dataGrid.snapshot) {
this._baseProfile._loadPromise.then(dataGrid.setBaseDataSource.bind(dataGrid));
}
if (!this.currentQuery || !this._searchResults) {
return;
}
// The current search needs to be performed again. First negate out previous match
// count by calling the search finished callback with a negative number of matches.
// Then perform the search again with the same query and callback.
this.performSearch(this.currentQuery, false);
}
_changeFilter() {
const profileIndex = this._filterSelect.selectedIndex() - 1;
this._dataGrid.filterSelectIndexChanged(this._profiles(), profileIndex);
if (!this.currentQuery || !this._searchResults) {
return;
}
// The current search needs to be performed again. First negate out previous match
// count by calling the search finished callback with a negative number of matches.
// Then perform the search again with the same query and callback.
this.performSearch(this.currentQuery, false);
}
/**
* @return {!Array.<!Profiler.ProfileHeader>}
*/
_profiles() {
return this._profile.profileType().getProfiles();
}
/**
* @param {!Common.Event} event
*/
_selectionChanged(event) {
const selectedNode = /** @type {!Profiler.HeapSnapshotGridNode} */ (event.data);
this._setSelectedNodeForDetailsView(selectedNode);
this._inspectedObjectChanged(event);
}
/**
* @param {!Common.Event} event
*/
_onSelectAllocationNode(event) {
const selectedNode = /** @type {!DataGrid.DataGridNode} */ (event.data);
this._constructorsDataGrid.setAllocationNodeId(selectedNode.allocationNodeId());
this._setSelectedNodeForDetailsView(null);
}
/**
* @param {!Common.Event} event
*/
_inspectedObjectChanged(event) {
const selectedNode = /** @type {!DataGrid.DataGridNode} */ (event.data);
const heapProfilerModel = this._profile.heapProfilerModel();
if (heapProfilerModel && selectedNode instanceof Profiler.HeapSnapshotGenericObjectNode) {
heapProfilerModel.addInspectedHeapObject(String(selectedNode.snapshotNodeId));
}
}
/**
* @param {?Profiler.HeapSnapshotGridNode} nodeItem
*/
_setSelectedNodeForDetailsView(nodeItem) {
const dataSource = nodeItem && nodeItem.retainersDataSource();
if (dataSource) {
this._retainmentDataGrid.setDataSource(dataSource.snapshot, dataSource.snapshotNodeIndex);
if (this._allocationStackView) {
this._allocationStackView.setAllocatedObject(dataSource.snapshot, dataSource.snapshotNodeIndex);
}
} else {
if (this._allocationStackView) {
this._allocationStackView.clear();
}
this._retainmentDataGrid.reset();
}
}
/**
* @param {string} perspectiveTitle
* @return {!Promise}
*/
_changePerspectiveAndWait(perspectiveTitle) {
const perspectiveIndex = this._perspectives.findIndex(perspective => perspective.title() === perspectiveTitle);
if (perspectiveIndex === -1 || this._currentPerspectiveIndex === perspectiveIndex) {
return Promise.resolve();
}
const promise = this._perspectives[perspectiveIndex].masterGrid(this).once(
Profiler.HeapSnapshotSortableDataGrid.Events.ContentShown);
const option = this._perspectiveSelect.options().find(option => option.value === String(perspectiveIndex));
this._perspectiveSelect.select(/** @type {!Element} */ (option));
this._changePerspective(perspectiveIndex);
return promise;
}
async _updateDataSourceAndView() {
const dataGrid = this._dataGrid;
if (!dataGrid || dataGrid.snapshot) {
return;
}
const snapshotProxy = await this._profile._loadPromise;
if (this._dataGrid !== dataGrid) {
return;
}
if (dataGrid.snapshot !== snapshotProxy) {
dataGrid.setDataSource(snapshotProxy);
}
if (dataGrid !== this._diffDataGrid) {
return;
}
if (!this._baseProfile) {
this._baseProfile = this._profiles()[this._baseSelect.selectedIndex()];
}
const baseSnapshotProxy = await this._baseProfile._loadPromise;
if (this._diffDataGrid.baseSnapshot !== baseSnapshotProxy) {
this._diffDataGrid.setBaseDataSource(baseSnapshotProxy);
}
}
/**
* @param {!Event} event
*/
_onSelectedPerspectiveChanged(event) {
this._changePerspective(event.target.selectedOptions[0].value);
}
/**
* @param {number} selectedIndex
*/
_changePerspective(selectedIndex) {
if (selectedIndex === this._currentPerspectiveIndex) {
return;
}
this._currentPerspectiveIndex = selectedIndex;
this._currentPerspective.deactivate(this);
const perspective = this._perspectives[selectedIndex];
this._currentPerspective = perspective;
this._dataGrid = perspective.masterGrid(this);
perspective.activate(this);
this.refreshVisibleData();
if (this._dataGrid) {
this._dataGrid.updateWidths();
}
this._updateDataSourceAndView();
if (!this.currentQuery || !this._searchResults) {
return;
}
// The current search needs to be performed again. First negate out previous match
// count by calling the search finished callback with a negative number of matches.
// Then perform the search again the with same query and callback.
this.performSearch(this.currentQuery, false);
}
/**
* @param {string} perspectiveName
* @param {!Protocol.HeapProfiler.HeapSnapshotObjectId} snapshotObjectId
*/
async selectLiveObject(perspectiveName, snapshotObjectId) {
await this._changePerspectiveAndWait(perspectiveName);
const node = await this._dataGrid.revealObjectByHeapSnapshotId(snapshotObjectId);
if (node) {
node.select();
} else {
Common.console.error('Cannot find corresponding heap snapshot node');
}
}
/**
* @param {!Event} event
* @return {?UI.PopoverRequest}
*/
_getPopoverRequest(event) {
const span = event.target.enclosingNodeOrSelfWithNodeName('span');
const row = event.target.enclosingNodeOrSelfWithNodeName('tr');
const heapProfilerModel = this._profile.heapProfilerModel();
if (!row || !span || !heapProfilerModel) {
return null;
}
const node = row._dataGridNode;
let objectPopoverHelper;
return {
box: span.boxInWindow(),
show: async popover => {
const remoteObject = await node.queryObjectContent(heapProfilerModel, 'popover');
if (!remoteObject) {
return false;
}
objectPopoverHelper = await ObjectUI.ObjectPopoverHelper.buildObjectPopover(remoteObject, popover);
if (!objectPopoverHelper) {
heapProfilerModel.runtimeModel().releaseObjectGroup('popover');
return false;
}
return true;
},
hide: () => {
heapProfilerModel.runtimeModel().releaseObjectGroup('popover');
objectPopoverHelper.dispose();
}
};
}
_updatePerspectiveOptions() {
const multipleSnapshots = this._profiles().length > 1;
this._perspectiveSelect.removeOptions();
this._perspectives.forEach((perspective, index) => {
if (multipleSnapshots || perspective !== this._comparisonPerspective) {
this._perspectiveSelect.createOption(perspective.title(), String(index));
}
});
}
_updateBaseOptions() {
const list = this._profiles();
const selectedIndex = this._baseSelect.selectedIndex();
this._baseSelect.removeOptions();
for (const item of list) {
this._baseSelect.createOption(item.title);
}
if (selectedIndex > -1) {
this._baseSelect.setSelectedIndex(selectedIndex);
}
}
_updateFilterOptions() {
const list = this._profiles();
const selectedIndex = this._filterSelect.selectedIndex();
this._filterSelect.removeOptions();
this._filterSelect.createOption(Common.UIString('All objects'));
for (let i = 0; i < list.length; ++i) {
let title;
if (!i) {
title = Common.UIString('Objects allocated before %s', list[i].title);
} else {
title = Common.UIString('Objects allocated between %s and %s', list[i - 1].title, list[i].title);
}
this._filterSelect.createOption(title);
}
if (selectedIndex > -1) {
this._filterSelect.setSelectedIndex(selectedIndex);
}
}
_updateControls() {
this._updatePerspectiveOptions();
this._updateBaseOptions();
this._updateFilterOptions();
}
/**
* @param {!Common.Event} event
*/
_onReceiveSnapshot(event) {
this._updateControls();
const profile = event.data;
profile.addEventListener(Profiler.ProfileHeader.Events.ProfileTitleChanged, this._updateControls, this);
}
/**
* @param {!Common.Event} event
*/
_onProfileHeaderRemoved(event) {
const profile = event.data;
profile.removeEventListener(Profiler.ProfileHeader.Events.ProfileTitleChanged, this._updateControls, this);
if (this._profile === profile) {
this.detach();
this._profile.profileType().removeEventListener(
Profiler.HeapSnapshotProfileType.SnapshotReceived, this._onReceiveSnapshot, this);
this._profile.profileType().removeEventListener(
Profiler.ProfileType.Events.RemoveProfileHeader, this._onProfileHeaderRemoved, this);
this.dispose();
} else {
this._updateControls();
}
}
dispose() {
this._linkifier.dispose();
this._popoverHelper.dispose();
if (this._allocationStackView) {
this._allocationStackView.clear();
this._allocationDataGrid.dispose();
}
this._onStopTracking();
if (this._trackingOverviewGrid) {
this._trackingOverviewGrid.removeEventListener(
Profiler.HeapTimelineOverview.IdsRangeChanged, this._onIdsRangeChanged.bind(this));
}
}
};
/**
* @unrestricted
*/
Profiler.HeapSnapshotView.Perspective = class {
/**
* @param {string} title
*/
constructor(title) {
this._title = title;
}
/**
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
*/
activate(heapSnapshotView) {
}
/**
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
*/
deactivate(heapSnapshotView) {
heapSnapshotView._baseSelect.setVisible(false);
heapSnapshotView._filterSelect.setVisible(false);
heapSnapshotView._classNameFilter.setVisible(false);
if (heapSnapshotView._trackingOverviewGrid) {
heapSnapshotView._trackingOverviewGrid.detach();
}
if (heapSnapshotView._allocationWidget) {
heapSnapshotView._allocationWidget.detach();
}
if (heapSnapshotView._statisticsView) {
heapSnapshotView._statisticsView.detach();
}
heapSnapshotView._splitWidget.detach();
heapSnapshotView._splitWidget.detachChildWidgets();
}
/**
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
* @return {?DataGrid.DataGrid}
*/
masterGrid(heapSnapshotView) {
return null;
}
/**
* @return {string}
*/
title() {
return this._title;
}
/**
* @return {boolean}
*/
supportsSearch() {
return false;
}
};
/**
* @unrestricted
*/
Profiler.HeapSnapshotView.SummaryPerspective = class extends Profiler.HeapSnapshotView.Perspective {
constructor() {
super(Common.UIString('Summary'));
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
*/
activate(heapSnapshotView) {
heapSnapshotView._splitWidget.setMainWidget(heapSnapshotView._constructorsWidget);
heapSnapshotView._splitWidget.setSidebarWidget(heapSnapshotView._objectDetailsView);
heapSnapshotView._splitWidget.show(heapSnapshotView._searchableView.element);
heapSnapshotView._filterSelect.setVisible(true);
heapSnapshotView._classNameFilter.setVisible(true);
if (!heapSnapshotView._trackingOverviewGrid) {
return;
}
heapSnapshotView._trackingOverviewGrid.show(
heapSnapshotView._searchableView.element, heapSnapshotView._splitWidget.element);
heapSnapshotView._trackingOverviewGrid.update();
heapSnapshotView._trackingOverviewGrid.updateGrid();
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
* @return {?DataGrid.DataGrid}
*/
masterGrid(heapSnapshotView) {
return heapSnapshotView._constructorsDataGrid;
}
/**
* @override
* @return {boolean}
*/
supportsSearch() {
return true;
}
};
/**
* @unrestricted
*/
Profiler.HeapSnapshotView.ComparisonPerspective = class extends Profiler.HeapSnapshotView.Perspective {
constructor() {
super(Common.UIString('Comparison'));
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
*/
activate(heapSnapshotView) {
heapSnapshotView._splitWidget.setMainWidget(heapSnapshotView._diffWidget);
heapSnapshotView._splitWidget.setSidebarWidget(heapSnapshotView._objectDetailsView);
heapSnapshotView._splitWidget.show(heapSnapshotView._searchableView.element);
heapSnapshotView._baseSelect.setVisible(true);
heapSnapshotView._classNameFilter.setVisible(true);
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
* @return {?DataGrid.DataGrid}
*/
masterGrid(heapSnapshotView) {
return heapSnapshotView._diffDataGrid;
}
/**
* @override
* @return {boolean}
*/
supportsSearch() {
return true;
}
};
/**
* @unrestricted
*/
Profiler.HeapSnapshotView.ContainmentPerspective = class extends Profiler.HeapSnapshotView.Perspective {
constructor() {
super(Common.UIString('Containment'));
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
*/
activate(heapSnapshotView) {
heapSnapshotView._splitWidget.setMainWidget(heapSnapshotView._containmentWidget);
heapSnapshotView._splitWidget.setSidebarWidget(heapSnapshotView._objectDetailsView);
heapSnapshotView._splitWidget.show(heapSnapshotView._searchableView.element);
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
* @return {?DataGrid.DataGrid}
*/
masterGrid(heapSnapshotView) {
return heapSnapshotView._containmentDataGrid;
}
};
/**
* @unrestricted
*/
Profiler.HeapSnapshotView.AllocationPerspective = class extends Profiler.HeapSnapshotView.Perspective {
constructor() {
super(Common.UIString('Allocation'));
this._allocationSplitWidget = new UI.SplitWidget(false, true, 'heapSnapshotAllocationSplitViewState', 200, 200);
this._allocationSplitWidget.setSidebarWidget(new UI.VBox());
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
*/
activate(heapSnapshotView) {
this._allocationSplitWidget.setMainWidget(heapSnapshotView._allocationWidget);
heapSnapshotView._splitWidget.setMainWidget(heapSnapshotView._constructorsWidget);
heapSnapshotView._splitWidget.setSidebarWidget(heapSnapshotView._objectDetailsView);
const allocatedObjectsView = new UI.VBox();
const resizer = createElementWithClass('div', 'heap-snapshot-view-resizer');
const title = resizer.createChild('div', 'title').createChild('span');
title.textContent = Common.UIString('Live objects');
this._allocationSplitWidget.hideDefaultResizer();
this._allocationSplitWidget.installResizer(resizer);
allocatedObjectsView.element.appendChild(resizer);
heapSnapshotView._splitWidget.show(allocatedObjectsView.element);
this._allocationSplitWidget.setSidebarWidget(allocatedObjectsView);
this._allocationSplitWidget.show(heapSnapshotView._searchableView.element);
heapSnapshotView._constructorsDataGrid.clear();
const selectedNode = heapSnapshotView._allocationDataGrid.selectedNode;
if (selectedNode) {
heapSnapshotView._constructorsDataGrid.setAllocationNodeId(selectedNode.allocationNodeId());
}
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
*/
deactivate(heapSnapshotView) {
this._allocationSplitWidget.detach();
super.deactivate(heapSnapshotView);
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
* @return {?DataGrid.DataGrid}
*/
masterGrid(heapSnapshotView) {
return heapSnapshotView._allocationDataGrid;
}
};
/**
* @unrestricted
*/
Profiler.HeapSnapshotView.StatisticsPerspective = class extends Profiler.HeapSnapshotView.Perspective {
constructor() {
super(Common.UIString('Statistics'));
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
*/
activate(heapSnapshotView) {
heapSnapshotView._statisticsView.show(heapSnapshotView._searchableView.element);
}
/**
* @override
* @param {!Profiler.HeapSnapshotView} heapSnapshotView
* @return {?DataGrid.DataGrid}
*/
masterGrid(heapSnapshotView) {
return null;
}
};
/**
* @implements {SDK.SDKModelObserver<!SDK.HeapProfilerModel>}
* @unrestricted
*/
Profiler.HeapSnapshotProfileType = class extends Profiler.ProfileType {
/**
* @param {string=} id
* @param {string=} title
*/
constructor(id, title) {
super(id || Profiler.HeapSnapshotProfileType.TypeId, title || ls`Heap snapshot`);
SDK.targetManager.observeModels(SDK.HeapProfilerModel, this);
SDK.targetManager.addModelListener(
SDK.HeapProfilerModel, SDK.HeapProfilerModel.Events.ResetProfiles, this._resetProfiles, this);
SDK.targetManager.addModelListener(
SDK.HeapProfilerModel, SDK.HeapProfilerModel.Events.AddHeapSnapshotChunk, this._addHeapSnapshotChunk, this);
SDK.targetManager.addModelListener(
SDK.HeapProfilerModel, SDK.HeapProfilerModel.Events.ReportHeapSnapshotProgress,
this._reportHeapSnapshotProgress, this);
}
/**
* @override
* @param {!SDK.HeapProfilerModel} heapProfilerModel
*/
modelAdded(heapProfilerModel) {
heapProfilerModel.enable();
}
/**
* @override
* @param {!SDK.HeapProfilerModel} heapProfilerModel
*/
modelRemoved(heapProfilerModel) {
}
/**
* @override
* @return {!Array<!Profiler.HeapProfileHeader>}
*/
getProfiles() {
return /** @type {!Array<!Profiler.HeapProfileHeader>} */ (super.getProfiles());
}
/**
* @override
* @return {string}
*/
fileExtension() {
return '.heapsnapshot';
}
/**
* @override
*/
get buttonTooltip() {
return Common.UIString('Take heap snapshot');
}
/**
* @override
* @return {boolean}
*/
isInstantProfile() {
return true;
}
/**
* @override
* @return {boolean}
*/
buttonClicked() {
this._takeHeapSnapshot();
Host.userMetrics.actionTaken(Host.UserMetrics.Action.ProfilesHeapProfileTaken);
return false;
}
/**
* @override
*/
get treeItemTitle() {
return Common.UIString('HEAP SNAPSHOTS');
}
/**
* @override
*/
get description() {
return Common.UIString(
'Heap snapshot profiles show memory distribution among your page\'s JavaScript objects and related DOM nodes.');
}
/**
* @override
* @param {string} title
* @return {!Profiler.ProfileHeader}
*/
createProfileLoadedFromFile(title) {
return new Profiler.HeapProfileHeader(null, this, title);
}
async _takeHeapSnapshot() {
if (this.profileBeingRecorded()) {
return;
}
const heapProfilerModel = UI.context.flavor(SDK.HeapProfilerModel);
if (!heapProfilerModel) {
return;
}
let profile = new Profiler.HeapProfileHeader(heapProfilerModel, this);
this.setProfileBeingRecorded(profile);
this.addProfile(profile);
profile.updateStatus(Common.UIString('Snapshotting\u2026'));
await heapProfilerModel.takeHeapSnapshot(true);
// ------------ ASYNC ------------
profile = this.profileBeingRecorded();
profile.title = Common.UIString('Snapshot %d', profile.uid);
profile._finishLoad();
this.setProfileBeingRecorded(null);
this.dispatchEventToListeners(Profiler.ProfileType.Events.ProfileComplete, profile);
}
/**
* @param {!Common.Event} event
*/
_addHeapSnapshotChunk(event) {
if (!this.profileBeingRecorded()) {
return;
}
const chunk = /** @type {string} */ (event.data);
this.profileBeingRecorded().transferChunk(chunk);
}
/**
* @param {!Common.Event} event
*/
_reportHeapSnapshotProgress(event) {
const profile = this.profileBeingRecorded();
if (!profile) {
return;
}
const data = /** @type {{done: number, total: number, finished: boolean}} */ (event.data);
profile.updateStatus(Common.UIString('%.0f%%', (data.done / data.total) * 100), true);
if (data.finished) {
profile._prepareToLoad();
}
}
/**
* @param {!Common.Event} event
*/
_resetProfiles(event) {
const heapProfilerModel = /** @type {!SDK.HeapProfilerModel} */ (event.data);
for (const profile of this.getProfiles()) {
if (profile.heapProfilerModel() === heapProfilerModel) {
this.removeProfile(profile);
}
}
}
_snapshotReceived(profile) {
if (this.profileBeingRecorded() === profile) {
this.setProfileBeingRecorded(null);
}
this.dispatchEventToListeners(Profiler.HeapSnapshotProfileType.SnapshotReceived, profile);
}
};
Profiler.HeapSnapshotProfileType.TypeId = 'HEAP';
Profiler.HeapSnapshotProfileType.SnapshotReceived = 'SnapshotReceived';
/**
* @unrestricted
*/
Profiler.TrackingHeapSnapshotProfileType = class extends Profiler.HeapSnapshotProfileType {
constructor() {
super(Profiler.TrackingHeapSnapshotProfileType.TypeId, ls`Allocation instrumentation on timeline`);
this._recordAllocationStacksSetting = Common.settings.createSetting('recordAllocationStacks', false);
/** @type {?UI.CheckboxLabel} */
this._customContent = null;
}
/**
* @override
* @param {!SDK.HeapProfilerModel} heapProfilerModel
*/
modelAdded(heapProfilerModel) {
super.modelAdded(heapProfilerModel);
heapProfilerModel.addEventListener(SDK.HeapProfilerModel.Events.HeapStatsUpdate, this._heapStatsUpdate, this);
heapProfilerModel.addEventListener(SDK.HeapProfilerModel.Events.LastSeenObjectId, this._lastSeenObjectId, this);
}
/**
* @override
* @param {!SDK.HeapProfilerModel} heapProfilerModel
*/
modelRemoved(heapProfilerModel) {
super.modelRemoved(heapProfilerModel);
heapProfilerModel.removeEventListener(SDK.HeapProfilerModel.Events.HeapStatsUpdate, this._heapStatsUpdate, this);
heapProfilerModel.removeEventListener(SDK.HeapProfilerModel.Events.LastSeenObjectId, this._lastSeenObjectId, this);
}
/**
* @param {!Common.Event} event
*/
_heapStatsUpdate(event) {
if (!this._profileSamples) {
return;
}
const samples = /** @type {!Array.<number>} */ (event.data);
let index;
for (let i = 0; i < samples.length; i += 3) {
index = samples[i];
const size = samples[i + 2];
this._profileSamples.sizes[index] = size;
if (!this._profileSamples.max[index]) {
this._profileSamples.max[index] = size;
}
}
}
/**
* @param {!Common.Event} event
*/
_lastSeenObjectId(event) {
const profileSamples = this._profileSamples;
if (!profileSamples) {
return;
}
const data = /** @type {{lastSeenObjectId: number, timestamp: number}} */ (event.data);
const currentIndex = Math.max(profileSamples.ids.length, profileSamples.max.length - 1);
profileSamples.ids[currentIndex] = data.lastSeenObjectId;
if (!profileSamples.max[currentIndex]) {
profileSamples.max[currentIndex] = 0;
profileSamples.sizes[currentIndex] = 0;
}
profileSamples.timestamps[currentIndex] = data.timestamp;
if (profileSamples.totalTime < data.timestamp - profileSamples.timestamps[0]) {
profileSamples.totalTime *= 2;
}
this.dispatchEventToListeners(Profiler.TrackingHeapSnapshotProfileType.HeapStatsUpdate, this._profileSamples);
this.profileBeingRecorded().updateStatus(null, true);
}
/**
* @override
* @return {boolean}
*/
hasTemporaryView() {
return true;
}
/**
* @override
*/
get buttonTooltip() {
return this._recording ? ls`Stop recording heap profile` : ls`Start recording heap profile`;
}
/**
* @override
* @return {boolean}
*/
isInstantProfile() {
return false;
}
/**
* @override
* @return {boolean}
*/
buttonClicked() {
return this._toggleRecording();
}
_startRecordingProfile() {
if (this.profileBeingRecorded()) {
return;
}
const heapProfilerModel = this._addNewProfile();
if (!heapProfilerModel) {
return;
}
heapProfilerModel.startTrackingHeapObjects(this._recordAllocationStacksSetting.get());
}
/**
* @override
* @return {?Element}
*/
customContent() {
const checkboxSetting = UI.SettingsUI.createSettingCheckbox(
ls`Record allocation stacks (extra performance overhead)`, this._recordAllocationStacksSetting, true);
this._customContent = /** @type {!UI.CheckboxLabel} */ (checkboxSetting);
return checkboxSetting;
}
/**
* @override
* @param {boolean} enable
*/
setCustomContentEnabled(enable) {
this._customContent.checkboxElement.disabled = !enable;
}
/**
* @return {?SDK.HeapProfilerModel}
*/
_addNewProfile() {
const heapProfilerModel = UI.context.flavor(SDK.HeapProfilerModel);
if (!heapProfilerModel) {
return null;
}
this.setProfileBeingRecorded(new Profiler.HeapProfileHeader(heapProfilerModel, this, undefined));
this._profileSamples = new Profiler.HeapTimelineOverview.Samples();
this.profileBeingRecorded()._profileSamples = this._profileSamples;
this._recording = true;
this.addProfile(/** @type {!Profiler.ProfileHeader} */ (this.profileBeingRecorded()));
this.profileBeingRecorded().updateStatus(Common.UIString('Recording\u2026'));
this.dispatchEventToListeners(Profiler.TrackingHeapSnapshotProfileType.TrackingStarted);
return heapProfilerModel;
}
async _stopRecordingProfile() {
this.profileBeingRecorded().updateStatus(Common.UIString('Snapshotting\u2026'));
const stopPromise = this.profileBeingRecorded().heapProfilerModel().stopTrackingHeapObjects(true);
this._recording = false;
this.dispatchEventToListeners(Profiler.TrackingHeapSnapshotProfileType.TrackingStopped);
await stopPromise;
// ------------ ASYNC ------------
const profile = this.profileBeingRecorded();
if (!profile) {
return;
}
profile._finishLoad();
this._profileSamples = null;
this.setProfileBeingRecorded(null);
this.dispatchEventToListeners(Profiler.ProfileType.Events.ProfileComplete, profile);
}
_toggleRecording() {
if (this._recording) {
this._stopRecordingProfile();
} else {
this._startRecordingProfile();
}
return this._recording;
}
/**
* @override
* @return {string}
*/
fileExtension() {
return '.heaptimeline';
}
/**
* @override
*/
get treeItemTitle() {
return ls`ALLOCATION TIMELINES`;
}
/**
* @override
*/
get description() {
return ls`
Allocation timelines show instrumented JavaScript memory allocations over time.
Once profile is recorded you can select a time interval to see objects that
were allocated within it and still alive by the end of recording.
Use this profile type to isolate memory leaks.`;
}
/**
* @override
* @param {!Common.Event} event
*/
_resetProfiles(event) {
const wasRecording = this._recording;
// Clear current profile to avoid stopping backend.
this.setProfileBeingRecorded(null);
super._resetProfiles(event);
this._profileSamples = null;
if (wasRecording) {
this._addNewProfile();
}
}
/**
* @override
*/
profileBeingRecordedRemoved() {
this._stopRecordingProfile();
this._profileSamples = null;
}
};
/**
* @override
*/
Profiler.TrackingHeapSnapshotProfileType.TypeId = 'HEAP-RECORD';
Profiler.TrackingHeapSnapshotProfileType.HeapStatsUpdate = 'HeapStatsUpdate';
Profiler.TrackingHeapSnapshotProfileType.TrackingStarted = 'TrackingStarted';
Profiler.TrackingHeapSnapshotProfileType.TrackingStopped = 'TrackingStopped';
/**
* @unrestricted
*/
Profiler.HeapProfileHeader = class extends Profiler.ProfileHeader {
/**
* @param {?SDK.HeapProfilerModel} heapProfilerModel
* @param {!Profiler.HeapSnapshotProfileType} type
* @param {string=} title
*/
constructor(heapProfilerModel, type, title) {
super(type, title || Common.UIString('Snapshot %d', type.nextProfileUid()));
this._heapProfilerModel = heapProfilerModel;
this.maxJSObjectId = -1;
/** @type {?Profiler.HeapSnapshotWorkerProxy} */
this._workerProxy = null;
/** @type {?Common.OutputStream} */
this._receiver = null;
/** @type {?Profiler.HeapSnapshotProxy} */
this._snapshotProxy = null;
/** @type {!Promise<!Profiler.HeapSnapshotProxy>} */
this._loadPromise = new Promise(resolve => this._fulfillLoad = resolve);
this._totalNumberOfChunks = 0;
this._bufferedWriter = null;
/** @type {?Bindings.TempFile} */
this._tempFile = null;
}
/**
* @return {?SDK.HeapProfilerModel}
*/
heapProfilerModel() {
return this._heapProfilerModel;
}
/**
* @param {number} nodeIndex
* @return {!Promise<?HeapSnapshotModel.Location>}
*/
getLocation(nodeIndex) {
return this._snapshotProxy.getLocation(nodeIndex);
}
/**
* @override
* @param {!Profiler.ProfileType.DataDisplayDelegate} dataDisplayDelegate
* @return {!Profiler.ProfileSidebarTreeElement}
*/
createSidebarTreeElement(dataDisplayDelegate) {
return new Profiler.ProfileSidebarTreeElement(dataDisplayDelegate, this, 'heap-snapshot-sidebar-tree-item');
}
/**
* @override
* @param {!Profiler.ProfileType.DataDisplayDelegate} dataDisplayDelegate
* @return {!Profiler.HeapSnapshotView}
*/
createView(dataDisplayDelegate) {
return new Profiler.HeapSnapshotView(dataDisplayDelegate, this);
}
_prepareToLoad() {
console.assert(!this._receiver, 'Already loading');
this._setupWorker();
this.updateStatus(Common.UIString('Loading\u2026'), true);
}
_finishLoad() {
if (!this._wasDisposed) {
this._receiver.close();
}
if (!this._bufferedWriter) {
return;
}
this._didWriteToTempFile(this._bufferedWriter);
}
/**
* @param {!Bindings.TempFile} tempFile
*/
_didWriteToTempFile(tempFile) {
if (this._wasDisposed) {
if (tempFile) {
tempFile.remove();
}
return;
}
this._tempFile = tempFile;
if (!tempFile) {
this._failedToCreateTempFile = true;
}
if (this._onTempFileReady) {
this._onTempFileReady();
this._onTempFileReady = null;
}
}
_setupWorker() {
/**
* @this {Profiler.HeapProfileHeader}
*/
function setProfileWait(event) {
this.updateStatus(null, event.data);
}
console.assert(!this._workerProxy, 'HeapSnapshotWorkerProxy already exists');
this._workerProxy = new Profiler.HeapSnapshotWorkerProxy(this._handleWorkerEvent.bind(this));
this._workerProxy.addEventListener(Profiler.HeapSnapshotWorkerProxy.Events.Wait, setProfileWait, this);
this._receiver = this._workerProxy.createLoader(this.uid, this._snapshotReceived.bind(this));
}
/**
* @param {string} eventName
* @param {*} data
*/
_handleWorkerEvent(eventName, data) {
if (HeapSnapshotModel.HeapSnapshotProgressEvent.BrokenSnapshot === eventName) {
const error = /** @type {string} */ (data);
Common.console.error(error);
return;
}
if (HeapSnapshotModel.HeapSnapshotProgressEvent.Update !== eventName) {
return;
}
const serializedMessage = /** @type {string} */ (data);
const messageObject = Common.deserializeUIString(serializedMessage);
this.updateStatus(ls(messageObject.messageParts, messageObject.values));
}
/**
* @override
*/
dispose() {
if (this._workerProxy) {
this._workerProxy.dispose();
}
this.removeTempFile();
this._wasDisposed = true;
}
_didCompleteSnapshotTransfer() {
if (!this._snapshotProxy) {
return;
}
this.updateStatus(Number.bytesToString(this._snapshotProxy.totalSize), false);
}
/**
* @param {string} chunk
*/
transferChunk(chunk) {
if (!this._bufferedWriter) {
this._bufferedWriter = new Bindings.TempFile();
}
this._bufferedWriter.write([chunk]);
++this._totalNumberOfChunks;
this._receiver.write(chunk);
}
_snapshotReceived(snapshotProxy) {
if (this._wasDisposed) {
return;
}
this._receiver = null;
this._snapshotProxy = snapshotProxy;
this.maxJSObjectId = snapshotProxy.maxJSObjectId();
this._didCompleteSnapshotTransfer();
this._workerProxy.startCheckingForLongRunningCalls();
this.notifySnapshotReceived();
}
notifySnapshotReceived() {
this._fulfillLoad(this._snapshotProxy);
this.profileType()._snapshotReceived(this);
if (this.canSaveToFile()) {
this.dispatchEventToListeners(Profiler.ProfileHeader.Events.ProfileReceived);
}
}
/**
* @override
* @return {boolean}
*/
canSaveToFile() {
return !this.fromFile() && !!this._snapshotProxy;
}
/**
* @override
*/
saveToFile() {
const fileOutputStream = new Bindings.FileOutputStream();
this._fileName = this._fileName || 'Heap-' + new Date().toISO8601Compact() + this.profileType().fileExtension();
fileOutputStream.open(this._fileName).then(onOpen.bind(this));
/**
* @param {boolean} accepted
* @this {Profiler.HeapProfileHeader}
*/
async function onOpen(accepted) {
if (!accepted) {
return;
}
if (this._failedToCreateTempFile) {
Common.console.error('Failed to open temp file with heap snapshot');
fileOutputStream.close();
return;
}
if (this._tempFile) {
const error = await this._tempFile.copyToOutputStream(fileOutputStream, this._onChunkTransferred.bind(this));
if (error) {
Common.console.error('Failed to read heap snapshot from temp file: ' + error.message);
}
this._didCompleteSnapshotTransfer();
return;
}
this._onTempFileReady = onOpen.bind(this, accepted);
this._updateSaveProgress(0, 1);
}
}
/**
* @param {!Bindings.ChunkedReader} reader
*/
_onChunkTransferred(reader) {
this._updateSaveProgress(reader.loadedSize(), reader.fileSize());
}
/**
* @param {number} value
* @param {number} total
*/
_updateSaveProgress(value, total) {
const percentValue = ((total && value / total) * 100).toFixed(0);
this.updateStatus(Common.UIString('Saving\u2026 %d%%', percentValue));
}
/**
* @override
* @param {!File} file
* @return {!Promise<?FileError>}
*/
async loadFromFile(file) {
this.updateStatus(Common.UIString('Loading\u2026'), true);
this._setupWorker();
const reader = new Bindings.ChunkedFileReader(file, 10000000);
const success = await reader.read(/** @type {!Common.OutputStream} */ (this._receiver));
if (!success) {
this.updateStatus(reader.error().message);
}
return success ? null : reader.error();
}
};
/**
* @unrestricted
*/
Profiler.HeapSnapshotStatisticsView = class extends UI.VBox {
constructor() {
super();
this.element.classList.add('heap-snapshot-statistics-view');
this._pieChart = new PerfUI.PieChart({
chartName: ls`Heap memory usage`,
size: 150,
formatter: Profiler.HeapSnapshotStatisticsView._valueFormatter,
showLegend: true
});
this._pieChart.element.classList.add('heap-snapshot-stats-pie-chart');
this.element.appendChild(this._pieChart.element);
}
/**
* @param {number} value
* @return {string}
*/
static _valueFormatter(value) {
return Common.UIString('%s KB', Number.withThousandsSeparator(Math.round(value / 1024)));
}
/**
* @param {number} value
*/
setTotal(value) {
this._pieChart.setTotal(value);
}
/**
* @param {number} value
* @param {string} name
* @param {string} color
*/
addRecord(value, name, color) {
this._pieChart.addSlice(value, color, name);
}
};
Profiler.HeapAllocationStackView = class extends UI.Widget {
/**
* @param {?SDK.HeapProfilerModel} heapProfilerModel
*/
constructor(heapProfilerModel) {
super();
this._heapProfilerModel = heapProfilerModel;
this._linkifier = new Components.Linkifier();
/** @type {!Array<!Element>} */
this._frameElements = [];
}
/**
* @param {!Element} link
* @param {!Event} event
*/
_onContextMenu(link, event) {
const contextMenu = new UI.ContextMenu(event);
if (!contextMenu.containsTarget(link)) {
contextMenu.appendApplicableItems(link);
}
contextMenu.show();
event.consume(true);
}
/**
* @param {!Event} event
*/
_onStackViewKeydown(event) {
const target = /** @type {?Element} */ (event.target);
if (!target) {
return;
}
if (isEnterKey(event)) {
const link = target._linkElement;
if (!link) {
return;
}
if (Components.Linkifier.invokeFirstAction(link)) {
event.consume(true);
}
return;
}
let navDown;
if (event.key === 'ArrowUp') {
navDown = false;
} else if (event.key === 'ArrowDown') {
navDown = true;
} else {
return;
}
const index = this._frameElements.indexOf(target);
if (index === -1) {
return;
}
const nextIndex = navDown ? index + 1 : index - 1;
if (nextIndex < 0 || nextIndex >= this._frameElements.length) {
return;
}
const nextFrame = this._frameElements[nextIndex];
nextFrame.tabIndex = 0;
target.tabIndex = -1;
nextFrame.focus();
event.consume(true);
}
/**
* @param {!Profiler.HeapSnapshotProxy} snapshot
* @param {number} snapshotNodeIndex
*/
async setAllocatedObject(snapshot, snapshotNodeIndex) {
this.clear();
const frames = await snapshot.allocationStack(snapshotNodeIndex);
if (!frames) {
const stackDiv = this.element.createChild('div', 'no-heap-allocation-stack');
stackDiv.createTextChild(Common.UIString(
'Stack was not recorded for this object because it had been allocated before this profile recording started.'));
return;
}
const stackDiv = this.element.createChild('div', 'heap-allocation-stack');
stackDiv.addEventListener('keydown', this._onStackViewKeydown.bind(this), false);
for (const frame of frames) {
const frameDiv = stackDiv.createChild('div', 'stack-frame');
this._frameElements.push(frameDiv);
frameDiv.tabIndex = -1;
const name = frameDiv.createChild('div');
name.textContent = UI.beautifyFunctionName(frame.functionName);
if (!frame.scriptId) {
continue;
}
const urlElement = this._linkifier.linkifyScriptLocation(
this._heapProfilerModel ? this._heapProfilerModel.target() : null, String(frame.scriptId), frame.scriptName,
frame.line - 1, frame.column - 1);
frameDiv.appendChild(urlElement);
frameDiv._linkElement = urlElement;
frameDiv.addEventListener('contextmenu', this._onContextMenu.bind(this, urlElement));
}
this._frameElements[0].tabIndex = 0;
}
clear() {
this.element.removeChildren();
this._frameElements = [];
this._linkifier.reset();
}
};