mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-07 04:26:32 +08:00
Bug: 1228674 Change-Id: I66cd36c680cda51eccdc83033d3f383f8fb9a528 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3103785 Reviewed-by: Simon Zünd <szuend@chromium.org> Commit-Queue: Kateryna Prokopenko <kprokopenko@google.com>
608 lines
20 KiB
TypeScript
608 lines
20 KiB
TypeScript
/*
|
||
* 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 '../../core/common/common.js';
|
||
import * as i18n from '../../core/i18n/i18n.js';
|
||
import * as Platform from '../../core/platform/platform.js';
|
||
import * as TimelineModel from '../../models/timeline_model/timeline_model.js';
|
||
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
|
||
import * as UI from '../../ui/legacy/legacy.js';
|
||
|
||
import type {PerformanceModel, WindowChangedEvent} from './PerformanceModel.js';
|
||
import {Events} from './PerformanceModel.js';
|
||
import type {TimelineModeViewDelegate} from './TimelinePanel.js';
|
||
|
||
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('panels/timeline/CountersGraph.ts', UIStrings);
|
||
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
||
|
||
export class CountersGraph extends UI.Widget.VBox {
|
||
private readonly delegate: TimelineModeViewDelegate;
|
||
private readonly calculator: Calculator;
|
||
private model!: PerformanceModel|null;
|
||
private readonly header: UI.Widget.HBox;
|
||
readonly toolbar: UI.Toolbar.Toolbar;
|
||
private graphsContainer: UI.Widget.VBox;
|
||
canvasContainer: UI.Widget.WidgetElement;
|
||
private canvas: HTMLCanvasElement;
|
||
private readonly timelineGrid: PerfUI.TimelineGrid.TimelineGrid;
|
||
private readonly counters: Counter[];
|
||
private readonly counterUI: CounterUI[];
|
||
private readonly countersByName: Map<string, Counter>;
|
||
private readonly gpuMemoryCounter: Counter;
|
||
private track?: TimelineModel.TimelineModel.Track|null;
|
||
currentValuesBar?: HTMLElement;
|
||
private markerXPosition?: number;
|
||
|
||
constructor(delegate: TimelineModeViewDelegate) {
|
||
super();
|
||
this.element.id = 'memory-graphs-container';
|
||
|
||
this.delegate = delegate;
|
||
this.calculator = new Calculator();
|
||
|
||
// 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 = 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);
|
||
|
||
this.counters = [];
|
||
this.counterUI = [];
|
||
|
||
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);
|
||
}
|
||
|
||
setModel(model: PerformanceModel|null, track: TimelineModel.TimelineModel.Track|null): void {
|
||
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]);
|
||
}
|
||
}
|
||
}
|
||
|
||
private createCurrentValuesBar(): void {
|
||
this.currentValuesBar = this.graphsContainer.element.createChild('div');
|
||
this.currentValuesBar.id = 'counter-values-bar';
|
||
}
|
||
|
||
private createCounter(uiName: string, color: string, formatter?: ((arg0: number) => string)): Counter {
|
||
const counter = new Counter();
|
||
this.counters.push(counter);
|
||
this.counterUI.push(new CounterUI(this, uiName, color, counter, formatter));
|
||
return counter;
|
||
}
|
||
|
||
resizerElement(): Element|null {
|
||
return this.header.element;
|
||
}
|
||
|
||
private resize(): void {
|
||
const parentElement = (this.canvas.parentElement as HTMLElement);
|
||
this.canvas.width = parentElement.clientWidth * window.devicePixelRatio;
|
||
this.canvas.height = parentElement.clientHeight * window.devicePixelRatio;
|
||
this.calculator.setDisplayWidth(this.canvas.width);
|
||
this.refresh();
|
||
}
|
||
|
||
private onWindowChanged(event: Common.EventTarget.EventTargetEvent<WindowChangedEvent>): void {
|
||
const window = event.data.window;
|
||
this.calculator.setWindow(window.left, window.right);
|
||
this.scheduleRefresh();
|
||
}
|
||
|
||
scheduleRefresh(): void {
|
||
UI.UIUtils.invokeOnceAfterBatchUpdate(this, this.refresh);
|
||
}
|
||
|
||
draw(): void {
|
||
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);
|
||
}
|
||
}
|
||
|
||
private onClick(event: Event): void {
|
||
const x = (event as MouseEvent).x - this.canvasContainer.totalOffsetLeft();
|
||
let minDistance: number = 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);
|
||
}
|
||
}
|
||
|
||
private onMouseLeave(_event: Event): void {
|
||
delete this.markerXPosition;
|
||
this.clearCurrentValueAndMarker();
|
||
}
|
||
|
||
private clearCurrentValueAndMarker(): void {
|
||
for (let i = 0; i < this.counterUI.length; i++) {
|
||
this.counterUI[i].clearCurrentValueAndMarker();
|
||
}
|
||
}
|
||
|
||
private onMouseMove(event: Event): void {
|
||
const x = (event as MouseEvent).x - this.canvasContainer.totalOffsetLeft();
|
||
this.markerXPosition = x;
|
||
this.refreshCurrentValues();
|
||
}
|
||
|
||
private refreshCurrentValues(): void {
|
||
if (this.markerXPosition === undefined) {
|
||
return;
|
||
}
|
||
for (let i = 0; i < this.counterUI.length; ++i) {
|
||
this.counterUI[i].updateCurrentValue(this.markerXPosition);
|
||
}
|
||
}
|
||
|
||
refresh(): void {
|
||
this.timelineGrid.updateDividers(this.calculator);
|
||
this.draw();
|
||
this.refreshCurrentValues();
|
||
}
|
||
|
||
private clear(): void {
|
||
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 {
|
||
times: number[];
|
||
values: number[];
|
||
x: number[];
|
||
minimumIndex: number;
|
||
maximumIndex: number;
|
||
private maxTime: number;
|
||
private minTime: number;
|
||
limitValue?: number;
|
||
|
||
constructor() {
|
||
this.times = [];
|
||
this.values = [];
|
||
this.x = [];
|
||
this.minimumIndex = 0;
|
||
this.maximumIndex = 0;
|
||
this.maxTime = 0;
|
||
this.minTime = 0;
|
||
}
|
||
|
||
appendSample(time: number, value: number): void {
|
||
if (this.values.length && this.values[this.values.length - 1] === value) {
|
||
return;
|
||
}
|
||
this.times.push(time);
|
||
this.values.push(value);
|
||
}
|
||
|
||
reset(): void {
|
||
this.times = [];
|
||
this.values = [];
|
||
}
|
||
|
||
setLimit(value: number): void {
|
||
this.limitValue = value;
|
||
}
|
||
|
||
calculateBounds(): {
|
||
min: number,
|
||
max: number,
|
||
} {
|
||
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};
|
||
}
|
||
|
||
calculateVisibleIndexes(calculator: Calculator): void {
|
||
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;
|
||
}
|
||
|
||
calculateXValues(width: number): void {
|
||
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 {
|
||
private readonly countersPane: CountersGraph;
|
||
counter: Counter;
|
||
private readonly formatter: (arg0: number) => string;
|
||
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
private readonly setting: Common.Settings.Setting<any>;
|
||
private filter: UI.Toolbar.ToolbarSettingCheckbox;
|
||
private range: HTMLElement;
|
||
private value: HTMLElement;
|
||
graphColor: string;
|
||
limitColor: string|null|undefined;
|
||
graphYValues: number[];
|
||
private readonly verticalPadding: number;
|
||
private currentValueLabel: string;
|
||
private marker: HTMLElement;
|
||
|
||
constructor(
|
||
countersPane: CountersGraph, title: string, graphColor: string, counter: Counter,
|
||
formatter?: (arg0: number) => string) {
|
||
this.countersPane = countersPane;
|
||
this.counter = counter;
|
||
this.formatter = formatter || Platform.NumberUtilities.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 = (this.filter.element as HTMLElement);
|
||
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 = (countersPane.currentValuesBar as HTMLElement).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);
|
||
}
|
||
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(): void {
|
||
this.range.textContent = '';
|
||
}
|
||
|
||
setRange(minValue: number, maxValue: number): void {
|
||
const min = this.formatter(minValue);
|
||
const max = this.formatter(maxValue);
|
||
this.range.textContent = i18nString(UIStrings.ss, {PH1: min, PH2: max});
|
||
}
|
||
|
||
private toggleCounterGraph(): void {
|
||
this.value.classList.toggle('hidden', !this.filter.checked());
|
||
this.countersPane.refresh();
|
||
}
|
||
|
||
recordIndexAt(x: number): number {
|
||
return Platform.ArrayUtilities.upperBound(
|
||
this.counter.x, x * window.devicePixelRatio, Platform.ArrayUtilities.DEFAULT_COMPARATOR,
|
||
this.counter.minimumIndex + 1, this.counter.maximumIndex + 1) -
|
||
1;
|
||
}
|
||
|
||
updateCurrentValue(x: number): void {
|
||
if (!this.visible() || !this.counter.values.length || !this.counter.x) {
|
||
return;
|
||
}
|
||
const index = this.recordIndexAt(x);
|
||
const value = Platform.NumberUtilities.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(): void {
|
||
this.value.textContent = '';
|
||
this.marker.classList.add('hidden');
|
||
}
|
||
|
||
drawGraph(canvas: HTMLCanvasElement): void {
|
||
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: number = 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();
|
||
}
|
||
|
||
visible(): boolean {
|
||
return this.filter.checked();
|
||
}
|
||
}
|
||
|
||
export class Calculator implements PerfUI.TimelineGrid.Calculator {
|
||
private minimumBoundaryInternal: number;
|
||
private maximumBoundaryInternal: number;
|
||
private workingArea: number;
|
||
private zeroTimeInternal: number;
|
||
|
||
constructor() {
|
||
this.minimumBoundaryInternal = 0;
|
||
this.maximumBoundaryInternal = 0;
|
||
this.workingArea = 0;
|
||
this.zeroTimeInternal = 0;
|
||
}
|
||
setZeroTime(time: number): void {
|
||
this.zeroTimeInternal = time;
|
||
}
|
||
|
||
computePosition(time: number): number {
|
||
return (time - this.minimumBoundaryInternal) / this.boundarySpan() * this.workingArea;
|
||
}
|
||
|
||
setWindow(minimumBoundary: number, maximumBoundary: number): void {
|
||
this.minimumBoundaryInternal = minimumBoundary;
|
||
this.maximumBoundaryInternal = maximumBoundary;
|
||
}
|
||
|
||
setDisplayWidth(clientWidth: number): void {
|
||
this.workingArea = clientWidth;
|
||
}
|
||
|
||
formatValue(value: number, precision?: number): string {
|
||
return i18n.TimeUtilities.preciseMillisToString(value - this.zeroTime(), precision);
|
||
}
|
||
|
||
maximumBoundary(): number {
|
||
return this.maximumBoundaryInternal;
|
||
}
|
||
|
||
minimumBoundary(): number {
|
||
return this.minimumBoundaryInternal;
|
||
}
|
||
|
||
zeroTime(): number {
|
||
return this.zeroTimeInternal;
|
||
}
|
||
|
||
boundarySpan(): number {
|
||
return this.maximumBoundaryInternal - this.minimumBoundaryInternal;
|
||
}
|
||
}
|