| /* |
| * 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(); |
| } |
| }; |