blob: 66531787c38bf2f1b81eab3e1115ca398471afa3 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @unrestricted
*/
Profiler.HeapTimelineOverview = class extends UI.VBox {
constructor() {
super();
this.element.id = 'heap-recording-view';
this.element.classList.add('heap-tracking-overview');
this._overviewCalculator = new Profiler.HeapTimelineOverview.OverviewCalculator();
this._overviewContainer = this.element.createChild('div', 'heap-overview-container');
this._overviewGrid = new PerfUI.OverviewGrid('heap-recording', this._overviewCalculator);
this._overviewGrid.element.classList.add('fill');
this._overviewCanvas = this._overviewContainer.createChild('canvas', 'heap-recording-overview-canvas');
this._overviewContainer.appendChild(this._overviewGrid.element);
this._overviewGrid.addEventListener(PerfUI.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
this._windowLeft = 0.0;
this._windowRight = 1.0;
this._overviewGrid.setWindow(this._windowLeft, this._windowRight);
this._yScale = new Profiler.HeapTimelineOverview.SmoothScale();
this._xScale = new Profiler.HeapTimelineOverview.SmoothScale();
this._profileSamples = new Profiler.HeapTimelineOverview.Samples();
}
start() {
this._running = true;
const drawFrame = () => {
this.update();
if (this._running) {
this.element.window().requestAnimationFrame(drawFrame);
}
};
drawFrame();
}
stop() {
this._running = false;
}
/**
* @param {!Profiler.HeapTimelineOverview.Samples} samples
*/
setSamples(samples) {
this._profileSamples = samples;
if (!this._running) {
this.update();
}
}
/**
* @param {number} width
* @param {number} height
*/
_drawOverviewCanvas(width, height) {
if (!this._profileSamples) {
return;
}
const profileSamples = this._profileSamples;
const sizes = profileSamples.sizes;
const topSizes = profileSamples.max;
const timestamps = profileSamples.timestamps;
const startTime = timestamps[0];
const scaleFactor = this._xScale.nextScale(width / profileSamples.totalTime);
let maxSize = 0;
/**
* @param {!Array.<number>} sizes
* @param {function(number, number):void} callback
*/
function aggregateAndCall(sizes, callback) {
let size = 0;
let currentX = 0;
for (let i = 1; i < timestamps.length; ++i) {
const x = Math.floor((timestamps[i] - startTime) * scaleFactor);
if (x !== currentX) {
if (size) {
callback(currentX, size);
}
size = 0;
currentX = x;
}
size += sizes[i];
}
callback(currentX, size);
}
/**
* @param {number} x
* @param {number} size
*/
function maxSizeCallback(x, size) {
maxSize = Math.max(maxSize, size);
}
aggregateAndCall(sizes, maxSizeCallback);
const yScaleFactor = this._yScale.nextScale(maxSize ? height / (maxSize * 1.1) : 0.0);
this._overviewCanvas.width = width * window.devicePixelRatio;
this._overviewCanvas.height = height * window.devicePixelRatio;
this._overviewCanvas.style.width = width + 'px';
this._overviewCanvas.style.height = height + 'px';
const context = this._overviewCanvas.getContext('2d');
context.scale(window.devicePixelRatio, window.devicePixelRatio);
if (this._running) {
context.beginPath();
context.lineWidth = 2;
context.strokeStyle = 'rgba(192, 192, 192, 0.6)';
const currentX = (Date.now() - startTime) * scaleFactor;
context.moveTo(currentX, height - 1);
context.lineTo(currentX, 0);
context.stroke();
context.closePath();
}
let gridY;
let gridValue;
const gridLabelHeight = 14;
if (yScaleFactor) {
const maxGridValue = (height - gridLabelHeight) / yScaleFactor;
// The round value calculation is a bit tricky, because
// it has a form k*10^n*1024^m, where k=[1,5], n=[0..3], m is an integer,
// e.g. a round value 10KB is 10240 bytes.
gridValue = Math.pow(1024, Math.floor(Math.log(maxGridValue) / Math.log(1024)));
gridValue *= Math.pow(10, Math.floor(Math.log(maxGridValue / gridValue) / Math.LN10));
if (gridValue * 5 <= maxGridValue) {
gridValue *= 5;
}
gridY = Math.round(height - gridValue * yScaleFactor - 0.5) + 0.5;
context.beginPath();
context.lineWidth = 1;
context.strokeStyle = 'rgba(0, 0, 0, 0.2)';
context.moveTo(0, gridY);
context.lineTo(width, gridY);
context.stroke();
context.closePath();
}
/**
* @param {number} x
* @param {number} size
*/
function drawBarCallback(x, size) {
context.moveTo(x, height - 1);
context.lineTo(x, Math.round(height - size * yScaleFactor - 1));
}
context.beginPath();
context.lineWidth = 2;
context.strokeStyle = 'rgba(192, 192, 192, 0.6)';
aggregateAndCall(topSizes, drawBarCallback);
context.stroke();
context.closePath();
context.beginPath();
context.lineWidth = 2;
context.strokeStyle = 'rgba(0, 0, 192, 0.8)';
aggregateAndCall(sizes, drawBarCallback);
context.stroke();
context.closePath();
if (gridValue) {
const label = Number.bytesToString(gridValue);
const labelPadding = 4;
const labelX = 0;
const labelY = gridY - 0.5;
const labelWidth = 2 * labelPadding + context.measureText(label).width;
context.beginPath();
context.textBaseline = 'bottom';
context.font = '10px ' + window.getComputedStyle(this.element, null).getPropertyValue('font-family');
context.fillStyle = 'rgba(255, 255, 255, 0.75)';
context.fillRect(labelX, labelY - gridLabelHeight, labelWidth, gridLabelHeight);
context.fillStyle = 'rgb(64, 64, 64)';
context.fillText(label, labelX + labelPadding, labelY);
context.fill();
context.closePath();
}
}
/**
* @override
*/
onResize() {
this._updateOverviewCanvas = true;
this._scheduleUpdate();
}
_onWindowChanged() {
if (!this._updateGridTimerId) {
this._updateGridTimerId = setTimeout(this.updateGrid.bind(this), 10);
}
}
_scheduleUpdate() {
if (this._updateTimerId) {
return;
}
this._updateTimerId = setTimeout(this.update.bind(this), 10);
}
_updateBoundaries() {
this._windowLeft = this._overviewGrid.windowLeft();
this._windowRight = this._overviewGrid.windowRight();
this._windowWidth = this._windowRight - this._windowLeft;
}
update() {
this._updateTimerId = null;
if (!this.isShowing()) {
return;
}
this._updateBoundaries();
this._overviewCalculator._updateBoundaries(this);
this._overviewGrid.updateDividers(this._overviewCalculator);
this._drawOverviewCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - 20);
}
updateGrid() {
this._updateGridTimerId = 0;
this._updateBoundaries();
const ids = this._profileSamples.ids;
if (!ids.length) {
return;
}
const timestamps = this._profileSamples.timestamps;
const sizes = this._profileSamples.sizes;
const startTime = timestamps[0];
const totalTime = this._profileSamples.totalTime;
const timeLeft = startTime + totalTime * this._windowLeft;
const timeRight = startTime + totalTime * this._windowRight;
const minIndex = timestamps.lowerBound(timeLeft);
const maxIndex = timestamps.upperBound(timeRight);
let size = 0;
for (let i = minIndex; i <= maxIndex; ++i) {
size += sizes[i];
}
const minId = minIndex > 0 ? ids[minIndex - 1] : 0;
const maxId = maxIndex < ids.length ? ids[maxIndex] : Infinity;
this.dispatchEventToListeners(Profiler.HeapTimelineOverview.IdsRangeChanged, {minId, maxId, size});
}
};
Profiler.HeapTimelineOverview.IdsRangeChanged = Symbol('IdsRangeChanged');
Profiler.HeapTimelineOverview.SmoothScale = class {
constructor() {
this._lastUpdate = 0;
this._currentScale = 0.0;
}
/**
* @param {number} target
* @return {number}
*/
nextScale(target) {
target = target || this._currentScale;
if (this._currentScale) {
const now = Date.now();
const timeDeltaMs = now - this._lastUpdate;
this._lastUpdate = now;
const maxChangePerSec = 20;
const maxChangePerDelta = Math.pow(maxChangePerSec, timeDeltaMs / 1000);
const scaleChange = target / this._currentScale;
this._currentScale *= Number.constrain(scaleChange, 1 / maxChangePerDelta, maxChangePerDelta);
} else {
this._currentScale = target;
}
return this._currentScale;
}
};
/**
* @unrestricted
*/
Profiler.HeapTimelineOverview.Samples = class {
constructor() {
/** @type {!Array<number>} */
this.sizes = [];
/** @type {!Array<number>} */
this.ids = [];
/** @type {!Array<number>} */
this.timestamps = [];
/** @type {!Array<number>} */
this.max = [];
/** @type {number} */
this.totalTime = 30000;
}
};
/**
* @implements {PerfUI.TimelineGrid.Calculator}
* @unrestricted
*/
Profiler.HeapTimelineOverview.OverviewCalculator = class {
/**
* @param {!Profiler.HeapTimelineOverview} chart
*/
_updateBoundaries(chart) {
this._minimumBoundaries = 0;
this._maximumBoundaries = chart._profileSamples.totalTime;
this._xScaleFactor = chart._overviewContainer.clientWidth / this._maximumBoundaries;
}
/**
* @override
* @param {number} time
* @return {number}
*/
computePosition(time) {
return (time - this._minimumBoundaries) * this._xScaleFactor;
}
/**
* @override
* @param {number} value
* @param {number=} precision
* @return {string}
*/
formatValue(value, precision) {
return Number.secondsToString(value / 1000, !!precision);
}
/**
* @override
* @return {number}
*/
maximumBoundary() {
return this._maximumBoundaries;
}
/**
* @override
* @return {number}
*/
minimumBoundary() {
return this._minimumBoundaries;
}
/**
* @override
* @return {number}
*/
zeroTime() {
return this._minimumBoundaries;
}
/**
* @override
* @return {number}
*/
boundarySpan() {
return this._maximumBoundaries - this._minimumBoundaries;
}
};