| /* |
| * Copyright (C) 2012 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. |
| */ |
| |
| /** |
| * @param {WebInspector.TimelinePanel} timelinePanel |
| * @param {WebInspector.TimelineModel} model |
| * @param {number} sidebarWidth |
| * @constructor |
| */ |
| WebInspector.MemoryStatistics = function(timelinePanel, model, sidebarWidth) |
| { |
| this._timelinePanel = timelinePanel; |
| this._counters = []; |
| |
| model.addEventListener(WebInspector.TimelineModel.Events.RecordAdded, this._onRecordAdded, this); |
| model.addEventListener(WebInspector.TimelineModel.Events.RecordsCleared, this._onRecordsCleared, this); |
| |
| this._containerAnchor = timelinePanel.element.lastChild; |
| this._memorySplitView = new WebInspector.SplitView(WebInspector.SplitView.SidebarPosition.Left, undefined, sidebarWidth); |
| this._memorySplitView.sidebarElement.addStyleClass("sidebar"); |
| this._memorySplitView.element.id = "memory-graphs-container"; |
| |
| this._memorySplitView.addEventListener(WebInspector.SplitView.EventTypes.Resized, this._sidebarResized.bind(this)); |
| |
| this._canvasContainer = this._memorySplitView.mainElement; |
| this._canvasContainer.id = "memory-graphs-canvas-container"; |
| this._currentValuesBar = this._canvasContainer.createChild("div"); |
| this._currentValuesBar.id = "counter-values-bar"; |
| this._canvas = this._canvasContainer.createChild("canvas"); |
| this._canvas.id = "memory-counters-graph"; |
| this._lastMarkerXPosition = 0; |
| |
| this._canvas.addEventListener("mouseover", this._onMouseOver.bind(this), true); |
| this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this), true); |
| this._canvas.addEventListener("mouseout", this._onMouseOut.bind(this), true); |
| this._canvas.addEventListener("click", this._onClick.bind(this), true); |
| // We create extra timeline grid here to reuse its event dividers. |
| this._timelineGrid = new WebInspector.TimelineGrid(); |
| this._canvasContainer.appendChild(this._timelineGrid.dividersElement); |
| |
| // Populate sidebar |
| this._memorySplitView.sidebarElement.createChild("div", "sidebar-tree sidebar-tree-section").textContent = WebInspector.UIString("COUNTERS"); |
| function getDocumentCount(entry) |
| { |
| return entry.documentCount; |
| } |
| function getNodeCount(entry) |
| { |
| return entry.nodeCount; |
| } |
| function getListenerCount(entry) |
| { |
| return entry.listenerCount; |
| } |
| this._counterUI = [ |
| new WebInspector.CounterUI(this, "Document Count", "Documents: %d", [100,0,0], getDocumentCount), |
| new WebInspector.CounterUI(this, "DOM Node Count", "Nodes: %d", [0,100,0], getNodeCount), |
| new WebInspector.CounterUI(this, "Event Listener Count", "Listeners: %d", [0,0,100], getListenerCount) |
| ]; |
| |
| TimelineAgent.setIncludeMemoryDetails(true); |
| } |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.Object} |
| */ |
| WebInspector.SwatchCheckbox = function(title, color) |
| { |
| this.element = document.createElement("div"); |
| this._swatch = this.element.createChild("div", "swatch"); |
| this.element.createChild("span", "title").textContent = title; |
| this._color = color; |
| this.checked = true; |
| |
| this.element.addEventListener("click", this._toggleCheckbox.bind(this), true); |
| } |
| |
| WebInspector.SwatchCheckbox.Events = { |
| Changed: "Changed" |
| } |
| |
| WebInspector.SwatchCheckbox.prototype = { |
| get checked() |
| { |
| return this._checked; |
| }, |
| |
| set checked(v) |
| { |
| this._checked = v; |
| if (this._checked) |
| this._swatch.style.backgroundColor = this._color; |
| else |
| this._swatch.style.backgroundColor = ""; |
| }, |
| |
| _toggleCheckbox: function(event) |
| { |
| this.checked = !this.checked; |
| this.dispatchEventToListeners(WebInspector.SwatchCheckbox.Events.Changed); |
| } |
| } |
| |
| WebInspector.SwatchCheckbox.prototype.__proto__ = WebInspector.Object.prototype; |
| |
| /** |
| * @constructor |
| */ |
| WebInspector.CounterUI = function(memoryCountersPane, title, currentValueLabel, rgb, valueGetter) |
| { |
| this._memoryCountersPane = memoryCountersPane; |
| this.valueGetter = valueGetter; |
| var container = memoryCountersPane._memorySplitView.sidebarElement.createChild("div", "memory-counter-sidebar-info"); |
| var swatchColor = "rgb(" + rgb.join(",") + ")"; |
| this._swatch = new WebInspector.SwatchCheckbox(WebInspector.UIString(title), swatchColor); |
| this._swatch.addEventListener(WebInspector.SwatchCheckbox.Events.Changed, this._toggleCounterGraph.bind(this)); |
| container.appendChild(this._swatch.element); |
| this._range = this._swatch.element.createChild("span"); |
| |
| this._value = memoryCountersPane._currentValuesBar.createChild("span", "memory-counter-value"); |
| this._value.style.color = swatchColor; |
| this._currentValueLabel = currentValueLabel; |
| |
| this.graphColor = "rgba(" + rgb.join(",") + ",0.8)"; |
| this.graphYValues = []; |
| } |
| |
| WebInspector.CounterUI.prototype = { |
| _toggleCounterGraph: function(event) |
| { |
| if (this._swatch.checked) |
| this._value.removeStyleClass("hidden"); |
| else |
| this._value.addStyleClass("hidden"); |
| this._memoryCountersPane.refresh(); |
| }, |
| |
| 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); |
| }, |
| |
| get visible() |
| { |
| return this._swatch.checked; |
| }, |
| |
| 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 }; |
| }, |
| |
| 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; |
| } |
| } |
| |
| |
| WebInspector.MemoryStatistics.prototype = { |
| _onRecordsCleared: function() |
| { |
| this._counters = []; |
| }, |
| |
| setMainTimelineGrid: function(timelineGrid) |
| { |
| this._mainTimelineGrid = timelineGrid; |
| }, |
| |
| setTopPosition: function(top) |
| { |
| this._memorySplitView.element.style.top = top + "px"; |
| this._updateSize(); |
| }, |
| |
| setSidebarWidth: function(width) |
| { |
| if (this._ignoreSidebarResize) |
| return; |
| this._ignoreSidebarResize = true; |
| this._memorySplitView.setSidebarWidth(width); |
| this._ignoreSidebarResize = false; |
| }, |
| |
| _sidebarResized: function(event) |
| { |
| if (this._ignoreSidebarResize) |
| return; |
| this._ignoreSidebarResize = true; |
| this._timelinePanel.splitView.setSidebarWidth(event.data); |
| this._ignoreSidebarResize = false; |
| }, |
| |
| _updateSize: function() |
| { |
| var width = this._mainTimelineGrid.dividersElement.offsetWidth + 1; |
| this._canvasContainer.style.width = width + "px"; |
| |
| var height = this._canvasContainer.offsetHeight - this._currentValuesBar.offsetHeight; |
| this._canvas.width = width; |
| this._canvas.height = height; |
| }, |
| |
| _onRecordAdded: function(event) |
| { |
| var counters = event.data["counters"]; |
| if (!counters) |
| return; |
| this._counters.push({ |
| time: event.data.endTime || event.data.startTime, |
| documentCount: counters["documents"], |
| nodeCount: counters["nodes"], |
| listenerCount: counters["jsEventListeners"] |
| }); |
| }, |
| |
| _draw: function() |
| { |
| this._calculateVisibleIndexes(); |
| this._calculateXValues(); |
| this._clear(); |
| |
| this._setVerticalClip(10, this._canvas.height - 20); |
| for (var i = 0; i < this._counterUI.length; i++) |
| this._drawGraph(this._counterUI[i]); |
| }, |
| |
| _calculateVisibleIndexes: function() |
| { |
| var calculator = this._timelinePanel.calculator; |
| var start = calculator.minimumBoundary * 1000; |
| var end = calculator.maximumBoundary * 1000; |
| var firstIndex = 0; |
| var lastIndex = this._counters.length - 1; |
| for (var i = 0; i < this._counters.length; i++) { |
| var time = this._counters[i].time; |
| if (time <= start) { |
| firstIndex = i; |
| } else { |
| if (end < time) |
| break; |
| lastIndex = i; |
| } |
| } |
| // Maximum index of element whose time <= start. |
| this._minimumIndex = firstIndex; |
| |
| // Maximum index of element whose time <= end. |
| this._maximumIndex = lastIndex; |
| |
| // Current window bounds. |
| this._minTime = start; |
| this._maxTime = end; |
| }, |
| |
| _onClick: function(event) |
| { |
| var x = event.x - event.target.offsetParent.offsetLeft |
| var i = this._recordIndexAt(x); |
| var counter = this._counters[i]; |
| this._timelinePanel.revealRecordAt(counter.time / 1000); |
| }, |
| |
| _onMouseOut: function(event) |
| { |
| delete this._markerXPosition; |
| |
| var ctx = this._canvas.getContext("2d"); |
| for (var i = 0; i < this._counterUI.length; i++) |
| this._counterUI[i].clearCurrentValueAndMarker(ctx); |
| }, |
| |
| _onMouseOver: function(event) |
| { |
| this._onMouseMove(event); |
| }, |
| |
| _onMouseMove: function(event) |
| { |
| var x = event.x - event.target.offsetParent.offsetLeft |
| this._markerXPosition = x; |
| this._refreshCurrentValues(); |
| }, |
| |
| _refreshCurrentValues: function() |
| { |
| if (!this._counters.length) |
| return; |
| if (this._markerXPosition === undefined) |
| return; |
| var i = this._recordIndexAt(this._markerXPosition); |
| |
| for (var j = 0; j < this._counterUI.length; j++) |
| this._counterUI[j].updateCurrentValue(this._counters[i]); |
| |
| this._highlightCurrentPositionOnGraphs(this._markerXPosition, i); |
| }, |
| |
| _recordIndexAt: function(x) |
| { |
| var i; |
| for (i = this._minimumIndex + 1; i <= this._maximumIndex; i++) { |
| var statX = this._counters[i].x; |
| if (x < statX) |
| break; |
| } |
| i--; |
| return i; |
| }, |
| |
| _highlightCurrentPositionOnGraphs: function(x, index) |
| { |
| var ctx = this._canvas.getContext("2d"); |
| for (var i = 0; i < this._counterUI.length; i++) { |
| var counterUI = this._counterUI[i]; |
| if (!counterUI.visible) |
| continue; |
| counterUI.restoreImageUnderMarker(ctx); |
| } |
| |
| 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); |
| } |
| |
| 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, y, radius, 0, Math.PI*2, true); |
| ctx.lineWidth = 1; |
| ctx.fillStyle = counterUI.graphColor; |
| ctx.strokeStyle = counterUI.graphColor; |
| ctx.fill(); |
| ctx.stroke(); |
| ctx.closePath(); |
| } |
| }, |
| |
| visible: function() |
| { |
| return this._memorySplitView.isShowing(); |
| }, |
| |
| show: function() |
| { |
| var anchor = /** @type {Element|null} */ this._containerAnchor.nextSibling; |
| this._memorySplitView.show(this._timelinePanel.element, anchor); |
| this._updateSize(); |
| this._refreshDividers(); |
| setTimeout(this._draw.bind(this), 0); |
| }, |
| |
| refresh: function() |
| { |
| this._updateSize(); |
| this._refreshDividers(); |
| this._draw(); |
| this._refreshCurrentValues(); |
| }, |
| |
| hide: function() |
| { |
| this._memorySplitView.detach(); |
| }, |
| |
| _refreshDividers: function() |
| { |
| this._timelineGrid.updateDividers(this._timelinePanel.calculator); |
| }, |
| |
| _setVerticalClip: function(originY, height) |
| { |
| this._originY = originY; |
| this._clippedHeight = height; |
| }, |
| |
| _calculateXValues: function() |
| { |
| if (!this._counters.length) |
| return; |
| |
| var width = this._canvas.width; |
| var xFactor = width / (this._maxTime - this._minTime); |
| |
| this._counters[this._minimumIndex].x = 0; |
| for (var i = this._minimumIndex + 1; i < this._maximumIndex; i++) |
| this._counters[i].x = xFactor * (this._counters[i].time - this._minTime); |
| this._counters[this._maximumIndex].x = width; |
| }, |
| |
| _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.beginPath(); |
| var currentY = originY + (height - (valueGetter(this._counters[this._minimumIndex])- minValue) * yFactor); |
| ctx.moveTo(0, currentY); |
| for (var i = this._minimumIndex; i <= this._maximumIndex; i++) { |
| var x = this._counters[i].x; |
| ctx.lineTo(x, currentY); |
| currentY = 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(); |
| }, |
| |
| _clear: function() { |
| var ctx = this._canvas.getContext("2d"); |
| ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); |
| for (var i = 0; i < this._counterUI.length; i++) |
| this._counterUI[i].discardImageUnderMarker(); |
| } |
| } |
| |