mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 05:35:05 +08:00
- Migrate timeline(Performance) to use Localization V2 APIs. - Removed timeline grdps Tested with en-XL.json https://imgur.com/a/mxMQWBF Bug: 1136655 Change-Id: I0b282c5c9efdd90fd57c376ea4f69ff8eb0d3006 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2689595 Commit-Queue: Vidal Diazleal <vidorteg@microsoft.com> Reviewed-by: Simon Zünd <szuend@chromium.org>
675 lines
20 KiB
JavaScript
675 lines
20 KiB
JavaScript
/*
|
||
* 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.
|
||
*/
|
||
|
||
import * as Common from '../common/common.js';
|
||
import * as i18n from '../i18n/i18n.js';
|
||
import * as PerfUI from '../perf_ui/perf_ui.js';
|
||
import * as Platform from '../platform/platform.js';
|
||
import * as TimelineModel from '../timeline_model/timeline_model.js';
|
||
import * as UI from '../ui/ui.js';
|
||
|
||
import {Events, PerformanceModel, Window} from './PerformanceModel.js'; // eslint-disable-line no-unused-vars
|
||
import {TimelineModeViewDelegate} from './TimelinePanel.js'; // eslint-disable-line no-unused-vars
|
||
|
||
export const UIStrings = {
|
||
/**
|
||
*@description Text for a heap profile type
|
||
*/
|
||
jsHeap: 'JS Heap',
|
||
/**
|
||
*@description Text for documents, a type of resources
|
||
*/
|
||
documents: 'Documents',
|
||
/**
|
||
*@description Text in Counters Graph of the Performance panel
|
||
*/
|
||
nodes: 'Nodes',
|
||
/**
|
||
*@description Text in Counters Graph of the Performance panel
|
||
*/
|
||
listeners: 'Listeners',
|
||
/**
|
||
*@description Text in Counters Graph of the Performance panel
|
||
*/
|
||
gpuMemory: 'GPU Memory',
|
||
/**
|
||
*@description Range text content in Counters Graph of the Performance panel
|
||
*@example {2} PH1
|
||
*@example {10} PH2
|
||
*/
|
||
ss: '[{PH1} – {PH2}]',
|
||
};
|
||
const str_ = i18n.i18n.registerUIStrings('timeline/CountersGraph.js', UIStrings);
|
||
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
||
export class CountersGraph extends UI.Widget.VBox {
|
||
/**
|
||
* @param {!TimelineModeViewDelegate} delegate
|
||
*/
|
||
constructor(delegate) {
|
||
super();
|
||
this.element.id = 'memory-graphs-container';
|
||
|
||
this._delegate = delegate;
|
||
this._calculator = new Calculator();
|
||
/** @type {?PerformanceModel} */
|
||
this._model;
|
||
|
||
// Create selectors
|
||
this._header = new UI.Widget.HBox();
|
||
this._header.element.classList.add('timeline-memory-header');
|
||
this._header.show(this.element);
|
||
this._toolbar = new UI.Toolbar.Toolbar('timeline-memory-toolbar');
|
||
this._header.element.appendChild(this._toolbar.element);
|
||
|
||
this._graphsContainer = new UI.Widget.VBox();
|
||
this._graphsContainer.show(this.element);
|
||
const canvasWidget = new UI.Widget.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 = /** @type {!HTMLCanvasElement} */ (document.createElement('canvas'));
|
||
this._canvasContainer.appendChild(this._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.TimelineGrid();
|
||
this._canvasContainer.appendChild(this._timelineGrid.dividersElement);
|
||
|
||
/** @type {!Array<!Counter>} */
|
||
this._counters = [];
|
||
/** @type {!Array<!CounterUI>} */
|
||
this._counterUI = [];
|
||
|
||
/** @type {!Map<string, !Counter>} */
|
||
this._countersByName = new Map();
|
||
this._countersByName.set(
|
||
'jsHeapSizeUsed',
|
||
this._createCounter(
|
||
i18nString(UIStrings.jsHeap), 'hsl(220, 90%, 43%)', Platform.NumberUtilities.bytesToString));
|
||
this._countersByName.set('documents', this._createCounter(i18nString(UIStrings.documents), 'hsl(0, 90%, 43%)'));
|
||
this._countersByName.set('nodes', this._createCounter(i18nString(UIStrings.nodes), 'hsl(120, 90%, 43%)'));
|
||
this._countersByName.set(
|
||
'jsEventListeners', this._createCounter(i18nString(UIStrings.listeners), 'hsl(38, 90%, 43%)'));
|
||
|
||
this._gpuMemoryCounter = this._createCounter(
|
||
i18nString(UIStrings.gpuMemory), 'hsl(300, 90%, 43%)', Platform.NumberUtilities.bytesToString);
|
||
this._countersByName.set('gpuMemoryUsedKB', this._gpuMemoryCounter);
|
||
}
|
||
|
||
/**
|
||
* @param {?PerformanceModel} model
|
||
* @param {?TimelineModel.TimelineModel.Track} track
|
||
*/
|
||
setModel(model, track) {
|
||
if (this._model !== model) {
|
||
if (this._model) {
|
||
this._model.removeEventListener(Events.WindowChanged, this._onWindowChanged, this);
|
||
}
|
||
this._model = model;
|
||
if (this._model) {
|
||
this._model.addEventListener(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.get(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} color
|
||
* @param {function(number):string=} formatter
|
||
* @return {!Counter}
|
||
*/
|
||
_createCounter(uiName, color, formatter) {
|
||
const counter = new Counter();
|
||
this._counters.push(counter);
|
||
this._counterUI.push(new CounterUI(this, uiName, color, counter, formatter));
|
||
return counter;
|
||
}
|
||
|
||
/**
|
||
* @return {?Element}
|
||
*/
|
||
resizerElement() {
|
||
return this._header.element;
|
||
}
|
||
|
||
_resize() {
|
||
const parentElement = /** @type {!HTMLElement} */ (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.EventTarget.EventTargetEvent} event
|
||
*/
|
||
_onWindowChanged(event) {
|
||
const window = /** @type {!Window} */ (event.data.window);
|
||
this._calculator.setWindow(window.left, window.right);
|
||
this.scheduleRefresh();
|
||
}
|
||
|
||
scheduleRefresh() {
|
||
UI.UIUtils.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 = /** @type {!MouseEvent} */ (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._track) {
|
||
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 = /** @type {!MouseEvent} */ (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');
|
||
if (!ctx) {
|
||
throw new Error('Unable to get canvas context');
|
||
}
|
||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||
}
|
||
}
|
||
|
||
export class Counter {
|
||
constructor() {
|
||
/** @type {!Array<number>} */
|
||
this.times = [];
|
||
/** @type {!Array<number>} */
|
||
this.values = [];
|
||
/** @type {!Array<number>} */
|
||
this.x = [];
|
||
this._minimumIndex = 0;
|
||
this._maximumIndex = 0;
|
||
this._maxTime = 0;
|
||
this._minTime = 0;
|
||
}
|
||
|
||
/**
|
||
* @param {number} time
|
||
* @param {number} value
|
||
*/
|
||
appendSample(time, value) {
|
||
if (this.values.length && this.values[this.values.length - 1] === 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 {!Calculator} calculator
|
||
*/
|
||
_calculateVisibleIndexes(calculator) {
|
||
const start = calculator.minimumBoundary();
|
||
const end = calculator.maximumBoundary();
|
||
|
||
// Maximum index of element whose time <= start.
|
||
this._minimumIndex = Platform.NumberUtilities.clamp(
|
||
Platform.ArrayUtilities.upperBound(this.times, start, Platform.ArrayUtilities.DEFAULT_COMPARATOR) - 1, 0,
|
||
this.times.length - 1);
|
||
|
||
// Minimum index of element whose time >= end.
|
||
this._maximumIndex = Platform.NumberUtilities.clamp(
|
||
Platform.ArrayUtilities.lowerBound(this.times, end, Platform.ArrayUtilities.DEFAULT_COMPARATOR), 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
export class CounterUI {
|
||
/**
|
||
* @param {!CountersGraph} countersPane
|
||
* @param {string} title
|
||
* @param {string} graphColor
|
||
* @param {!Counter} counter
|
||
* @param {(function(number): string)|undefined} formatter
|
||
*/
|
||
constructor(countersPane, title, graphColor, counter, formatter) {
|
||
this._countersPane = countersPane;
|
||
this.counter = counter;
|
||
this._formatter = formatter || Number.withThousandsSeparator;
|
||
|
||
this._setting = Common.Settings.Settings.instance().createSetting('timelineCountersGraph-' + title, true);
|
||
this._setting.setTitle(title);
|
||
this._filter = new UI.Toolbar.ToolbarSettingCheckbox(this._setting, title);
|
||
this._filter.inputElement.classList.add('-theme-preserve-input');
|
||
const parsedColor = Common.Color.Color.parse(graphColor);
|
||
if (parsedColor) {
|
||
const colorWithAlpha = parsedColor.setAlpha(0.5).asString(Common.Color.Format.RGBA);
|
||
const htmlElement = /** @type {!HTMLElement} */ (this._filter.element);
|
||
if (colorWithAlpha) {
|
||
htmlElement.style.backgroundColor = colorWithAlpha;
|
||
}
|
||
htmlElement.style.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 =
|
||
/** @type {!HTMLElement} */ (countersPane._currentValuesBar).createChild('span', 'memory-counter-value');
|
||
this._value.style.color = graphColor;
|
||
this.graphColor = graphColor;
|
||
if (parsedColor) {
|
||
this.limitColor = parsedColor.setAlpha(0.3).asString(Common.Color.Format.RGBA);
|
||
}
|
||
/** @type {!Array<number>} */
|
||
this.graphYValues = [];
|
||
this._verticalPadding = 10;
|
||
|
||
this._currentValueLabel = title;
|
||
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 = i18nString(UIStrings.ss, {PH1: min, PH2: max});
|
||
}
|
||
|
||
_toggleCounterGraph() {
|
||
this._value.classList.toggle('hidden', !this._filter.checked());
|
||
this._countersPane.refresh();
|
||
}
|
||
|
||
/**
|
||
* @param {number} x
|
||
* @return {number}
|
||
*/
|
||
_recordIndexAt(x) {
|
||
return Platform.ArrayUtilities.upperBound(
|
||
this.counter.x, x * window.devicePixelRatio, Platform.ArrayUtilities.DEFAULT_COMPARATOR,
|
||
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 = `${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');
|
||
if (!ctx) {
|
||
throw new Error('Unable to get canvas context');
|
||
}
|
||
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);
|
||
if (this.limitColor) {
|
||
ctx.strokeStyle = this.limitColor;
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
ctx.closePath();
|
||
ctx.restore();
|
||
}
|
||
|
||
/**
|
||
* @return {boolean}
|
||
*/
|
||
visible() {
|
||
return this._filter.checked();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @implements {PerfUI.TimelineGrid.Calculator}
|
||
*/
|
||
export class Calculator {
|
||
constructor() {
|
||
this._minimumBoundary = 0;
|
||
this._maximumBoundary = 0;
|
||
this._workingArea = 0;
|
||
this._zeroTime = 0;
|
||
}
|
||
/**
|
||
* @param {number} time
|
||
*/
|
||
setZeroTime(time) {
|
||
this._zeroTime = time;
|
||
}
|
||
|
||
/**
|
||
* @override
|
||
* @param {number} time
|
||
* @return {number}
|
||
*/
|
||
computePosition(time) {
|
||
return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* @param {number} minimumBoundary
|
||
* @param {number} maximumBoundary
|
||
*/
|
||
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;
|
||
}
|
||
}
|