blob: b55900f66825cf0c48ea7766b46128b82d557c41 [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
* @extends {WebInspector.MemoryStatistics}
* @param {!WebInspector.TimelinePanel} timelinePanel
* @param {!WebInspector.TimelineModel} model
*/
WebInspector.DOMCountersGraph = function(timelinePanel, model)
{
WebInspector.MemoryStatistics.call(this, timelinePanel, model);
}
/**
* @constructor
* @extends {WebInspector.CounterUIBase}
* @param {!WebInspector.DOMCountersGraph} memoryCountersPane
* @param {string} title
* @param {string} currentValueLabel
* @param {!string} color
* @param {function(!WebInspector.DOMCountersGraph.Counter):number} valueGetter
*/
WebInspector.DOMCounterUI = function(memoryCountersPane, title, currentValueLabel, color, valueGetter)
{
WebInspector.CounterUIBase.call(this, memoryCountersPane, title, color, valueGetter)
this._range = this._swatch.element.createChild("span");
this._value = memoryCountersPane._currentValuesBar.createChild("span", "memory-counter-value");
this._value.style.color = color;
this._currentValueLabel = currentValueLabel;
this.graphColor = color;
this.graphYValues = [];
}
/**
* @constructor
* @extends {WebInspector.MemoryStatistics.Counter}
* @param {number} time
* @param {number} documentCount
* @param {number} nodeCount
* @param {number} listenerCount
*/
WebInspector.DOMCountersGraph.Counter = function(time, documentCount, nodeCount, listenerCount, usedGPUMemoryKBytes)
{
WebInspector.MemoryStatistics.Counter.call(this, time);
this.documentCount = documentCount;
this.nodeCount = nodeCount;
this.listenerCount = listenerCount;
this.usedGPUMemoryKBytes = usedGPUMemoryKBytes;
}
WebInspector.DOMCounterUI.prototype = {
/**
* @param {number} minValue
* @param {number} maxValue
*/
setRange: function(minValue, maxValue)
{
this._range.textContent = WebInspector.UIString("[%d:%d]", minValue, maxValue);
},
updateCurrentValue: function(countersEntry)
{
this._value.textContent = WebInspector.UIString(this._currentValueLabel, this.valueGetter(countersEntry));
},
clearCurrentValueAndMarker: function(ctx)
{
this._value.textContent = "";
this.restoreImageUnderMarker(ctx);
},
/**
* @param {!CanvasRenderingContext2D} ctx
* @param {number} x
* @param {number} y
* @param {number} radius
*/
saveImageUnderMarker: function(ctx, x, y, radius)
{
const w = radius + 1;
var imageData = ctx.getImageData(x - w, y - w, 2 * w, 2 * w);
this._imageUnderMarker = {
x: x - w,
y: y - w,
imageData: imageData
};
},
/**
* @param {!CanvasRenderingContext2D} ctx
*/
restoreImageUnderMarker: function(ctx)
{
if (!this.visible)
return;
if (this._imageUnderMarker)
ctx.putImageData(this._imageUnderMarker.imageData, this._imageUnderMarker.x, this._imageUnderMarker.y);
this.discardImageUnderMarker();
},
discardImageUnderMarker: function()
{
delete this._imageUnderMarker;
},
__proto__: WebInspector.CounterUIBase.prototype
}
WebInspector.DOMCountersGraph.prototype = {
_createCurrentValuesBar: function()
{
this._currentValuesBar = this._canvasContainer.createChild("div");
this._currentValuesBar.id = "counter-values-bar";
this._canvasContainer.classList.add("dom-counters");
},
/**
* @return {!Element}
*/
resizeElement: function()
{
return this._currentValuesBar;
},
/**
* @return {!Array.<!WebInspector.DOMCounterUI>}
*/
_createCounterUIList: function()
{
function getDocumentCount(entry)
{
return entry.documentCount;
}
function getNodeCount(entry)
{
return entry.nodeCount;
}
function getListenerCount(entry)
{
return entry.listenerCount;
}
function getUsedGPUMemoryKBytes(entry)
{
return entry.usedGPUMemoryKBytes || 0;
}
var counterUIs = [
new WebInspector.DOMCounterUI(this, "Documents", "Documents: %d", "#d00", getDocumentCount),
new WebInspector.DOMCounterUI(this, "Nodes", "Nodes: %d", "#0a0", getNodeCount),
new WebInspector.DOMCounterUI(this, "Listeners", "Listeners: %d", "#00d", getListenerCount)
];
if (WebInspector.experimentsSettings.gpuTimeline.isEnabled())
counterUIs.push(new WebInspector.DOMCounterUI(this, "GPU Memory", "GPU Memory [KB]: %d", "#c0c", getUsedGPUMemoryKBytes));
return counterUIs;
},
/**
* @param {!WebInspector.Event} event
*/
_onRecordAdded: function(event)
{
/**
* @param {!Array.<!T>} array
* @param {!S} item
* @param {!function(!T,!S):!number} comparator
* @return {!number}
* @template T,S
*/
function findInsertionLocation(array, item, comparator)
{
var index = array.length;
while (index > 0 && comparator(array[index - 1], item) > 0)
--index;
return index;
}
/**
* @this {WebInspector.DOMCountersGraph}
*/
function addStatistics(record)
{
var counters = record["counters"];
var isGPURecord = record.data && typeof record.data["usedGPUMemoryBytes"] !== "undefined";
if (isGPURecord) {
counters = counters || {
"startTime": record.startTime,
"endTime": record.endTime
};
counters["usedGPUMemoryKBytes"] = Math.round(record.data["usedGPUMemoryBytes"] / 1024);
}
if (!counters)
return;
var time = record.endTime || record.startTime;
var counter = new WebInspector.DOMCountersGraph.Counter(
time,
counters["documents"],
counters["nodes"],
counters["jsEventListeners"],
counters["usedGPUMemoryKBytes"]
);
function compare(record, time)
{
return record.time - time;
}
var index = findInsertionLocation(this._counters, time, compare);
this._counters.splice(index, 0, counter);
if (isGPURecord) {
// Populate missing values from preceeding records.
// FIXME: Refactor the code to make each WebInspector.DOMCountersGraph.Counter
// be responsible for a single graph to avoid such synchronizations.
for (var i = index - 1; i >= 0 && typeof this._counters[i].usedGPUMemoryKBytes === "undefined"; --i) { }
var usedGPUMemoryKBytes = this._counters[i >= 0 ? i : index].usedGPUMemoryKBytes;
for (i = Math.max(i, 0); i < index; ++i)
this._counters[i].usedGPUMemoryKBytes = usedGPUMemoryKBytes;
var copyFrom = index > 0 ? index - 1 : index + 1;
if (copyFrom < this._counters.length) {
this._counters[index].documentCount = this._counters[copyFrom].documentCount;
this._counters[index].nodeCount = this._counters[copyFrom].nodeCount;
this._counters[index].listenerCount = this._counters[copyFrom].listenerCount;
} else {
this._counters[index].documentCount = 0;
this._counters[index].nodeCount = 0;
this._counters[index].listenerCount = 0;
}
}
}
WebInspector.TimelinePresentationModel.forAllRecords([event.data], null, addStatistics.bind(this));
},
draw: function()
{
WebInspector.MemoryStatistics.prototype.draw.call(this);
for (var i = 0; i < this._counterUI.length; i++)
this._drawGraph(this._counterUI[i]);
},
/**
* @param {!CanvasRenderingContext2D} ctx
*/
_restoreImageUnderMarker: function(ctx)
{
for (var i = 0; i < this._counterUI.length; i++) {
var counterUI = this._counterUI[i];
if (!counterUI.visible)
continue;
counterUI.restoreImageUnderMarker(ctx);
}
},
/**
* @param {!CanvasRenderingContext2D} ctx
* @param {number} x
* @param {number} index
*/
_saveImageUnderMarker: function(ctx, x, index)
{
const radius = 2;
for (var i = 0; i < this._counterUI.length; i++) {
var counterUI = this._counterUI[i];
if (!counterUI.visible)
continue;
var y = counterUI.graphYValues[index];
counterUI.saveImageUnderMarker(ctx, x, y, radius);
}
},
/**
* @param {!CanvasRenderingContext2D} ctx
* @param {number} x
* @param {number} index
*/
_drawMarker: function(ctx, x, index)
{
this._saveImageUnderMarker(ctx, x, index);
const radius = 2;
for (var i = 0; i < this._counterUI.length; i++) {
var counterUI = this._counterUI[i];
if (!counterUI.visible)
continue;
var y = counterUI.graphYValues[index];
ctx.beginPath();
ctx.arc(x + 0.5, y + 0.5, radius, 0, Math.PI * 2, true);
ctx.lineWidth = 1;
ctx.fillStyle = counterUI.graphColor;
ctx.strokeStyle = counterUI.graphColor;
ctx.fill();
ctx.stroke();
ctx.closePath();
}
},
/**
* @param {!WebInspector.CounterUIBase} counterUI
*/
_drawGraph: function(counterUI)
{
var canvas = this._canvas;
var ctx = canvas.getContext("2d");
var width = canvas.width;
var height = this._clippedHeight;
var originY = this._originY;
var valueGetter = counterUI.valueGetter;
if (!this._counters.length)
return;
var maxValue;
var minValue;
for (var i = this._minimumIndex; i <= this._maximumIndex; i++) {
var value = valueGetter(this._counters[i]);
if (minValue === undefined || value < minValue)
minValue = value;
if (maxValue === undefined || value > maxValue)
maxValue = value;
}
counterUI.setRange(minValue, maxValue);
if (!counterUI.visible)
return;
var yValues = counterUI.graphYValues;
yValues.length = this._counters.length;
var maxYRange = maxValue - minValue;
var yFactor = maxYRange ? height / (maxYRange) : 1;
ctx.save();
ctx.translate(0.5, 0.5);
ctx.beginPath();
var currentY = Math.round(originY + (height - (valueGetter(this._counters[this._minimumIndex]) - minValue) * yFactor));
ctx.moveTo(0, currentY);
for (var i = this._minimumIndex; i <= this._maximumIndex; i++) {
var x = Math.round(this._counters[i].x);
ctx.lineTo(x, currentY);
currentY = Math.round(originY + (height - (valueGetter(this._counters[i]) - minValue) * yFactor));
ctx.lineTo(x, currentY);
yValues[i] = currentY;
}
ctx.lineTo(width, currentY);
ctx.lineWidth = 1;
ctx.strokeStyle = counterUI.graphColor;
ctx.stroke();
ctx.closePath();
ctx.restore();
},
_discardImageUnderMarker: function()
{
for (var i = 0; i < this._counterUI.length; i++)
this._counterUI[i].discardImageUnderMarker();
},
__proto__: WebInspector.MemoryStatistics.prototype
}