blob: cececbc3f1d7c9631544d8f92d4e842f6f5d7604 [file] [log] [blame]
/**
* 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';
}
};