blob: 8c012baf19b81bc603e0a101611f9472392fc943 [file] [log] [blame]
/*
* 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;
}
};