mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 05:35:05 +08:00
Move and rename files. NOAUTOREVERT=true NOPRESUBMIT=true NOTREECHECKS=true Bug: 768828 TBR=darin@chromium.org NOTRY=true Change-Id: I66d3b155808bc5bdbf237b80208e1e552bcf7f28 Reviewed-on: https://chromium-review.googlesource.com/1001153 Reviewed-by: Blink Reformat <blink-reformat@chromium.org> Commit-Queue: Blink Reformat <blink-reformat@chromium.org> Cr-Original-Commit-Position: refs/heads/master@{#549061} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: 0aee4434a4dba42a42abaea9bfbc0cd196a63bc1
2008 lines
63 KiB
JavaScript
2008 lines
63 KiB
JavaScript
/*
|
|
* 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;
|
|
|
|
profile.profileType().addEventListener(
|
|
Profiler.HeapSnapshotProfileType.SnapshotReceived, this._onReceiveSnapshot, this);
|
|
profile.profileType().addEventListener(
|
|
Profiler.ProfileType.Events.RemoveProfileHeader, this._onProfileHeaderRemoved, this);
|
|
|
|
const isHeapTimeline = profile.profileType().id === Profiler.TrackingHeapSnapshotProfileType.TypeId;
|
|
if (isHeapTimeline) {
|
|
this._trackingOverviewGrid = new Profiler.HeapTrackingOverviewGrid(profile);
|
|
this._trackingOverviewGrid.addEventListener(
|
|
Profiler.HeapTrackingOverviewGrid.IdsRangeChanged, this._onIdsRangeChanged.bind(this));
|
|
}
|
|
|
|
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);
|
|
|
|
this._containmentDataGrid = new Profiler.HeapSnapshotContainmentDataGrid(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(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(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(profile._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(profile._heapProfilerModel);
|
|
this._allocationStackView.setMinimumSize(50, 25);
|
|
|
|
this._tabbedPane = new UI.TabbedPane();
|
|
}
|
|
|
|
this._retainmentDataGrid = new Profiler.HeapSnapshotRetainmentDataGrid(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));
|
|
this._updatePerspectiveOptions();
|
|
|
|
this._baseSelect = new UI.ToolbarComboBox(this._changeBase.bind(this));
|
|
this._baseSelect.setVisible(false);
|
|
this._updateBaseOptions();
|
|
|
|
this._filterSelect = new UI.ToolbarComboBox(this._changeFilter.bind(this));
|
|
this._filterSelect.setVisible(false);
|
|
this._updateFilterOptions();
|
|
|
|
this._classNameFilter = new UI.ToolbarInput('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);
|
|
|
|
this.element.addEventListener('contextmenu', this._handleContextMenuEvent.bind(this), true);
|
|
|
|
for (const existingProfile of this._profiles())
|
|
existingProfile.addEventListener(Profiler.ProfileHeader.Events.ProfileTitleChanged, this._updateControls, this);
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
|
|
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();
|
|
this._trackingOverviewGrid._setSamples(samples);
|
|
}
|
|
|
|
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');
|
|
this._statisticsView.addRecord(statistics.total, Common.UIString('Total'));
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
*/
|
|
_handleContextMenuEvent(event) {
|
|
const contextMenu = new UI.ContextMenu(event);
|
|
if (this._dataGrid)
|
|
this._dataGrid.populateContextMenu(contextMenu, event);
|
|
contextMenu.show();
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
if (this._profile._heapProfilerModel && selectedNode instanceof Profiler.HeapSnapshotGenericObjectNode)
|
|
this._profile._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 === 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._popoverHelper.dispose();
|
|
if (this._allocationStackView) {
|
|
this._allocationStackView.clear();
|
|
this._allocationDataGrid.dispose();
|
|
}
|
|
if (this._trackingOverviewGrid)
|
|
this._trackingOverviewGrid.dispose();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @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';
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
get treeItemTitle() {
|
|
return Common.UIString('HEAP SNAPSHOTS');
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
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() {
|
|
return UI.SettingsUI.createSettingCheckbox(
|
|
ls`Record allocation stacks (extra performance overhead)`, this._recordAllocationStacksSetting, true);
|
|
}
|
|
|
|
/**
|
|
* @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.TrackingHeapSnapshotProfileType.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';
|
|
}
|
|
|
|
get treeItemTitle() {
|
|
return ls`ALLOCATION TIMELINES`;
|
|
}
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
Profiler.TrackingHeapSnapshotProfileType.TypeId = 'HEAP-RECORD';
|
|
|
|
Profiler.TrackingHeapSnapshotProfileType.HeapStatsUpdate = 'HeapStatsUpdate';
|
|
Profiler.TrackingHeapSnapshotProfileType.TrackingStarted = 'TrackingStarted';
|
|
Profiler.TrackingHeapSnapshotProfileType.TrackingStopped = 'TrackingStopped';
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
Profiler.TrackingHeapSnapshotProfileType.Samples = class {
|
|
constructor() {
|
|
/** @type {!Array.<number>} */
|
|
this.sizes = [];
|
|
/** @type {!Array.<number>} */
|
|
this.ids = [];
|
|
/** @type {!Array.<number>} */
|
|
this.timestamps = [];
|
|
/** @type {!Array.<number>} */
|
|
this.max = [];
|
|
/** @type {number} */
|
|
this.totalTime = 30000;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* @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 subtitle = /** @type {string} */ (data);
|
|
this.updateStatus(subtitle);
|
|
}
|
|
|
|
/**
|
|
* @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<?Error>}
|
|
*/
|
|
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.HeapTrackingOverviewGrid = class extends UI.VBox {
|
|
/**
|
|
* @param {!Profiler.HeapProfileHeader} heapProfileHeader
|
|
*/
|
|
constructor(heapProfileHeader) {
|
|
super();
|
|
this.element.id = 'heap-recording-view';
|
|
this.element.classList.add('heap-tracking-overview');
|
|
|
|
this._overviewContainer = this.element.createChild('div', 'heap-overview-container');
|
|
this._overviewGrid = new PerfUI.OverviewGrid('heap-recording');
|
|
this._overviewGrid.element.classList.add('fill');
|
|
|
|
this._overviewCanvas = this._overviewContainer.createChild('canvas', 'heap-recording-overview-canvas');
|
|
this._overviewContainer.appendChild(this._overviewGrid.element);
|
|
this._overviewCalculator = new Profiler.HeapTrackingOverviewGrid.OverviewCalculator();
|
|
this._overviewGrid.addEventListener(PerfUI.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
|
|
|
|
this._profileSamples = heapProfileHeader.fromFile() ? new Profiler.TrackingHeapSnapshotProfileType.Samples() :
|
|
heapProfileHeader._profileSamples;
|
|
this._profileType = heapProfileHeader.profileType();
|
|
if (!heapProfileHeader.fromFile() && heapProfileHeader.profileType().profileBeingRecorded() === heapProfileHeader) {
|
|
this._profileType.addEventListener(
|
|
Profiler.TrackingHeapSnapshotProfileType.HeapStatsUpdate, this._onHeapStatsUpdate, this);
|
|
this._profileType.addEventListener(
|
|
Profiler.TrackingHeapSnapshotProfileType.TrackingStopped, this._onStopTracking, this);
|
|
}
|
|
this._windowLeft = 0.0;
|
|
this._windowRight = 1.0;
|
|
this._overviewGrid.setWindow(this._windowLeft, this._windowRight);
|
|
this._yScale = new Profiler.HeapTrackingOverviewGrid.SmoothScale();
|
|
this._xScale = new Profiler.HeapTrackingOverviewGrid.SmoothScale();
|
|
}
|
|
|
|
dispose() {
|
|
this._onStopTracking();
|
|
}
|
|
|
|
_onStopTracking() {
|
|
this._profileType.removeEventListener(
|
|
Profiler.TrackingHeapSnapshotProfileType.HeapStatsUpdate, this._onHeapStatsUpdate, this);
|
|
this._profileType.removeEventListener(
|
|
Profiler.TrackingHeapSnapshotProfileType.TrackingStopped, this._onStopTracking, this);
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.Event} event
|
|
*/
|
|
_onHeapStatsUpdate(event) {
|
|
this._profileSamples = event.data;
|
|
this._scheduleUpdate();
|
|
}
|
|
|
|
/**
|
|
* @param {?HeapSnapshotModel.Samples} samples
|
|
*/
|
|
_setSamples(samples) {
|
|
if (!samples)
|
|
return;
|
|
console.assert(!this._profileSamples.timestamps.length, 'Should only call this method when loading from file.');
|
|
console.assert(samples.timestamps.length);
|
|
this._profileSamples = new Profiler.TrackingHeapSnapshotProfileType.Samples();
|
|
this._profileSamples.sizes = samples.sizes;
|
|
this._profileSamples.ids = samples.lastAssignedIds;
|
|
this._profileSamples.timestamps = samples.timestamps;
|
|
this._profileSamples.max = samples.sizes;
|
|
this._profileSamples.totalTime = /** @type{number} */ (samples.timestamps.peekLast());
|
|
this.update();
|
|
}
|
|
|
|
/**
|
|
* @param {number} width
|
|
* @param {number} height
|
|
*/
|
|
_drawOverviewCanvas(width, height) {
|
|
if (!this._profileSamples)
|
|
return;
|
|
const profileSamples = this._profileSamples;
|
|
const sizes = profileSamples.sizes;
|
|
const topSizes = profileSamples.max;
|
|
const timestamps = profileSamples.timestamps;
|
|
const startTime = timestamps[0];
|
|
const endTime = timestamps[timestamps.length - 1];
|
|
|
|
const scaleFactor = this._xScale.nextScale(width / profileSamples.totalTime);
|
|
let maxSize = 0;
|
|
/**
|
|
* @param {!Array.<number>} sizes
|
|
* @param {function(number, number):void} callback
|
|
*/
|
|
function aggregateAndCall(sizes, callback) {
|
|
let size = 0;
|
|
let currentX = 0;
|
|
for (let i = 1; i < timestamps.length; ++i) {
|
|
const x = Math.floor((timestamps[i] - startTime) * scaleFactor);
|
|
if (x !== currentX) {
|
|
if (size)
|
|
callback(currentX, size);
|
|
size = 0;
|
|
currentX = x;
|
|
}
|
|
size += sizes[i];
|
|
}
|
|
callback(currentX, size);
|
|
}
|
|
|
|
/**
|
|
* @param {number} x
|
|
* @param {number} size
|
|
*/
|
|
function maxSizeCallback(x, size) {
|
|
maxSize = Math.max(maxSize, size);
|
|
}
|
|
|
|
aggregateAndCall(sizes, maxSizeCallback);
|
|
|
|
const yScaleFactor = this._yScale.nextScale(maxSize ? height / (maxSize * 1.1) : 0.0);
|
|
|
|
this._overviewCanvas.width = width * window.devicePixelRatio;
|
|
this._overviewCanvas.height = height * window.devicePixelRatio;
|
|
this._overviewCanvas.style.width = width + 'px';
|
|
this._overviewCanvas.style.height = height + 'px';
|
|
|
|
const context = this._overviewCanvas.getContext('2d');
|
|
context.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
|
|
context.beginPath();
|
|
context.lineWidth = 2;
|
|
context.strokeStyle = 'rgba(192, 192, 192, 0.6)';
|
|
const currentX = (endTime - startTime) * scaleFactor;
|
|
context.moveTo(currentX, height - 1);
|
|
context.lineTo(currentX, 0);
|
|
context.stroke();
|
|
context.closePath();
|
|
|
|
let gridY;
|
|
let gridValue;
|
|
const gridLabelHeight = 14;
|
|
if (yScaleFactor) {
|
|
const maxGridValue = (height - gridLabelHeight) / yScaleFactor;
|
|
// The round value calculation is a bit tricky, because
|
|
// it has a form k*10^n*1024^m, where k=[1,5], n=[0..3], m is an integer,
|
|
// e.g. a round value 10KB is 10240 bytes.
|
|
gridValue = Math.pow(1024, Math.floor(Math.log(maxGridValue) / Math.log(1024)));
|
|
gridValue *= Math.pow(10, Math.floor(Math.log(maxGridValue / gridValue) / Math.LN10));
|
|
if (gridValue * 5 <= maxGridValue)
|
|
gridValue *= 5;
|
|
gridY = Math.round(height - gridValue * yScaleFactor - 0.5) + 0.5;
|
|
context.beginPath();
|
|
context.lineWidth = 1;
|
|
context.strokeStyle = 'rgba(0, 0, 0, 0.2)';
|
|
context.moveTo(0, gridY);
|
|
context.lineTo(width, gridY);
|
|
context.stroke();
|
|
context.closePath();
|
|
}
|
|
|
|
/**
|
|
* @param {number} x
|
|
* @param {number} size
|
|
*/
|
|
function drawBarCallback(x, size) {
|
|
context.moveTo(x, height - 1);
|
|
context.lineTo(x, Math.round(height - size * yScaleFactor - 1));
|
|
}
|
|
|
|
context.beginPath();
|
|
context.lineWidth = 2;
|
|
context.strokeStyle = 'rgba(192, 192, 192, 0.6)';
|
|
aggregateAndCall(topSizes, drawBarCallback);
|
|
context.stroke();
|
|
context.closePath();
|
|
|
|
context.beginPath();
|
|
context.lineWidth = 2;
|
|
context.strokeStyle = 'rgba(0, 0, 192, 0.8)';
|
|
aggregateAndCall(sizes, drawBarCallback);
|
|
context.stroke();
|
|
context.closePath();
|
|
|
|
if (gridValue) {
|
|
const label = Number.bytesToString(gridValue);
|
|
const labelPadding = 4;
|
|
const labelX = 0;
|
|
const labelY = gridY - 0.5;
|
|
const labelWidth = 2 * labelPadding + context.measureText(label).width;
|
|
context.beginPath();
|
|
context.textBaseline = 'bottom';
|
|
context.font = '10px ' + window.getComputedStyle(this.element, null).getPropertyValue('font-family');
|
|
context.fillStyle = 'rgba(255, 255, 255, 0.75)';
|
|
context.fillRect(labelX, labelY - gridLabelHeight, labelWidth, gridLabelHeight);
|
|
context.fillStyle = 'rgb(64, 64, 64)';
|
|
context.fillText(label, labelX + labelPadding, labelY);
|
|
context.fill();
|
|
context.closePath();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onResize() {
|
|
this._updateOverviewCanvas = true;
|
|
this._scheduleUpdate();
|
|
}
|
|
|
|
_onWindowChanged() {
|
|
if (!this._updateGridTimerId)
|
|
this._updateGridTimerId = setTimeout(this._updateGrid.bind(this), 10);
|
|
}
|
|
|
|
_scheduleUpdate() {
|
|
if (this._updateTimerId)
|
|
return;
|
|
this._updateTimerId = setTimeout(this.update.bind(this), 10);
|
|
}
|
|
|
|
_updateBoundaries() {
|
|
this._windowLeft = this._overviewGrid.windowLeft();
|
|
this._windowRight = this._overviewGrid.windowRight();
|
|
this._windowWidth = this._windowRight - this._windowLeft;
|
|
}
|
|
|
|
update() {
|
|
this._updateTimerId = null;
|
|
if (!this.isShowing())
|
|
return;
|
|
this._updateBoundaries();
|
|
this._overviewCalculator._updateBoundaries(this);
|
|
this._overviewGrid.updateDividers(this._overviewCalculator);
|
|
this._drawOverviewCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - 20);
|
|
}
|
|
|
|
_updateGrid() {
|
|
this._updateGridTimerId = 0;
|
|
this._updateBoundaries();
|
|
const ids = this._profileSamples.ids;
|
|
const timestamps = this._profileSamples.timestamps;
|
|
const sizes = this._profileSamples.sizes;
|
|
const startTime = timestamps[0];
|
|
const totalTime = this._profileSamples.totalTime;
|
|
const timeLeft = startTime + totalTime * this._windowLeft;
|
|
const timeRight = startTime + totalTime * this._windowRight;
|
|
let minId = 0;
|
|
let maxId = ids[ids.length - 1] + 1;
|
|
let size = 0;
|
|
for (let i = 0; i < timestamps.length; ++i) {
|
|
if (!timestamps[i])
|
|
continue;
|
|
if (timestamps[i] > timeRight)
|
|
break;
|
|
maxId = ids[i];
|
|
if (timestamps[i] < timeLeft) {
|
|
minId = ids[i];
|
|
continue;
|
|
}
|
|
size += sizes[i];
|
|
}
|
|
|
|
this.dispatchEventToListeners(
|
|
Profiler.HeapTrackingOverviewGrid.IdsRangeChanged, {minId: minId, maxId: maxId, size: size});
|
|
}
|
|
};
|
|
|
|
Profiler.HeapTrackingOverviewGrid.IdsRangeChanged = Symbol('IdsRangeChanged');
|
|
|
|
Profiler.HeapTrackingOverviewGrid.SmoothScale = class {
|
|
constructor() {
|
|
this._lastUpdate = 0;
|
|
this._currentScale = 0.0;
|
|
}
|
|
|
|
/**
|
|
* @param {number} target
|
|
* @return {number}
|
|
*/
|
|
nextScale(target) {
|
|
target = target || this._currentScale;
|
|
if (this._currentScale) {
|
|
const now = Date.now();
|
|
const timeDeltaMs = now - this._lastUpdate;
|
|
this._lastUpdate = now;
|
|
const maxChangePerSec = 20;
|
|
const maxChangePerDelta = Math.pow(maxChangePerSec, timeDeltaMs / 1000);
|
|
const scaleChange = target / this._currentScale;
|
|
this._currentScale *= Number.constrain(scaleChange, 1 / maxChangePerDelta, maxChangePerDelta);
|
|
} else {
|
|
this._currentScale = target;
|
|
}
|
|
return this._currentScale;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @implements {PerfUI.TimelineGrid.Calculator}
|
|
* @unrestricted
|
|
*/
|
|
Profiler.HeapTrackingOverviewGrid.OverviewCalculator = class {
|
|
/**
|
|
* @param {!Profiler.HeapTrackingOverviewGrid} chart
|
|
*/
|
|
_updateBoundaries(chart) {
|
|
this._minimumBoundaries = 0;
|
|
this._maximumBoundaries = chart._profileSamples.totalTime;
|
|
this._xScaleFactor = chart._overviewContainer.clientWidth / this._maximumBoundaries;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {number} time
|
|
* @return {number}
|
|
*/
|
|
computePosition(time) {
|
|
return (time - this._minimumBoundaries) * this._xScaleFactor;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {number} value
|
|
* @param {number=} precision
|
|
* @return {string}
|
|
*/
|
|
formatValue(value, precision) {
|
|
return Number.secondsToString(value / 1000, !!precision);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {number}
|
|
*/
|
|
maximumBoundary() {
|
|
return this._maximumBoundaries;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {number}
|
|
*/
|
|
minimumBoundary() {
|
|
return this._minimumBoundaries;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {number}
|
|
*/
|
|
zeroTime() {
|
|
return this._minimumBoundaries;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {number}
|
|
*/
|
|
boundarySpan() {
|
|
return this._maximumBoundaries - this._minimumBoundaries;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
Profiler.HeapSnapshotStatisticsView = class extends UI.VBox {
|
|
constructor() {
|
|
super();
|
|
this.element.classList.add('heap-snapshot-statistics-view');
|
|
this._pieChart = new PerfUI.PieChart(150, Profiler.HeapSnapshotStatisticsView._valueFormatter, true);
|
|
this._pieChart.element.classList.add('heap-snapshot-stats-pie-chart');
|
|
this.element.appendChild(this._pieChart.element);
|
|
this._labels = this.element.createChild('div', 'heap-snapshot-stats-legend');
|
|
}
|
|
|
|
/**
|
|
* @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) {
|
|
if (color)
|
|
this._pieChart.addSlice(value, color);
|
|
|
|
const node = this._labels.createChild('div');
|
|
const swatchDiv = node.createChild('div', 'heap-snapshot-stats-swatch');
|
|
const nameDiv = node.createChild('div', 'heap-snapshot-stats-name');
|
|
const sizeDiv = node.createChild('div', 'heap-snapshot-stats-size');
|
|
if (color)
|
|
swatchDiv.style.backgroundColor = color;
|
|
else
|
|
swatchDiv.classList.add('heap-snapshot-stats-empty-swatch');
|
|
nameDiv.textContent = name;
|
|
sizeDiv.textContent = Profiler.HeapSnapshotStatisticsView._valueFormatter(value);
|
|
}
|
|
};
|
|
|
|
Profiler.HeapAllocationStackView = class extends UI.Widget {
|
|
/**
|
|
* @param {?SDK.HeapProfilerModel} heapProfilerModel
|
|
*/
|
|
constructor(heapProfilerModel) {
|
|
super();
|
|
this._heapProfilerModel = heapProfilerModel;
|
|
this._linkifier = new Components.Linkifier();
|
|
}
|
|
|
|
/**
|
|
* @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');
|
|
for (const frame of frames) {
|
|
const frameDiv = stackDiv.createChild('div', 'stack-frame');
|
|
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);
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.element.removeChildren();
|
|
this._linkifier.reset();
|
|
}
|
|
};
|