/* * 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.} */ _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.} 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 }