UNPKG

chrome-devtools-frontend

Version:
645 lines (580 loc) • 18.4 kB
/** * Copyright (C) 2014 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. */ import * as Common from '../common/common.js'; import * as Host from '../host/host.js'; import * as PerfUI from '../perf_ui/perf_ui.js'; import * as SDK from '../sdk/sdk.js'; // eslint-disable-line no-unused-vars import * as UI from '../ui/ui.js'; /** @type {!Common.Color.Generator|null} */ let colorGeneratorInstance = null; /** * @implements {PerfUI.FlameChart.FlameChartDataProvider} */ export class ProfileFlameChartDataProvider { constructor() { this._colorGenerator = ProfileFlameChartDataProvider.colorGenerator(); this._maxStackDepth = 0; /** * @type {?PerfUI.FlameChart.TimelineData} * @protected */ this.timelineData_ = null; /** * @type {!Array<!SDK.ProfileTreeModel.ProfileNode>} * @protected */ this.entryNodes = []; } /** * @return {!Common.Color.Generator} */ static colorGenerator() { if (!colorGeneratorInstance) { colorGeneratorInstance = new Common.Color.Generator( {min: 30, max: 330, count: undefined}, {min: 50, max: 80, count: 5}, {min: 80, max: 90, count: 3}); colorGeneratorInstance.setColorForID('(idle)', 'hsl(0, 0%, 94%)'); colorGeneratorInstance.setColorForID('(program)', 'hsl(0, 0%, 80%)'); colorGeneratorInstance.setColorForID('(garbage collector)', 'hsl(0, 0%, 80%)'); } return colorGeneratorInstance; } /** * @override * @return {number} */ minimumBoundary() { throw 'Not implemented.'; } /** * @override * @return {number} */ totalTime() { throw 'Not implemented.'; } /** * @override * @param {number} value * @param {number=} precision * @return {string} */ formatValue(value, precision) { return Number.preciseMillisToString(value, precision); } /** * @override * @return {number} */ maxStackDepth() { return this._maxStackDepth; } /** * @override * @return {?PerfUI.FlameChart.TimelineData} */ timelineData() { return this.timelineData_ || this._calculateTimelineData(); } /** * @return {!PerfUI.FlameChart.TimelineData} */ _calculateTimelineData() { throw 'Not implemented.'; } /** * @override * @param {number} entryIndex * @return {?Element} */ prepareHighlightedEntryInfo(entryIndex) { throw 'Not implemented.'; } /** * @override * @param {number} entryIndex * @return {boolean} */ canJumpToEntry(entryIndex) { return this.entryNodes[entryIndex].scriptId !== '0'; } /** * @override * @param {number} entryIndex * @return {string} */ entryTitle(entryIndex) { const node = this.entryNodes[entryIndex]; return UI.UIUtils.beautifyFunctionName(node.functionName); } /** * @override * @param {number} entryIndex * @return {?string} */ entryFont(entryIndex) { if (!this._font) { this._font = '11px ' + Host.Platform.fontFamily(); this._boldFont = 'bold ' + this._font; } return this.entryHasDeoptReason(entryIndex) ? /** @type {string} */ (this._boldFont) : this._font; } /** * @param {number} entryIndex * @return {boolean} */ entryHasDeoptReason(entryIndex) { throw 'Not implemented.'; } /** * @override * @param {number} entryIndex * @return {string} */ entryColor(entryIndex) { const node = this.entryNodes[entryIndex]; // For idle and program, we want different 'shades of gray', so we fallback to functionName as scriptId = 0 // For rest of nodes e.g eval scripts, if url is empty then scriptId will be guaranteed to be non-zero return this._colorGenerator.colorForID(node.url || (node.scriptId !== '0' ? node.scriptId : node.functionName)); } /** * @override * @param {number} entryIndex * @param {!CanvasRenderingContext2D} context * @param {?string} text * @param {number} barX * @param {number} barY * @param {number} barWidth * @param {number} barHeight * @return {boolean} */ decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight) { return false; } /** * @override * @param {number} entryIndex * @return {boolean} */ forceDecoration(entryIndex) { return false; } /** * @override * @param {number} entryIndex * @return {string} */ textColor(entryIndex) { return '#333'; } /** * @override */ navStartTimes() { return new Map(); } /** * @return {number} */ entryNodesLength() { return this.entryNodes.length; } } /** * @implements {UI.SearchableView.Searchable} */ export class CPUProfileFlameChart extends UI.Widget.VBox { /** * @param {!UI.SearchableView.SearchableView} searchableView * @param {!ProfileFlameChartDataProvider} dataProvider */ constructor(searchableView, dataProvider) { super(); this.element.id = 'cpu-flame-chart'; this._searchableView = searchableView; this._overviewPane = new OverviewPane(dataProvider); this._overviewPane.show(this.element); this._mainPane = new PerfUI.FlameChart.FlameChart(dataProvider, this._overviewPane); this._mainPane.setBarHeight(15); this._mainPane.setTextBaseline(4); this._mainPane.setTextPadding(2); this._mainPane.show(this.element); this._mainPane.addEventListener(PerfUI.FlameChart.Events.EntrySelected, this._onEntrySelected, this); this._mainPane.addEventListener(PerfUI.FlameChart.Events.EntryInvoked, this._onEntryInvoked, this); this._entrySelected = false; this._mainPane.addEventListener(PerfUI.FlameChart.Events.CanvasFocused, this._onEntrySelected, this); this._overviewPane.addEventListener(PerfUI.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this); this._dataProvider = dataProvider; /** @type {!Array<number>} */ this._searchResults = []; } /** * @override */ focus() { this._mainPane.focus(); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _onWindowChanged(event) { const windowLeft = event.data.windowTimeLeft; const windowRight = event.data.windowTimeRight; this._mainPane.setWindowTimes(windowLeft, windowRight, /* animate */ true); } /** * @param {number} timeLeft * @param {number} timeRight */ selectRange(timeLeft, timeRight) { this._overviewPane._selectRange(timeLeft, timeRight); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _onEntrySelected(event) { if (event.data) { const eventIndex = Number(event.data); this._mainPane.setSelectedEntry(eventIndex); if (eventIndex === -1) { this._entrySelected = false; } else { this._entrySelected = true; } } else if (!this._entrySelected) { this._mainPane.setSelectedEntry(0); this._entrySelected = true; } } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _onEntryInvoked(event) { this._onEntrySelected(event); this.dispatchEventToListeners(PerfUI.FlameChart.Events.EntryInvoked, event.data); } update() { this._overviewPane.update(); this._mainPane.update(); } /** * @override * @param {!UI.SearchableView.SearchConfig} searchConfig * @param {boolean} shouldJump * @param {boolean=} jumpBackwards */ performSearch(searchConfig, shouldJump, jumpBackwards) { const matcher = createPlainTextSearchRegex(searchConfig.query, searchConfig.caseSensitive ? '' : 'i'); /** @type {number} */ const selectedEntryIndex = this._searchResultIndex !== -1 ? this._searchResults[this._searchResultIndex] : -1; this._searchResults = []; const entriesCount = this._dataProvider.entryNodesLength(); for (let index = 0; index < entriesCount; ++index) { if (this._dataProvider.entryTitle(index).match(matcher)) { this._searchResults.push(index); } } if (this._searchResults.length) { this._searchResultIndex = this._searchResults.indexOf(selectedEntryIndex); if (this._searchResultIndex === -1) { this._searchResultIndex = jumpBackwards ? this._searchResults.length - 1 : 0; } this._mainPane.setSelectedEntry(this._searchResults[this._searchResultIndex]); } else { this.searchCanceled(); } this._searchableView.updateSearchMatchesCount(this._searchResults.length); this._searchableView.updateCurrentMatchIndex(this._searchResultIndex); } /** * @override */ searchCanceled() { this._mainPane.setSelectedEntry(-1); this._searchResults = []; this._searchResultIndex = -1; } /** * @override */ jumpToNextSearchResult() { this._searchResultIndex = (this._searchResultIndex + 1) % this._searchResults.length; this._mainPane.setSelectedEntry(this._searchResults[this._searchResultIndex]); this._searchableView.updateCurrentMatchIndex(this._searchResultIndex); } /** * @override */ jumpToPreviousSearchResult() { this._searchResultIndex = (this._searchResultIndex - 1 + this._searchResults.length) % this._searchResults.length; this._mainPane.setSelectedEntry(this._searchResults[this._searchResultIndex]); this._searchableView.updateCurrentMatchIndex(this._searchResultIndex); } /** * @override * @return {boolean} */ supportsCaseSensitiveSearch() { return true; } /** * @override * @return {boolean} */ supportsRegexSearch() { return false; } } /** * @implements {PerfUI.TimelineGrid.Calculator} */ export class OverviewCalculator { /** * @param {function(number, number=): string} formatter */ constructor(formatter) { this._formatter = formatter; /** @type {number} */ this._minimumBoundaries; /** @type {number} */ this._maximumBoundaries; /** @type {number} */ this._xScaleFactor; } /** * @param {!OverviewPane} overviewPane */ _updateBoundaries(overviewPane) { this._minimumBoundaries = overviewPane._dataProvider.minimumBoundary(); const totalTime = overviewPane._dataProvider.totalTime(); this._maximumBoundaries = this._minimumBoundaries + totalTime; this._xScaleFactor = overviewPane._overviewContainer.clientWidth / totalTime; } /** * @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 this._formatter(value - this._minimumBoundaries, 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; } } /** * @implements {PerfUI.FlameChart.FlameChartDelegate} */ export class OverviewPane extends UI.Widget.VBox { /** * @param {!PerfUI.FlameChart.FlameChartDataProvider} dataProvider */ constructor(dataProvider) { super(); this.element.classList.add('cpu-profile-flame-chart-overview-pane'); this._overviewContainer = this.element.createChild('div', 'cpu-profile-flame-chart-overview-container'); this._overviewCalculator = new OverviewCalculator(dataProvider.formatValue); this._overviewGrid = new PerfUI.OverviewGrid.OverviewGrid('cpu-profile-flame-chart', this._overviewCalculator); this._overviewGrid.element.classList.add('fill'); /** @type {!HTMLCanvasElement} */ this._overviewCanvas = /** @type {!HTMLCanvasElement} */ ( this._overviewContainer.createChild('canvas', 'cpu-profile-flame-chart-overview-canvas')); this._overviewContainer.appendChild(this._overviewGrid.element); this._dataProvider = dataProvider; this._overviewGrid.addEventListener(PerfUI.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this); } /** * @override * @param {number} windowStartTime * @param {number} windowEndTime */ windowChanged(windowStartTime, windowEndTime) { this._selectRange(windowStartTime, windowEndTime); } /** * @override * @param {number} startTime * @param {number} endTime */ updateRangeSelection(startTime, endTime) { } /** * @override * @param {!PerfUI.FlameChart.FlameChart} flameChart * @param {?PerfUI.FlameChart.Group} group */ updateSelectedGroup(flameChart, group) { } /** * @param {number} timeLeft * @param {number} timeRight */ _selectRange(timeLeft, timeRight) { const startTime = this._dataProvider.minimumBoundary(); const totalTime = this._dataProvider.totalTime(); this._overviewGrid.setWindow((timeLeft - startTime) / totalTime, (timeRight - startTime) / totalTime); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _onWindowChanged(event) { const windowPosition = {windowTimeLeft: event.data.rawStartValue, windowTimeRight: event.data.rawEndValue}; this._windowTimeLeft = windowPosition.windowTimeLeft; this._windowTimeRight = windowPosition.windowTimeRight; this.dispatchEventToListeners(PerfUI.OverviewGrid.Events.WindowChanged, windowPosition); } /** * @return {?PerfUI.FlameChart.TimelineData} */ _timelineData() { return this._dataProvider.timelineData(); } /** * @override */ onResize() { this._scheduleUpdate(); } _scheduleUpdate() { if (this._updateTimerId) { return; } this._updateTimerId = this.element.window().requestAnimationFrame(this.update.bind(this)); } update() { this._updateTimerId = 0; const timelineData = this._timelineData(); if (!timelineData) { return; } this._resetCanvas( this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - PerfUI.FlameChart.HeaderHeight); this._overviewCalculator._updateBoundaries(this); this._overviewGrid.updateDividers(this._overviewCalculator); this._drawOverviewCanvas(); } _drawOverviewCanvas() { const canvasWidth = this._overviewCanvas.width; const canvasHeight = this._overviewCanvas.height; const drawData = this._calculateDrawData(canvasWidth); const context = this._overviewCanvas.getContext('2d'); if (!context) { throw new Error('Failed to get canvas context'); } const ratio = window.devicePixelRatio; const offsetFromBottom = ratio; const lineWidth = 1; const yScaleFactor = canvasHeight / (this._dataProvider.maxStackDepth() * 1.1); context.lineWidth = lineWidth; context.translate(0.5, 0.5); context.strokeStyle = 'rgba(20,0,0,0.4)'; context.fillStyle = 'rgba(214,225,254,0.8)'; context.moveTo(-lineWidth, canvasHeight + lineWidth); context.lineTo(-lineWidth, Math.round(canvasHeight - drawData[0] * yScaleFactor - offsetFromBottom)); let value = 0; for (let x = 0; x < canvasWidth; ++x) { value = Math.round(canvasHeight - drawData[x] * yScaleFactor - offsetFromBottom); context.lineTo(x, value); } context.lineTo(canvasWidth + lineWidth, value); context.lineTo(canvasWidth + lineWidth, canvasHeight + lineWidth); context.fill(); context.stroke(); context.closePath(); } /** * @param {number} width * @return {!Uint8Array} */ _calculateDrawData(width) { const dataProvider = this._dataProvider; const timelineData = /** @type {!PerfUI.FlameChart.TimelineData} */ (this._timelineData()); const entryStartTimes = timelineData.entryStartTimes; const entryTotalTimes = timelineData.entryTotalTimes; const entryLevels = timelineData.entryLevels; const length = entryStartTimes.length; const minimumBoundary = this._dataProvider.minimumBoundary(); const drawData = new Uint8Array(width); const scaleFactor = width / dataProvider.totalTime(); for (let entryIndex = 0; entryIndex < length; ++entryIndex) { const start = Math.floor((entryStartTimes[entryIndex] - minimumBoundary) * scaleFactor); const finish = Math.floor((entryStartTimes[entryIndex] - minimumBoundary + entryTotalTimes[entryIndex]) * scaleFactor); for (let x = start; x <= finish; ++x) { drawData[x] = Math.max(drawData[x], entryLevels[entryIndex] + 1); } } return drawData; } /** * @param {number} width * @param {number} height */ _resetCanvas(width, height) { const ratio = window.devicePixelRatio; this._overviewCanvas.width = width * ratio; this._overviewCanvas.height = height * ratio; this._overviewCanvas.style.width = width + 'px'; this._overviewCanvas.style.height = height + 'px'; } }