| /* |
| * 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(); |
| |
| 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); |
| } |
| |
| /** |
| * @override |
| * @param {!Protocol.HeapProfiler.HeapSnapshotObjectId} snapshotObjectId |
| * @return {!Promise<?Element>} |
| */ |
| async linkifyObject(snapshotObjectId) { |
| const heapProfilerModel = this._profile.heapProfilerModel(); |
| const remoteObject = await heapProfilerModel.objectForSnapshotObjectId(String(snapshotObjectId), 'link'); |
| if (!remoteObject || remoteObject.type !== 'function') |
| return null; |
| const functionDetails = await remoteObject.debuggerModel().functionDetailsPromise(remoteObject); |
| if (!functionDetails || !functionDetails.location) |
| return null; |
| const rawLocation = functionDetails.location; |
| 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(); |
| 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); |
| 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 === 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(); |
| } |
| 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(); |
| } |
| }; |