blob: 544f7d3d71c2ae194d175e6a8be36223253ae622 [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.
*/
/**
* @constructor
* @implements {WebInspector.FlameChartDataProvider}
* @param {!WebInspector.CPUProfileDataModel} cpuProfile
* @param {!WebInspector.Target} target
*/
WebInspector.CPUFlameChartDataProvider = function(cpuProfile, target)
{
WebInspector.FlameChartDataProvider.call(this);
this._cpuProfile = cpuProfile;
this._target = target;
this._colorGenerator = WebInspector.CPUFlameChartDataProvider.colorGenerator();
}
WebInspector.CPUFlameChartDataProvider.prototype = {
/**
* @return {number}
*/
barHeight: function()
{
return 15;
},
/**
* @return {number}
*/
textBaseline: function()
{
return 4;
},
/**
* @return {number}
*/
textPadding: function()
{
return 2;
},
/**
* @param {number} startTime
* @param {number} endTime
* @return {?Array.<number>}
*/
dividerOffsets: function(startTime, endTime)
{
return null;
},
/**
* @return {number}
*/
zeroTime: function()
{
return this._cpuProfile.profileStartTime;
},
/**
* @return {number}
*/
totalTime: function()
{
return this._cpuProfile.profileHead.totalTime;
},
/**
* @return {number}
*/
maxStackDepth: function()
{
return this._maxStackDepth;
},
/**
* @return {?WebInspector.FlameChart.TimelineData}
*/
timelineData: function()
{
return this._timelineData || this._calculateTimelineData();
},
/**
* @return {?WebInspector.FlameChart.TimelineData}
*/
_calculateTimelineData: function()
{
/**
* @constructor
* @param {number} depth
* @param {number} duration
* @param {number} startTime
* @param {!Object} node
*/
function ChartEntry(depth, duration, startTime, selfTime, node)
{
this.depth = depth;
this.duration = duration;
this.startTime = startTime;
this.node = node;
this.selfTime = selfTime;
}
/** @type {!Array.<?ChartEntry>} */
var entries = [];
/** @type {!Array.<number>} */
var stack = [];
var maxDepth = 5;
function onOpenFrame()
{
stack.push(entries.length);
// Reserve space for the entry, as they have to be ordered by startTime.
// The entry itself will be put there in onCloseFrame.
entries.push(null);
}
function onCloseFrame(depth, node, startTime, totalTime, selfTime)
{
var index = stack.pop();
entries[index] = new ChartEntry(depth, totalTime, startTime, selfTime, node);
maxDepth = Math.max(maxDepth, depth);
}
this._cpuProfile.forEachFrame(onOpenFrame, onCloseFrame);
/** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */
var entryNodes = new Array(entries.length);
var entryLevels = new Uint8Array(entries.length);
var entryTotalTimes = new Float32Array(entries.length);
var entrySelfTimes = new Float32Array(entries.length);
var entryOffsets = new Float32Array(entries.length);
var zeroTime = this.zeroTime();
for (var i = 0; i < entries.length; ++i) {
var entry = entries[i];
entryNodes[i] = entry.node;
entryLevels[i] = entry.depth;
entryTotalTimes[i] = entry.duration;
entryOffsets[i] = entry.startTime - zeroTime;
entrySelfTimes[i] = entry.selfTime;
}
this._maxStackDepth = maxDepth;
/** @type {!WebInspector.FlameChart.TimelineData} */
this._timelineData = {
entryLevels: entryLevels,
entryTotalTimes: entryTotalTimes,
entryOffsets: entryOffsets,
};
/** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */
this._entryNodes = entryNodes;
this._entrySelfTimes = entrySelfTimes;
return this._timelineData;
},
/**
* @param {number} ms
* @return {string}
*/
_millisecondsToString: function(ms)
{
if (ms === 0)
return "0";
if (ms < 1000)
return WebInspector.UIString("%.1f\u2009ms", ms);
return Number.secondsToString(ms / 1000, true);
},
/**
* @param {number} entryIndex
* @return {?Array.<!{title: string, text: string}>}
*/
prepareHighlightedEntryInfo: function(entryIndex)
{
var timelineData = this._timelineData;
var node = this._entryNodes[entryIndex];
if (!node)
return null;
var entryInfo = [];
function pushEntryInfoRow(title, text)
{
var row = {};
row.title = title;
row.text = text;
entryInfo.push(row);
}
pushEntryInfoRow(WebInspector.UIString("Name"), node.functionName);
var selfTime = this._millisecondsToString(this._entrySelfTimes[entryIndex]);
var totalTime = this._millisecondsToString(timelineData.entryTotalTimes[entryIndex]);
pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime);
pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime);
var target = this._target;
var text = WebInspector.Linkifier.liveLocationText(target, node.scriptId, node.lineNumber, node.columnNumber);
pushEntryInfoRow(WebInspector.UIString("URL"), text);
pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true));
pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true));
if (node.deoptReason && node.deoptReason !== "no reason")
pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason);
return entryInfo;
},
/**
* @param {number} entryIndex
* @return {boolean}
*/
canJumpToEntry: function(entryIndex)
{
return this._entryNodes[entryIndex].scriptId !== "0";
},
/**
* @param {number} entryIndex
* @return {?string}
*/
entryTitle: function(entryIndex)
{
var node = this._entryNodes[entryIndex];
return node.functionName;
},
/**
* @param {number} entryIndex
* @return {?string}
*/
entryFont: function(entryIndex)
{
if (!this._font) {
this._font = (this.barHeight() - 4) + "px " + WebInspector.fontFamily();
this._boldFont = "bold " + this._font;
}
var node = this._entryNodes[entryIndex];
var reason = node.deoptReason;
return (reason && reason !== "no reason") ? this._boldFont : this._font;
},
/**
* @param {number} entryIndex
* @return {!string}
*/
entryColor: function(entryIndex)
{
var node = this._entryNodes[entryIndex];
return this._colorGenerator.colorForID(node.functionName + ":" + node.url + ":" + node.lineNumber);
},
/**
* @param {number} entryIndex
* @param {!CanvasRenderingContext2D} context
* @param {?string} text
* @param {number} barX
* @param {number} barY
* @param {number} barWidth
* @param {number} barHeight
* @param {function(number):number} offsetToPosition
* @return {boolean}
*/
decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, offsetToPosition)
{
return false;
},
/**
* @param {number} entryIndex
* @return {boolean}
*/
forceDecoration: function(entryIndex)
{
return false;
},
/**
* @param {number} entryIndex
* @return {!{startTimeOffset: number, endTimeOffset: number}}
*/
highlightTimeRange: function(entryIndex)
{
var startTimeOffset = this._timelineData.entryOffsets[entryIndex];
return {
startTimeOffset: startTimeOffset,
endTimeOffset: startTimeOffset + this._timelineData.entryTotalTimes[entryIndex]
};
},
/**
* @return {number}
*/
paddingLeft: function()
{
return 15;
},
/**
* @param {number} entryIndex
* @return {!string}
*/
textColor: function(entryIndex)
{
return "#333";
}
}
/**
* @return {!WebInspector.FlameChart.ColorGenerator}
*/
WebInspector.CPUFlameChartDataProvider.colorGenerator = function()
{
if (!WebInspector.CPUFlameChartDataProvider._colorGenerator) {
var colorGenerator = new WebInspector.FlameChart.ColorGenerator();
colorGenerator.setColorForID("(idle)::0", "hsl(0, 0%, 94%)");
colorGenerator.setColorForID("(program)::0", "hsl(0, 0%, 80%)");
colorGenerator.setColorForID("(garbage collector)::0", "hsl(0, 0%, 80%)");
WebInspector.CPUFlameChartDataProvider._colorGenerator = colorGenerator;
}
return WebInspector.CPUFlameChartDataProvider._colorGenerator;
}
/**
* @constructor
* @extends {WebInspector.VBox}
* @param {!WebInspector.FlameChartDataProvider} dataProvider
*/
WebInspector.CPUProfileFlameChart = function(dataProvider)
{
WebInspector.VBox.call(this);
this.registerRequiredCSS("flameChart.css");
this.element.id = "cpu-flame-chart";
this._overviewPane = new WebInspector.CPUProfileFlameChart.OverviewPane(dataProvider);
this._overviewPane.show(this.element);
this._mainPane = new WebInspector.FlameChart(dataProvider, this._overviewPane, true);
this._mainPane.show(this.element);
this._mainPane.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this);
this._overviewPane.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
}
WebInspector.CPUProfileFlameChart.prototype = {
/**
* @param {!WebInspector.Event} event
*/
_onWindowChanged: function(event)
{
var windowLeft = event.data.windowTimeLeft;
var windowRight = event.data.windowTimeRight;
this._mainPane.setWindowTimes(windowLeft, windowRight);
},
/**
* @param {!number} timeLeft
* @param {!number} timeRight
*/
selectRange: function(timeLeft, timeRight)
{
this._overviewPane._selectRange(timeLeft, timeRight);
},
/**
* @param {!WebInspector.Event} event
*/
_onEntrySelected: function(event)
{
this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, event.data);
},
update: function()
{
this._overviewPane.update();
this._mainPane.update();
},
__proto__: WebInspector.VBox.prototype
};
/**
* @constructor
* @implements {WebInspector.TimelineGrid.Calculator}
*/
WebInspector.CPUProfileFlameChart.OverviewCalculator = function()
{
}
WebInspector.CPUProfileFlameChart.OverviewCalculator.prototype = {
/**
* @return {number}
*/
paddingLeft: function()
{
return 0;
},
/**
* @param {!WebInspector.CPUProfileFlameChart.OverviewPane} overviewPane
*/
_updateBoundaries: function(overviewPane)
{
this._minimumBoundaries = overviewPane._dataProvider.zeroTime();
var totalTime = overviewPane._dataProvider.totalTime();
this._maximumBoundaries = this._minimumBoundaries + totalTime;
this._xScaleFactor = overviewPane._overviewContainer.clientWidth / totalTime;
},
/**
* @param {number} time
* @return {number}
*/
computePosition: function(time)
{
return (time - this._minimumBoundaries) * this._xScaleFactor;
},
/**
* @param {number} value
* @param {number=} precision
* @return {string}
*/
formatTime: function(value, precision)
{
return Number.secondsToString((value - this._minimumBoundaries) / 1000);
},
/**
* @return {number}
*/
maximumBoundary: function()
{
return this._maximumBoundaries;
},
/**
* @return {number}
*/
minimumBoundary: function()
{
return this._minimumBoundaries;
},
/**
* @return {number}
*/
zeroTime: function()
{
return this._minimumBoundaries;
},
/**
* @return {number}
*/
boundarySpan: function()
{
return this._maximumBoundaries - this._minimumBoundaries;
}
}
/**
* @constructor
* @extends {WebInspector.VBox}
* @implements {WebInspector.FlameChartDelegate}
* @param {!WebInspector.FlameChartDataProvider} dataProvider
*/
WebInspector.CPUProfileFlameChart.OverviewPane = function(dataProvider)
{
WebInspector.VBox.call(this);
this.element.classList.add("flame-chart-overview-pane");
this._overviewContainer = this.element.createChild("div", "overview-container");
this._overviewGrid = new WebInspector.OverviewGrid("flame-chart");
this._overviewGrid.element.classList.add("fill");
this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas");
this._overviewContainer.appendChild(this._overviewGrid.element);
this._overviewCalculator = new WebInspector.CPUProfileFlameChart.OverviewCalculator();
this._dataProvider = dataProvider;
this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
}
WebInspector.CPUProfileFlameChart.OverviewPane.prototype = {
/**
* @param {number} windowStartTime
* @param {number} windowEndTime
*/
requestWindowTimes: function(windowStartTime, windowEndTime)
{
this._selectRange(windowStartTime, windowEndTime);
},
/**
* @param {!number} timeLeft
* @param {!number} timeRight
*/
_selectRange: function(timeLeft, timeRight)
{
var startTime = this._dataProvider.zeroTime();
var totalTime = this._dataProvider.totalTime();
this._overviewGrid.setWindow((timeLeft - startTime) / totalTime, (timeRight - startTime) / totalTime);
},
/**
* @param {!WebInspector.Event} event
*/
_onWindowChanged: function(event)
{
var startTime = this._dataProvider.zeroTime();
var totalTime = this._dataProvider.totalTime();
var data = {
windowTimeLeft: startTime + this._overviewGrid.windowLeft() * totalTime,
windowTimeRight: startTime + this._overviewGrid.windowRight() * totalTime
};
this.dispatchEventToListeners(WebInspector.OverviewGrid.Events.WindowChanged, data);
},
/**
* @return {?WebInspector.FlameChart.TimelineData}
*/
_timelineData: function()
{
return this._dataProvider.timelineData();
},
onResize: function()
{
this._scheduleUpdate();
},
_scheduleUpdate: function()
{
if (this._updateTimerId)
return;
this._updateTimerId = requestAnimationFrame(this.update.bind(this));
},
update: function()
{
this._updateTimerId = 0;
var timelineData = this._timelineData();
if (!timelineData)
return;
this._resetCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - WebInspector.FlameChart.DividersBarHeight);
this._overviewCalculator._updateBoundaries(this);
this._overviewGrid.updateDividers(this._overviewCalculator);
this._drawOverviewCanvas();
},
_drawOverviewCanvas: function()
{
var canvasWidth = this._overviewCanvas.width;
var canvasHeight = this._overviewCanvas.height;
var drawData = this._calculateDrawData(canvasWidth);
var context = this._overviewCanvas.getContext("2d");
var ratio = window.devicePixelRatio;
var offsetFromBottom = ratio;
var lineWidth = 1;
var 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));
var value;
for (var 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: function(width)
{
var dataProvider = this._dataProvider;
var timelineData = this._timelineData();
var entryOffsets = timelineData.entryOffsets;
var entryTotalTimes = timelineData.entryTotalTimes;
var entryLevels = timelineData.entryLevels;
var length = entryOffsets.length;
var drawData = new Uint8Array(width);
var scaleFactor = width / dataProvider.totalTime();
for (var entryIndex = 0; entryIndex < length; ++entryIndex) {
var start = Math.floor(entryOffsets[entryIndex] * scaleFactor);
var finish = Math.floor((entryOffsets[entryIndex] + entryTotalTimes[entryIndex]) * scaleFactor);
for (var x = start; x <= finish; ++x)
drawData[x] = Math.max(drawData[x], entryLevels[entryIndex] + 1);
}
return drawData;
},
/**
* @param {!number} width
* @param {!number} height
*/
_resetCanvas: function(width, height)
{
var 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";
},
__proto__: WebInspector.VBox.prototype
}