blob: 75f55a42512184b389a8bbed810dcf619e019d08 [file] [log] [blame]
/*
* Copyright (C) 2013 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
* @param {function(string=)} showImageCallback
* @extends {WebInspector.HBox}
*/
WebInspector.PaintProfilerView = function(showImageCallback)
{
WebInspector.HBox.call(this);
this.element.classList.add("paint-profiler-overview", "hbox");
this._canvasContainer = this.element.createChild("div", "paint-profiler-canvas-container");
this._pieChart = new WebInspector.PieChart(55, this._formatPieChartTime.bind(this));
this.element.createChild("div", "paint-profiler-pie-chart").appendChild(this._pieChart.element);
this._showImageCallback = showImageCallback;
this._canvas = this._canvasContainer.createChild("canvas", "fill");
this._context = this._canvas.getContext("2d");
this._selectionWindow = new WebInspector.OverviewGrid.Window(this._canvasContainer);
this._selectionWindow.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
this._innerBarWidth = 4 * window.devicePixelRatio;
this._minBarHeight = window.devicePixelRatio;
this._barPaddingWidth = 2 * window.devicePixelRatio;
this._outerBarWidth = this._innerBarWidth + this._barPaddingWidth;
this._reset();
}
WebInspector.PaintProfilerView.Events = {
WindowChanged: "WindowChanged"
};
WebInspector.PaintProfilerView.prototype = {
onResize: function()
{
this._update();
},
/**
* @param {?WebInspector.PaintProfilerSnapshot} snapshot
* @param {!Array.<!WebInspector.PaintProfilerLogItem>} log
*/
setSnapshotAndLog: function(snapshot, log)
{
this._reset();
this._snapshot = snapshot;
this._log = log;
this._logCategories = this._log.map(WebInspector.PaintProfilerView._categoryForLogItem);
if (!this._snapshot) {
this._update();
return;
}
snapshot.requestImage(null, null, 1, this._showImageCallback);
snapshot.profile(onProfileDone.bind(this));
/**
* @param {!Array.<!LayerTreeAgent.PaintProfile>=} profiles
* @this {WebInspector.PaintProfilerView}
*/
function onProfileDone(profiles)
{
this._profiles = profiles;
this._update();
}
},
_update: function()
{
this._canvas.width = this._canvasContainer.clientWidth * window.devicePixelRatio;
this._canvas.height = this._canvasContainer.clientHeight * window.devicePixelRatio;
this._samplesPerBar = 0;
if (!this._profiles || !this._profiles.length)
return;
var maxBars = Math.floor((this._canvas.width - 2 * this._barPaddingWidth) / this._outerBarWidth);
var sampleCount = this._log.length;
this._samplesPerBar = Math.ceil(sampleCount / maxBars);
var barCount = Math.floor(sampleCount / this._samplesPerBar);
var maxBarTime = 0;
var barTimes = [];
var barHeightByCategory = [];
var heightByCategory = {};
for (var i = 0, lastBarIndex = 0, lastBarTime = 0; i < sampleCount;) {
var categoryName = (this._logCategories[i] && this._logCategories[i].name) || "misc";
var sampleIndex = this._log[i].commandIndex;
for (var row = 0; row < this._profiles.length; row++) {
var sample = this._profiles[row][sampleIndex];
lastBarTime += sample;
heightByCategory[categoryName] = (heightByCategory[categoryName] || 0) + sample;
}
++i;
if (i - lastBarIndex == this._samplesPerBar || i == sampleCount) {
// Normalize by total number of samples accumulated.
var factor = this._profiles.length * (i - lastBarIndex);
lastBarTime /= factor;
for (categoryName in heightByCategory)
heightByCategory[categoryName] /= factor;
barTimes.push(lastBarTime);
barHeightByCategory.push(heightByCategory);
if (lastBarTime > maxBarTime)
maxBarTime = lastBarTime;
lastBarTime = 0;
heightByCategory = {};
lastBarIndex = i;
}
}
const paddingHeight = 4 * window.devicePixelRatio;
var scale = (this._canvas.height - paddingHeight - this._minBarHeight) / maxBarTime;
for (var i = 0; i < barTimes.length; ++i) {
for (var categoryName in barHeightByCategory[i])
barHeightByCategory[i][categoryName] *= (barTimes[i] * scale + this._minBarHeight) / barTimes[i];
this._renderBar(i, barHeightByCategory[i]);
}
},
/**
* @param {number} index
* @param {!Object.<string, number>} heightByCategory
*/
_renderBar: function(index, heightByCategory)
{
var categories = WebInspector.PaintProfilerView.categories();
var currentHeight = 0;
var x = this._barPaddingWidth + index * this._outerBarWidth;
for (var categoryName in categories) {
if (!heightByCategory[categoryName])
continue;
currentHeight += heightByCategory[categoryName];
var y = this._canvas.height - currentHeight;
this._context.fillStyle = categories[categoryName].color;
this._context.fillRect(x, y, this._innerBarWidth, heightByCategory[categoryName]);
}
},
_onWindowChanged: function()
{
this.dispatchEventToListeners(WebInspector.PaintProfilerView.Events.WindowChanged);
// Update pie chart
var window = this.windowBoundaries();
var totalTime = 0;
var timeByCategory = {};
for (var i = window.left; i <= window.right; ++i) {
var logEntry = this._log[i];
var category = WebInspector.PaintProfilerView._categoryForLogItem(logEntry);
timeByCategory[category.color] = timeByCategory[category.color] || 0;
for (var j = 0; j < this._profiles.length; ++j) {
var time = this._profiles[j][logEntry.commandIndex];
totalTime += time;
timeByCategory[category.color] += time;
}
}
this._pieChart.setTotal(totalTime / this._profiles.length);
for (var color in timeByCategory)
this._pieChart.addSlice(timeByCategory[color] / this._profiles.length, color);
if (this._updateImageTimer)
return;
this._updateImageTimer = setTimeout(this._updateImage.bind(this), 100);
},
/**
* @param {number} value
* @return {string}
*/
_formatPieChartTime: function(value)
{
return Number.millisToString(value * 1000, true);
},
/**
* @return {{left: number, right: number}}
*/
windowBoundaries: function()
{
var screenLeft = this._selectionWindow.windowLeft * this._canvas.width;
var screenRight = this._selectionWindow.windowRight * this._canvas.width;
var barLeft = Math.floor((screenLeft - this._barPaddingWidth) / this._outerBarWidth);
var barRight = Math.floor((screenRight - this._barPaddingWidth + this._innerBarWidth)/ this._outerBarWidth);
var stepLeft = Number.constrain(barLeft * this._samplesPerBar, 0, this._log.length - 1);
var stepRight = Number.constrain(barRight * this._samplesPerBar, 0, this._log.length - 1);
return { left: stepLeft, right: stepRight };
},
_updateImage: function()
{
delete this._updateImageTimer;
if (!this._profiles || !this._profiles.length)
return;
var window = this.windowBoundaries();
this._snapshot.requestImage(this._log[window.left].commandIndex, this._log[window.right].commandIndex, 1, this._showImageCallback);
},
_reset: function()
{
this._snapshot = null;
this._profiles = null;
this._selectionWindow.reset();
},
__proto__: WebInspector.HBox.prototype
};
/**
* @constructor
* @extends {WebInspector.VBox}
*/
WebInspector.PaintProfilerCommandLogView = function()
{
WebInspector.VBox.call(this);
this.setMinimumSize(100, 25);
this.element.classList.add("outline-disclosure", "profiler-log-view", "section");
var sidebarTreeElement = this.element.createChild("ol", "sidebar-tree properties monospace");
sidebarTreeElement.addEventListener("mousemove", this._onMouseMove.bind(this), false);
sidebarTreeElement.addEventListener("mouseout", this._onMouseMove.bind(this), false);
sidebarTreeElement.addEventListener("contextmenu", this._onContextMenu.bind(this), true);
this.sidebarTree = new TreeOutline(sidebarTreeElement);
this._reset();
}
WebInspector.PaintProfilerCommandLogView.prototype = {
/**
* @param {?WebInspector.Target} target
* @param {!Array.<!WebInspector.PaintProfilerLogItem>=} log
*/
setCommandLog: function(target, log)
{
this._target = target;
this._log = log;
this.updateWindow();
},
/**
* @param {!TreeOutline} treeOutline
* @param {!WebInspector.PaintProfilerLogItem} logItem
*/
_appendLogItem: function(treeOutline, logItem)
{
var treeElement = new WebInspector.LogTreeElement(this, logItem);
treeOutline.appendChild(treeElement);
},
/**
* @param {number=} stepLeft
* @param {number=} stepRight
*/
updateWindow: function(stepLeft, stepRight)
{
stepLeft = stepLeft || 0;
stepRight = stepRight || this._log.length - 1;
this.sidebarTree.removeChildren();
for (var i = stepLeft; i <= stepRight; ++i)
this._appendLogItem(this.sidebarTree, this._log[i]);
},
_reset: function()
{
this._log = [];
},
/**
* @param {?Event} event
*/
_onMouseMove: function(event)
{
var node = this.sidebarTree.treeElementFromPoint(event.pageX, event.pageY);
if (node === this._lastHoveredNode || !(node instanceof WebInspector.LogTreeElement))
return;
if (this._lastHoveredNode)
this._lastHoveredNode.setHovered(false);
this._lastHoveredNode = node;
if (this._lastHoveredNode)
this._lastHoveredNode.setHovered(true);
},
/**
* @param {?Event} event
*/
_onContextMenu: function(event)
{
if (!this._target)
return;
var node = this.sidebarTree.treeElementFromPoint(event.pageX, event.pageY);
if (!node || !node.representedObject || !(node instanceof WebInspector.LogTreeElement))
return;
var logItem = /** @type {!WebInspector.PaintProfilerLogItem} */ (node.representedObject);
if (!logItem.nodeId())
return;
var contextMenu = new WebInspector.ContextMenu(event);
var domNode = new WebInspector.DeferredDOMNode(this._target, logItem.nodeId());
contextMenu.appendApplicableItems(domNode);
contextMenu.show();
},
__proto__: WebInspector.VBox.prototype
};
/**
* @constructor
* @param {!WebInspector.PaintProfilerCommandLogView} ownerView
* @param {!WebInspector.PaintProfilerLogItem} logItem
* @extends {TreeElement}
*/
WebInspector.LogTreeElement = function(ownerView, logItem)
{
TreeElement.call(this, "", logItem);
this._ownerView = ownerView;
this._filled = false;
}
WebInspector.LogTreeElement.prototype = {
onattach: function()
{
this._update();
this.hasChildren = !!this.representedObject.params;
},
onexpand: function()
{
if (this._filled)
return;
this._filled = true;
for (var param in this.representedObject.params)
WebInspector.LogPropertyTreeElement._appendLogPropertyItem(this, param, this.representedObject.params[param]);
},
/**
* @param {!Object} param
* @param {string} name
* @return {string}
*/
_paramToString: function(param, name)
{
if (typeof param !== "object")
return typeof param === "string" && param.length > 100 ? name : JSON.stringify(param);
var str = "";
var keyCount = 0;
for (var key in param) {
if (++keyCount > 4 || typeof param[key] === "object" || (typeof param[key] === "string" && param[key].length > 100))
return name;
if (str)
str += ", ";
str += param[key];
}
return str;
},
/**
* @param {!Object} params
* @return {string}
*/
_paramsToString: function(params)
{
var str = "";
for (var key in params) {
if (str)
str += ", ";
str += this._paramToString(params[key], key);
}
return str;
},
_update: function()
{
var logItem = this.representedObject;
var title = document.createDocumentFragment();
title.createChild("div", "selection");
title.createTextChild(logItem.method + "(" + this._paramsToString(logItem.params) + ")");
this.title = title;
},
/**
* @param {boolean} hovered
*/
setHovered: function(hovered)
{
this.listItemElement.classList.toggle("hovered", hovered);
var target = this._ownerView._target;
if (!target)
return;
if (!hovered) {
target.domModel.hideDOMNodeHighlight();
return;
}
var logItem = /** @type {!WebInspector.PaintProfilerLogItem} */ (this.representedObject);
if (!logItem)
return;
var backendNodeId = logItem.nodeId();
if (!backendNodeId)
return;
new WebInspector.DeferredDOMNode(target, backendNodeId).resolve(highlightNode);
/**
* @param {?WebInspector.DOMNode} node
*/
function highlightNode(node)
{
if (node)
node.highlight();
}
},
__proto__: TreeElement.prototype
};
/**
* @constructor
* @param {!{name: string, value}} property
* @extends {TreeElement}
*/
WebInspector.LogPropertyTreeElement = function(property)
{
TreeElement.call(this, "", property);
};
/**
* @param {!TreeElement} element
* @param {string} name
* @param {*} value
*/
WebInspector.LogPropertyTreeElement._appendLogPropertyItem = function(element, name, value)
{
var treeElement = new WebInspector.LogPropertyTreeElement({name: name, value: value});
element.appendChild(treeElement);
if (value && typeof value === "object") {
for (var property in value)
WebInspector.LogPropertyTreeElement._appendLogPropertyItem(treeElement, property, value[property]);
}
};
WebInspector.LogPropertyTreeElement.prototype = {
onattach: function()
{
var property = this.representedObject;
var title = document.createDocumentFragment();
title.createChild("div", "selection");
var nameElement = title.createChild("span", "name");
nameElement.textContent = property.name;
var separatorElement = title.createChild("span", "separator");
separatorElement.textContent = ": ";
if (property.value === null || typeof property.value !== "object") {
var valueElement = title.createChild("span", "value");
valueElement.textContent = JSON.stringify(property.value);
valueElement.classList.add("console-formatted-" + property.value === null ? "null" : typeof property.value);
}
this.title = title;
},
__proto__: TreeElement.prototype
}
/**
* @return {!Object.<string, !WebInspector.PaintProfilerCategory>}
*/
WebInspector.PaintProfilerView.categories = function()
{
if (WebInspector.PaintProfilerView._categories)
return WebInspector.PaintProfilerView._categories;
WebInspector.PaintProfilerView._categories = {
shapes: new WebInspector.PaintProfilerCategory("shapes", WebInspector.UIString("Shapes"), "rgb(255, 161, 129)"),
bitmap: new WebInspector.PaintProfilerCategory("bitmap", WebInspector.UIString("Bitmap"), "rgb(136, 196, 255)"),
text: new WebInspector.PaintProfilerCategory("text", WebInspector.UIString("Text"), "rgb(180, 255, 137)"),
misc: new WebInspector.PaintProfilerCategory("misc", WebInspector.UIString("Misc"), "rgb(206, 160, 255)")
};
return WebInspector.PaintProfilerView._categories;
};
/**
* @constructor
* @param {string} name
* @param {string} title
* @param {string} color
*/
WebInspector.PaintProfilerCategory = function(name, title, color)
{
this.name = name;
this.title = title;
this.color = color;
}
/**
* @return {!Object.<string, !WebInspector.PaintProfilerCategory>}
*/
WebInspector.PaintProfilerView._initLogItemCategories = function()
{
if (WebInspector.PaintProfilerView._logItemCategoriesMap)
return WebInspector.PaintProfilerView._logItemCategoriesMap;
var categories = WebInspector.PaintProfilerView.categories();
var logItemCategories = {};
logItemCategories["Clear"] = categories["misc"];
logItemCategories["DrawPaint"] = categories["misc"];
logItemCategories["DrawData"] = categories["misc"];
logItemCategories["SetMatrix"] = categories["misc"];
logItemCategories["PushCull"] = categories["misc"];
logItemCategories["PopCull"] = categories["misc"];
logItemCategories["Translate"] = categories["misc"];
logItemCategories["Scale"] = categories["misc"];
logItemCategories["Concat"] = categories["misc"];
logItemCategories["Restore"] = categories["misc"];
logItemCategories["SaveLayer"] = categories["misc"];
logItemCategories["Save"] = categories["misc"];
logItemCategories["BeginCommentGroup"] = categories["misc"];
logItemCategories["AddComment"] = categories["misc"];
logItemCategories["EndCommentGroup"] = categories["misc"];
logItemCategories["ClipRect"] = categories["misc"];
logItemCategories["ClipRRect"] = categories["misc"];
logItemCategories["ClipPath"] = categories["misc"];
logItemCategories["ClipRegion"] = categories["misc"];
logItemCategories["DrawPoints"] = categories["shapes"];
logItemCategories["DrawRect"] = categories["shapes"];
logItemCategories["DrawOval"] = categories["shapes"];
logItemCategories["DrawRRect"] = categories["shapes"];
logItemCategories["DrawPath"] = categories["shapes"];
logItemCategories["DrawVertices"] = categories["shapes"];
logItemCategories["DrawDRRect"] = categories["shapes"];
logItemCategories["DrawBitmap"] = categories["bitmap"];
logItemCategories["DrawBitmapRectToRect"] = categories["bitmap"];
logItemCategories["DrawBitmapMatrix"] = categories["bitmap"];
logItemCategories["DrawBitmapNine"] = categories["bitmap"];
logItemCategories["DrawSprite"] = categories["bitmap"];
logItemCategories["DrawPicture"] = categories["bitmap"];
logItemCategories["DrawText"] = categories["text"];
logItemCategories["DrawPosText"] = categories["text"];
logItemCategories["DrawPosTextH"] = categories["text"];
logItemCategories["DrawTextOnPath"] = categories["text"];
WebInspector.PaintProfilerView._logItemCategoriesMap = logItemCategories;
return logItemCategories;
}
/**
* @param {!Object} logItem
* @return {!WebInspector.PaintProfilerCategory}
*/
WebInspector.PaintProfilerView._categoryForLogItem = function(logItem)
{
var method = logItem.method.toTitleCase();
var logItemCategories = WebInspector.PaintProfilerView._initLogItemCategories();
var result = logItemCategories[method];
if (!result) {
result = WebInspector.PaintProfilerView.categories()["misc"];
logItemCategories[method] = result;
}
return result;
}