| /** |
| * 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. |
| */ |
| |
| /** |
| * @implements {PerfUI.FlameChartDataProvider} |
| * @unrestricted |
| */ |
| Profiler.ProfileFlameChartDataProvider = class { |
| constructor() { |
| PerfUI.FlameChartDataProvider.call(this); |
| this._colorGenerator = Profiler.ProfileFlameChartDataProvider.colorGenerator(); |
| } |
| |
| /** |
| * @return {!Common.Color.Generator} |
| */ |
| static colorGenerator() { |
| if (!Profiler.ProfileFlameChartDataProvider._colorGenerator) { |
| const colorGenerator = |
| new Common.Color.Generator({min: 30, max: 330}, {min: 50, max: 80, count: 5}, {min: 80, max: 90, count: 3}); |
| |
| colorGenerator.setColorForID('(idle)', 'hsl(0, 0%, 94%)'); |
| colorGenerator.setColorForID('(program)', 'hsl(0, 0%, 80%)'); |
| colorGenerator.setColorForID('(garbage collector)', 'hsl(0, 0%, 80%)'); |
| Profiler.ProfileFlameChartDataProvider._colorGenerator = colorGenerator; |
| } |
| return Profiler.ProfileFlameChartDataProvider._colorGenerator; |
| } |
| |
| /** |
| * @override |
| * @return {number} |
| */ |
| minimumBoundary() { |
| return this._cpuProfile.profileStartTime; |
| } |
| |
| /** |
| * @override |
| * @return {number} |
| */ |
| totalTime() { |
| return this._cpuProfile.profileHead.total; |
| } |
| |
| /** |
| * @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.beautifyFunctionName(node.functionName); |
| } |
| |
| /** |
| * @override |
| * @param {number} entryIndex |
| * @return {?string} |
| */ |
| entryFont(entryIndex) { |
| if (!this._font) { |
| this._font = '11px ' + Host.fontFamily(); |
| this._boldFont = 'bold ' + this._font; |
| } |
| const node = this._entryNodes[entryIndex]; |
| return node.deoptReason ? this._boldFont : this._font; |
| } |
| |
| /** |
| * @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'; |
| } |
| }; |
| |
| |
| /** |
| * @implements {UI.Searchable} |
| * @unrestricted |
| */ |
| Profiler.CPUProfileFlameChart = class extends UI.VBox { |
| /** |
| * @param {!UI.SearchableView} searchableView |
| * @param {!PerfUI.FlameChartDataProvider} dataProvider |
| */ |
| constructor(searchableView, dataProvider) { |
| super(); |
| this.element.id = 'cpu-flame-chart'; |
| |
| this._searchableView = searchableView; |
| this._overviewPane = new Profiler.CPUProfileFlameChart.OverviewPane(dataProvider); |
| this._overviewPane.show(this.element); |
| |
| this._mainPane = new PerfUI.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; |
| this._searchResults = []; |
| } |
| |
| /** |
| * @override |
| */ |
| focus() { |
| this._mainPane.focus(); |
| } |
| |
| /** |
| * @param {!Common.Event} 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.Event} 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.Event} 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'); |
| |
| const selectedEntryIndex = this._searchResultIndex !== -1 ? this._searchResults[this._searchResultIndex] : -1; |
| this._searchResults = []; |
| const entriesCount = this._dataProvider._entryNodes.length; |
| 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} |
| * @unrestricted |
| */ |
| Profiler.CPUProfileFlameChart.OverviewCalculator = class { |
| constructor(dataProvider) { |
| this._dataProvider = dataProvider; |
| } |
| |
| /** |
| * @param {!Profiler.CPUProfileFlameChart.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._dataProvider.formatValue(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.FlameChartDelegate} |
| * @unrestricted |
| */ |
| Profiler.CPUProfileFlameChart.OverviewPane = class extends UI.VBox { |
| /** |
| * @param {!PerfUI.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 Profiler.CPUProfileFlameChart.OverviewCalculator(dataProvider); |
| this._overviewGrid = new PerfUI.OverviewGrid('cpu-profile-flame-chart', this._overviewCalculator); |
| this._overviewGrid.element.classList.add('fill'); |
| this._overviewCanvas = 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 |
| * @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.Event} 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'); |
| 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; |
| 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 = 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'; |
| } |
| }; |