blob: 113e9e0108ca6b3b23f21f72a0c428966d680973 [file] [log] [blame]
/*
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
* Copyright (C) 2009 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:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. 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.
* 3. Neither the name of Apple Computer, Inc. ("Apple") 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 APPLE AND ITS 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 APPLE OR ITS 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
*/
PerfUI.TimelineGrid = class {
constructor() {
this.element = createElement('div');
UI.appendStyle(this.element, 'perf_ui/timelineGrid.css');
this._dividersElement = this.element.createChild('div', 'resources-dividers');
this._gridHeaderElement = createElement('div');
this._gridHeaderElement.classList.add('timeline-grid-header');
this._eventDividersElement = this._gridHeaderElement.createChild('div', 'resources-event-dividers');
this._dividersLabelBarElement = this._gridHeaderElement.createChild('div', 'resources-dividers-label-bar');
this.element.appendChild(this._gridHeaderElement);
}
/**
* @param {!PerfUI.TimelineGrid.Calculator} calculator
* @param {number=} freeZoneAtLeft
* @return {!PerfUI.TimelineGrid.DividersData}
*/
static calculateGridOffsets(calculator, freeZoneAtLeft) {
/** @const */ const minGridSlicePx = 64; // minimal distance between grid lines.
const clientWidth = calculator.computePosition(calculator.maximumBoundary());
let dividersCount = clientWidth / minGridSlicePx;
let gridSliceTime = calculator.boundarySpan() / dividersCount;
const pixelsPerTime = clientWidth / calculator.boundarySpan();
// Align gridSliceTime to a nearest round value.
// We allow spans that fit into the formula: span = (1|2|5)x10^n,
// e.g.: ... .1 .2 .5 1 2 5 10 20 50 ...
// After a span has been chosen make grid lines at multiples of the span.
const logGridSliceTime = Math.ceil(Math.log(gridSliceTime) / Math.LN10);
gridSliceTime = Math.pow(10, logGridSliceTime);
if (gridSliceTime * pixelsPerTime >= 5 * minGridSlicePx) {
gridSliceTime = gridSliceTime / 5;
}
if (gridSliceTime * pixelsPerTime >= 2 * minGridSlicePx) {
gridSliceTime = gridSliceTime / 2;
}
const firstDividerTime =
Math.ceil((calculator.minimumBoundary() - calculator.zeroTime()) / gridSliceTime) * gridSliceTime +
calculator.zeroTime();
let lastDividerTime = calculator.maximumBoundary();
// Add some extra space past the right boundary as the rightmost divider label text
// may be partially shown rather than just pop up when a new rightmost divider gets into the view.
lastDividerTime += minGridSlicePx / pixelsPerTime;
dividersCount = Math.ceil((lastDividerTime - firstDividerTime) / gridSliceTime);
if (!gridSliceTime) {
dividersCount = 0;
}
const offsets = [];
for (let i = 0; i < dividersCount; ++i) {
const time = firstDividerTime + gridSliceTime * i;
if (calculator.computePosition(time) < freeZoneAtLeft) {
continue;
}
offsets.push({position: Math.floor(calculator.computePosition(time)), time: time});
}
return {offsets: offsets, precision: Math.max(0, -Math.floor(Math.log(gridSliceTime * 1.01) / Math.LN10))};
}
/**
* @param {!CanvasRenderingContext2D} context
* @param {!PerfUI.TimelineGrid.DividersData} dividersData
*/
static drawCanvasGrid(context, dividersData) {
context.save();
context.scale(window.devicePixelRatio, window.devicePixelRatio);
const height = Math.floor(context.canvas.height / window.devicePixelRatio);
context.strokeStyle = UI.themeSupport.patchColorText('rgba(0, 0, 0, 0.1)', UI.ThemeSupport.ColorUsage.Foreground);
context.lineWidth = 1;
context.translate(0.5, 0.5);
context.beginPath();
for (const offsetInfo of dividersData.offsets) {
context.moveTo(offsetInfo.position, 0);
context.lineTo(offsetInfo.position, height);
}
context.stroke();
context.restore();
}
/**
* @param {!CanvasRenderingContext2D} context
* @param {!PerfUI.TimelineGrid.DividersData} dividersData
* @param {function(number):string} formatTimeFunction
* @param {number} paddingTop
* @param {number} headerHeight
* @param {number=} freeZoneAtLeft
*/
static drawCanvasHeaders(context, dividersData, formatTimeFunction, paddingTop, headerHeight, freeZoneAtLeft) {
context.save();
context.scale(window.devicePixelRatio, window.devicePixelRatio);
const width = Math.ceil(context.canvas.width / window.devicePixelRatio);
context.beginPath();
context.fillStyle =
UI.themeSupport.patchColorText('rgba(255, 255, 255, 0.5)', UI.ThemeSupport.ColorUsage.Background);
context.fillRect(0, 0, width, headerHeight);
context.fillStyle = UI.themeSupport.patchColorText('#333', UI.ThemeSupport.ColorUsage.Foreground);
context.textBaseline = 'hanging';
context.font = '11px ' + Host.fontFamily();
const paddingRight = 4;
for (const offsetInfo of dividersData.offsets) {
const text = formatTimeFunction(offsetInfo.time);
const textWidth = context.measureText(text).width;
const textPosition = offsetInfo.position - textWidth - paddingRight;
if (!freeZoneAtLeft || freeZoneAtLeft < textPosition) {
context.fillText(text, textPosition, paddingTop);
}
}
context.restore();
}
get dividersElement() {
return this._dividersElement;
}
get dividersLabelBarElement() {
return this._dividersLabelBarElement;
}
removeDividers() {
this._dividersElement.removeChildren();
this._dividersLabelBarElement.removeChildren();
}
/**
* @param {!PerfUI.TimelineGrid.Calculator} calculator
* @param {number=} freeZoneAtLeft
* @return {boolean}
*/
updateDividers(calculator, freeZoneAtLeft) {
const dividersData = PerfUI.TimelineGrid.calculateGridOffsets(calculator, freeZoneAtLeft);
const dividerOffsets = dividersData.offsets;
const precision = dividersData.precision;
const dividersElementClientWidth = this._dividersElement.clientWidth;
// Reuse divider elements and labels.
let divider = /** @type {?Element} */ (this._dividersElement.firstChild);
let dividerLabelBar = /** @type {?Element} */ (this._dividersLabelBarElement.firstChild);
for (let i = 0; i < dividerOffsets.length; ++i) {
if (!divider) {
divider = createElement('div');
divider.className = 'resources-divider';
this._dividersElement.appendChild(divider);
dividerLabelBar = createElement('div');
dividerLabelBar.className = 'resources-divider';
const label = createElement('div');
label.className = 'resources-divider-label';
dividerLabelBar._labelElement = label;
dividerLabelBar.appendChild(label);
this._dividersLabelBarElement.appendChild(dividerLabelBar);
}
const time = dividerOffsets[i].time;
const position = dividerOffsets[i].position;
dividerLabelBar._labelElement.textContent = calculator.formatValue(time, precision);
const percentLeft = 100 * position / dividersElementClientWidth;
divider.style.left = percentLeft + '%';
dividerLabelBar.style.left = percentLeft + '%';
divider = /** @type {?Element} */ (divider.nextSibling);
dividerLabelBar = /** @type {?Element} */ (dividerLabelBar.nextSibling);
}
// Remove extras.
while (divider) {
const nextDivider = divider.nextSibling;
this._dividersElement.removeChild(divider);
divider = nextDivider;
}
while (dividerLabelBar) {
const nextDivider = dividerLabelBar.nextSibling;
this._dividersLabelBarElement.removeChild(dividerLabelBar);
dividerLabelBar = nextDivider;
}
return true;
}
/**
* @param {!Element} divider
*/
addEventDivider(divider) {
this._eventDividersElement.appendChild(divider);
}
/**
* @param {!Array.<!Element>} dividers
*/
addEventDividers(dividers) {
this._gridHeaderElement.removeChild(this._eventDividersElement);
for (const divider of dividers) {
this._eventDividersElement.appendChild(divider);
}
this._gridHeaderElement.appendChild(this._eventDividersElement);
}
removeEventDividers() {
this._eventDividersElement.removeChildren();
}
hideEventDividers() {
this._eventDividersElement.classList.add('hidden');
}
showEventDividers() {
this._eventDividersElement.classList.remove('hidden');
}
hideDividers() {
this._dividersElement.classList.add('hidden');
}
showDividers() {
this._dividersElement.classList.remove('hidden');
}
/**
* @param {number} scrollTop
*/
setScrollTop(scrollTop) {
this._dividersLabelBarElement.style.top = scrollTop + 'px';
this._eventDividersElement.style.top = scrollTop + 'px';
}
};
/** @typedef {!{offsets: !Array<!{position: number, time: number}>, precision: number}} */
PerfUI.TimelineGrid.DividersData;
/**
* @interface
*/
PerfUI.TimelineGrid.Calculator = function() {};
PerfUI.TimelineGrid.Calculator.prototype = {
/**
* @param {number} time
* @return {number}
*/
computePosition(time) {},
/**
* @param {number} time
* @param {number=} precision
* @return {string}
*/
formatValue(time, precision) {},
/** @return {number} */
minimumBoundary() {},
/** @return {number} */
zeroTime() {},
/** @return {number} */
maximumBoundary() {},
/** @return {number} */
boundarySpan() {}
};