| /* |
| * 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. |
| */ |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.CountersGraph = class extends UI.VBox { |
| /** |
| * @param {!Timeline.TimelineModeViewDelegate} delegate |
| */ |
| constructor(delegate) { |
| super(); |
| this.element.id = 'memory-graphs-container'; |
| |
| this._delegate = delegate; |
| this._calculator = new Timeline.CountersGraph.Calculator(); |
| |
| // Create selectors |
| this._header = new UI.HBox(); |
| this._header.element.classList.add('timeline-memory-header'); |
| this._header.show(this.element); |
| this._toolbar = new UI.Toolbar('timeline-memory-toolbar'); |
| this._header.element.appendChild(this._toolbar.element); |
| |
| this._graphsContainer = new UI.VBox(); |
| this._graphsContainer.show(this.element); |
| const canvasWidget = new UI.VBoxWithResizeCallback(this._resize.bind(this)); |
| canvasWidget.show(this._graphsContainer.element); |
| this._createCurrentValuesBar(); |
| this._canvasContainer = canvasWidget.element; |
| this._canvasContainer.id = 'memory-graphs-canvas-container'; |
| this._canvas = this._canvasContainer.createChild('canvas'); |
| this._canvas.id = 'memory-counters-graph'; |
| |
| this._canvasContainer.addEventListener('mouseover', this._onMouseMove.bind(this), true); |
| this._canvasContainer.addEventListener('mousemove', this._onMouseMove.bind(this), true); |
| this._canvasContainer.addEventListener('mouseleave', this._onMouseLeave.bind(this), true); |
| this._canvasContainer.addEventListener('click', this._onClick.bind(this), true); |
| // We create extra timeline grid here to reuse its event dividers. |
| this._timelineGrid = new PerfUI.TimelineGrid(); |
| this._canvasContainer.appendChild(this._timelineGrid.dividersElement); |
| |
| this._counters = []; |
| this._counterUI = []; |
| |
| this._countersByName = {}; |
| this._countersByName['jsHeapSizeUsed'] = this._createCounter( |
| Common.UIString('JS Heap'), Common.UIString('JS Heap: %s'), 'hsl(220, 90%, 43%)', Number.bytesToString); |
| this._countersByName['documents'] = |
| this._createCounter(Common.UIString('Documents'), Common.UIString('Documents: %s'), 'hsl(0, 90%, 43%)'); |
| this._countersByName['nodes'] = |
| this._createCounter(Common.UIString('Nodes'), Common.UIString('Nodes: %s'), 'hsl(120, 90%, 43%)'); |
| this._countersByName['jsEventListeners'] = |
| this._createCounter(Common.UIString('Listeners'), Common.UIString('Listeners: %s'), 'hsl(38, 90%, 43%)'); |
| this._gpuMemoryCounter = this._createCounter( |
| Common.UIString('GPU Memory'), Common.UIString('GPU Memory [KB]: %s'), 'hsl(300, 90%, 43%)', |
| Number.bytesToString); |
| this._countersByName['gpuMemoryUsedKB'] = this._gpuMemoryCounter; |
| } |
| |
| /** |
| * @param {?Timeline.PerformanceModel} model |
| * @param {?TimelineModel.TimelineModel.Track} track |
| */ |
| setModel(model, track) { |
| if (this._model !== model) { |
| if (this._model) { |
| this._model.removeEventListener(Timeline.PerformanceModel.Events.WindowChanged, this._onWindowChanged, this); |
| } |
| this._model = model; |
| if (this._model) { |
| this._model.addEventListener(Timeline.PerformanceModel.Events.WindowChanged, this._onWindowChanged, this); |
| } |
| } |
| this._calculator.setZeroTime(model ? model.timelineModel().minimumRecordTime() : 0); |
| for (let i = 0; i < this._counters.length; ++i) { |
| this._counters[i].reset(); |
| this._counterUI[i].reset(); |
| } |
| this.scheduleRefresh(); |
| this._track = track; |
| if (!track) { |
| return; |
| } |
| const events = track.syncEvents(); |
| for (let i = 0; i < events.length; ++i) { |
| const event = events[i]; |
| if (event.name !== TimelineModel.TimelineModel.RecordType.UpdateCounters) { |
| continue; |
| } |
| |
| const counters = event.args.data; |
| if (!counters) { |
| return; |
| } |
| for (const name in counters) { |
| const counter = this._countersByName[name]; |
| if (counter) { |
| counter.appendSample(event.startTime, counters[name]); |
| } |
| } |
| |
| const gpuMemoryLimitCounterName = 'gpuMemoryLimitKB'; |
| if (gpuMemoryLimitCounterName in counters) { |
| this._gpuMemoryCounter.setLimit(counters[gpuMemoryLimitCounterName]); |
| } |
| } |
| } |
| |
| _createCurrentValuesBar() { |
| this._currentValuesBar = this._graphsContainer.element.createChild('div'); |
| this._currentValuesBar.id = 'counter-values-bar'; |
| } |
| |
| /** |
| * @param {string} uiName |
| * @param {string} uiValueTemplate |
| * @param {string} color |
| * @param {function(number):string=} formatter |
| * @return {!Timeline.CountersGraph.Counter} |
| */ |
| _createCounter(uiName, uiValueTemplate, color, formatter) { |
| const counter = new Timeline.CountersGraph.Counter(); |
| this._counters.push(counter); |
| this._counterUI.push( |
| new Timeline.CountersGraph.CounterUI(this, uiName, uiValueTemplate, color, counter, formatter)); |
| return counter; |
| } |
| |
| /** |
| * @return {?Element} |
| */ |
| resizerElement() { |
| return this._header.element; |
| } |
| |
| _resize() { |
| const parentElement = this._canvas.parentElement; |
| this._canvas.width = parentElement.clientWidth * window.devicePixelRatio; |
| this._canvas.height = parentElement.clientHeight * window.devicePixelRatio; |
| this._calculator.setDisplayWidth(this._canvas.width); |
| this.refresh(); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onWindowChanged(event) { |
| const window = /** @type {!Timeline.PerformanceModel.Window} */ (event.data.window); |
| this._calculator.setWindow(window.left, window.right); |
| this.scheduleRefresh(); |
| } |
| |
| scheduleRefresh() { |
| UI.invokeOnceAfterBatchUpdate(this, this.refresh); |
| } |
| |
| draw() { |
| this._clear(); |
| for (const counter of this._counters) { |
| counter._calculateVisibleIndexes(this._calculator); |
| counter._calculateXValues(this._canvas.width); |
| } |
| for (const counterUI of this._counterUI) { |
| counterUI._drawGraph(this._canvas); |
| } |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _onClick(event) { |
| const x = event.x - this._canvasContainer.totalOffsetLeft(); |
| let minDistance = Infinity; |
| let bestTime; |
| for (const counterUI of this._counterUI) { |
| if (!counterUI.counter.times.length) { |
| continue; |
| } |
| const index = counterUI._recordIndexAt(x); |
| const distance = Math.abs(x * window.devicePixelRatio - counterUI.counter.x[index]); |
| if (distance < minDistance) { |
| minDistance = distance; |
| bestTime = counterUI.counter.times[index]; |
| } |
| } |
| if (bestTime !== undefined) { |
| this._delegate.selectEntryAtTime( |
| this._track.events.length ? this._track.events : this._track.asyncEvents, bestTime); |
| } |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _onMouseLeave(event) { |
| delete this._markerXPosition; |
| this._clearCurrentValueAndMarker(); |
| } |
| |
| _clearCurrentValueAndMarker() { |
| for (let i = 0; i < this._counterUI.length; i++) { |
| this._counterUI[i]._clearCurrentValueAndMarker(); |
| } |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _onMouseMove(event) { |
| const x = event.x - this._canvasContainer.totalOffsetLeft(); |
| this._markerXPosition = x; |
| this._refreshCurrentValues(); |
| } |
| |
| _refreshCurrentValues() { |
| if (this._markerXPosition === undefined) { |
| return; |
| } |
| for (let i = 0; i < this._counterUI.length; ++i) { |
| this._counterUI[i].updateCurrentValue(this._markerXPosition); |
| } |
| } |
| |
| refresh() { |
| this._timelineGrid.updateDividers(this._calculator); |
| this.draw(); |
| this._refreshCurrentValues(); |
| } |
| |
| _clear() { |
| const ctx = this._canvas.getContext('2d'); |
| ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.CountersGraph.Counter = class { |
| constructor() { |
| this.times = []; |
| this.values = []; |
| } |
| |
| /** |
| * @param {number} time |
| * @param {number} value |
| */ |
| appendSample(time, value) { |
| if (this.values.length && this.values.peekLast() === value) { |
| return; |
| } |
| this.times.push(time); |
| this.values.push(value); |
| } |
| |
| reset() { |
| this.times = []; |
| this.values = []; |
| } |
| |
| /** |
| * @param {number} value |
| */ |
| setLimit(value) { |
| this._limitValue = value; |
| } |
| |
| /** |
| * @return {!{min: number, max: number}} |
| */ |
| _calculateBounds() { |
| let maxValue; |
| let minValue; |
| for (let i = this._minimumIndex; i <= this._maximumIndex; i++) { |
| const value = this.values[i]; |
| if (minValue === undefined || value < minValue) { |
| minValue = value; |
| } |
| if (maxValue === undefined || value > maxValue) { |
| maxValue = value; |
| } |
| } |
| minValue = minValue || 0; |
| maxValue = maxValue || 1; |
| if (this._limitValue) { |
| if (maxValue > this._limitValue * 0.5) { |
| maxValue = Math.max(maxValue, this._limitValue); |
| } |
| minValue = Math.min(minValue, this._limitValue); |
| } |
| return {min: minValue, max: maxValue}; |
| } |
| |
| /** |
| * @param {!Timeline.CountersGraph.Calculator} calculator |
| */ |
| _calculateVisibleIndexes(calculator) { |
| const start = calculator.minimumBoundary(); |
| const end = calculator.maximumBoundary(); |
| |
| // Maximum index of element whose time <= start. |
| this._minimumIndex = Number.constrain(this.times.upperBound(start) - 1, 0, this.times.length - 1); |
| |
| // Minimum index of element whose time >= end. |
| this._maximumIndex = Number.constrain(this.times.lowerBound(end), 0, this.times.length - 1); |
| |
| // Current window bounds. |
| this._minTime = start; |
| this._maxTime = end; |
| } |
| |
| /** |
| * @param {number} width |
| */ |
| _calculateXValues(width) { |
| if (!this.values.length) { |
| return; |
| } |
| |
| const xFactor = width / (this._maxTime - this._minTime); |
| |
| this.x = new Array(this.values.length); |
| for (let i = this._minimumIndex + 1; i <= this._maximumIndex; i++) { |
| this.x[i] = xFactor * (this.times[i] - this._minTime); |
| } |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.CountersGraph.CounterUI = class { |
| /** |
| * @param {!Timeline.CountersGraph} countersPane |
| * @param {string} title |
| * @param {string} currentValueLabel |
| * @param {string} graphColor |
| * @param {!Timeline.CountersGraph.Counter} counter |
| * @param {(function(number): string)|undefined} formatter |
| */ |
| constructor(countersPane, title, currentValueLabel, graphColor, counter, formatter) { |
| this._countersPane = countersPane; |
| this.counter = counter; |
| this._formatter = formatter || Number.withThousandsSeparator; |
| |
| this._setting = Common.settings.createSetting('timelineCountersGraph-' + title, true); |
| this._setting.setTitle(title); |
| this._filter = new UI.ToolbarSettingCheckbox(this._setting, title); |
| this._filter.inputElement.classList.add('-theme-preserve'); |
| const color = Common.Color.parse(graphColor).setAlpha(0.5).asString(Common.Color.Format.RGBA); |
| if (color) { |
| this._filter.element.backgroundColor = color; |
| this._filter.element.borderColor = 'transparent'; |
| } |
| this._filter.inputElement.addEventListener('click', this._toggleCounterGraph.bind(this)); |
| countersPane._toolbar.appendToolbarItem(this._filter); |
| this._range = this._filter.element.createChild('span', 'range'); |
| |
| this._value = countersPane._currentValuesBar.createChild('span', 'memory-counter-value'); |
| this._value.style.color = graphColor; |
| this.graphColor = graphColor; |
| this.limitColor = Common.Color.parse(graphColor).setAlpha(0.3).asString(Common.Color.Format.RGBA); |
| this.graphYValues = []; |
| this._verticalPadding = 10; |
| |
| this._currentValueLabel = currentValueLabel; |
| this._marker = countersPane._canvasContainer.createChild('div', 'memory-counter-marker'); |
| this._marker.style.backgroundColor = graphColor; |
| this._clearCurrentValueAndMarker(); |
| } |
| |
| reset() { |
| this._range.textContent = ''; |
| } |
| |
| /** |
| * @param {number} minValue |
| * @param {number} maxValue |
| */ |
| setRange(minValue, maxValue) { |
| const min = this._formatter(minValue); |
| const max = this._formatter(maxValue); |
| this._range.textContent = Common.UIString('[%s\xa0\u2013\xa0%s]', min, max); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _toggleCounterGraph(event) { |
| this._value.classList.toggle('hidden', !this._filter.checked()); |
| this._countersPane.refresh(); |
| } |
| |
| /** |
| * @param {number} x |
| * @return {number} |
| */ |
| _recordIndexAt(x) { |
| return this.counter.x.upperBound( |
| x * window.devicePixelRatio, null, this.counter._minimumIndex + 1, this.counter._maximumIndex + 1) - |
| 1; |
| } |
| |
| /** |
| * @param {number} x |
| */ |
| updateCurrentValue(x) { |
| if (!this.visible() || !this.counter.values.length || !this.counter.x) { |
| return; |
| } |
| const index = this._recordIndexAt(x); |
| const value = Number.withThousandsSeparator(this.counter.values[index]); |
| this._value.textContent = Common.UIString(this._currentValueLabel, value); |
| const y = this.graphYValues[index] / window.devicePixelRatio; |
| this._marker.style.left = x + 'px'; |
| this._marker.style.top = y + 'px'; |
| this._marker.classList.remove('hidden'); |
| } |
| |
| _clearCurrentValueAndMarker() { |
| this._value.textContent = ''; |
| this._marker.classList.add('hidden'); |
| } |
| |
| /** |
| * @param {!HTMLCanvasElement} canvas |
| */ |
| _drawGraph(canvas) { |
| const ctx = canvas.getContext('2d'); |
| const width = canvas.width; |
| const height = canvas.height - 2 * this._verticalPadding; |
| if (height <= 0) { |
| this.graphYValues = []; |
| return; |
| } |
| const originY = this._verticalPadding; |
| const counter = this.counter; |
| const values = counter.values; |
| |
| if (!values.length) { |
| return; |
| } |
| |
| const bounds = counter._calculateBounds(); |
| const minValue = bounds.min; |
| const maxValue = bounds.max; |
| this.setRange(minValue, maxValue); |
| |
| if (!this.visible()) { |
| return; |
| } |
| |
| const yValues = this.graphYValues; |
| const maxYRange = maxValue - minValue; |
| const yFactor = maxYRange ? height / (maxYRange) : 1; |
| |
| ctx.save(); |
| ctx.lineWidth = window.devicePixelRatio; |
| if (ctx.lineWidth % 2) { |
| ctx.translate(0.5, 0.5); |
| } |
| ctx.beginPath(); |
| let value = values[counter._minimumIndex]; |
| let currentY = Math.round(originY + height - (value - minValue) * yFactor); |
| ctx.moveTo(0, currentY); |
| let i = counter._minimumIndex; |
| for (; i <= counter._maximumIndex; i++) { |
| const x = Math.round(counter.x[i]); |
| ctx.lineTo(x, currentY); |
| const currentValue = values[i]; |
| if (typeof currentValue !== 'undefined') { |
| value = currentValue; |
| } |
| currentY = Math.round(originY + height - (value - minValue) * yFactor); |
| ctx.lineTo(x, currentY); |
| yValues[i] = currentY; |
| } |
| yValues.length = i; |
| ctx.lineTo(width, currentY); |
| ctx.strokeStyle = this.graphColor; |
| ctx.stroke(); |
| if (counter._limitValue) { |
| const limitLineY = Math.round(originY + height - (counter._limitValue - minValue) * yFactor); |
| ctx.moveTo(0, limitLineY); |
| ctx.lineTo(width, limitLineY); |
| ctx.strokeStyle = this.limitColor; |
| ctx.stroke(); |
| } |
| ctx.closePath(); |
| ctx.restore(); |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| visible() { |
| return this._filter.checked(); |
| } |
| }; |
| |
| /** |
| * @implements {PerfUI.TimelineGrid.Calculator} |
| * @unrestricted |
| */ |
| Timeline.CountersGraph.Calculator = class { |
| /** |
| * @param {number} time |
| */ |
| setZeroTime(time) { |
| this._zeroTime = time; |
| } |
| |
| /** |
| * @override |
| * @param {number} time |
| * @return {number} |
| */ |
| computePosition(time) { |
| return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea; |
| } |
| |
| setWindow(minimumBoundary, maximumBoundary) { |
| this._minimumBoundary = minimumBoundary; |
| this._maximumBoundary = maximumBoundary; |
| } |
| |
| /** |
| * @param {number} clientWidth |
| */ |
| setDisplayWidth(clientWidth) { |
| this._workingArea = clientWidth; |
| } |
| |
| /** |
| * @override |
| * @param {number} value |
| * @param {number=} precision |
| * @return {string} |
| */ |
| formatValue(value, precision) { |
| return Number.preciseMillisToString(value - this.zeroTime(), precision); |
| } |
| |
| /** |
| * @override |
| * @return {number} |
| */ |
| maximumBoundary() { |
| return this._maximumBoundary; |
| } |
| |
| /** |
| * @override |
| * @return {number} |
| */ |
| minimumBoundary() { |
| return this._minimumBoundary; |
| } |
| |
| /** |
| * @override |
| * @return {number} |
| */ |
| zeroTime() { |
| return this._zeroTime; |
| } |
| |
| /** |
| * @override |
| * @return {number} |
| */ |
| boundarySpan() { |
| return this._maximumBoundary - this._minimumBoundary; |
| } |
| }; |