blob: 6581c81214d0495357ddaa015a73d4a1e712b57b [file] [log] [blame]
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {assertTrue} from '../base/logging';
import {Span, tpDurationToSeconds} from '../common/time';
import {TPDuration, TPTime, TPTimeSpan} from '../common/time';
import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
import {globals} from './globals';
import {TimeScale} from './time_scale';
const micros = 1000n;
const millis = 1000n * micros;
const seconds = 1000n * millis;
const minutes = 60n * seconds;
const hours = 60n * minutes;
const days = 24n * hours;
// These patterns cover the entire range of 0 - 2^63-1 nanoseconds
const patterns: [bigint, string][] = [
[1n, '|'],
[2n, '|:'],
[5n, '|....'],
[10n, '|....:....'],
[20n, '|.:.'],
[50n, '|....'],
[100n, '|....:....'],
[200n, '|.:.'],
[500n, '|....'],
[1n * micros, '|....:....'],
[2n * micros, '|.:.'],
[5n * micros, '|....'],
[10n * micros, '|....:....'],
[20n * micros, '|.:.'],
[50n * micros, '|....'],
[100n * micros, '|....:....'],
[200n * micros, '|.:.'],
[500n * micros, '|....'],
[1n * millis, '|....:....'],
[2n * millis, '|.:.'],
[5n * millis, '|....'],
[10n * millis, '|....:....'],
[20n * millis, '|.:.'],
[50n * millis, '|....'],
[100n * millis, '|....:....'],
[200n * millis, '|.:.'],
[500n * millis, '|....'],
[1n * seconds, '|....:....'],
[2n * seconds, '|.:.'],
[5n * seconds, '|....'],
[10n * seconds, '|....:....'],
[30n * seconds, '|.:.:.'],
[1n * minutes, '|.....'],
[2n * minutes, '|.:.'],
[5n * minutes, '|.....'],
[10n * minutes, '|....:....'],
[30n * minutes, '|.:.:.'],
[1n * hours, '|.....'],
[2n * hours, '|.:.'],
[6n * hours, '|.....'],
[12n * hours, '|.....:.....'],
[1n * days, '|.:.'],
[2n * days, '|.:.'],
[5n * days, '|....'],
[10n * days, '|....:....'],
[20n * days, '|.:.'],
[50n * days, '|....'],
[100n * days, '|....:....'],
[200n * days, '|.:.'],
[500n * days, '|....'],
[1000n * days, '|....:....'],
[2000n * days, '|.:.'],
[5000n * days, '|....'],
[10000n * days, '|....:....'],
[20000n * days, '|.:.'],
[50000n * days, '|....'],
[100000n * days, '|....:....'],
[200000n * days, '|.:.'],
];
// Returns the optimal step size and pattern of ticks within the step.
export function getPattern(minPatternSize: bigint): [TPDuration, string] {
for (const [size, pattern] of patterns) {
if (size >= minPatternSize) {
return [size, pattern];
}
}
throw new Error('Pattern not defined for this minsize');
}
function tickPatternToArray(pattern: string): TickType[] {
const array = Array.from(pattern);
return array.map((char) => {
switch (char) {
case '|':
return TickType.MAJOR;
case ':':
return TickType.MEDIUM;
case '.':
return TickType.MINOR;
default:
// This is almost certainly a developer/fat-finger error
throw Error(`Invalid char "${char}" in pattern "${pattern}"`);
}
});
}
// Get the number of decimal places we would have to print a time to for a given
// min step size. For example, if we know the min step size is 0.1 and all
// values are going to be aligned to integral multiples of 0.1, there's no
// point printing these values with more than 1 decimal place.
// Note: It's assumed that stepSize only has one significant figure.
// E.g. 0.3 and 0.00002 are fine, but 0.123 will be treated as if it were 0.1.
// Some examples: (seconds -> decimal places)
// 1.0 -> 0
// 0.5 -> 1
// 0.009 -> 3
// 0.00007 -> 5
// 30000 -> 0
// 0.30000000000000004 -> 1
export function guessDecimalPlaces(stepSize: TPDuration): number {
const stepSizeSeconds = tpDurationToSeconds(stepSize);
const decimalPlaces = -Math.floor(Math.log10(stepSizeSeconds));
return Math.max(0, decimalPlaces);
}
export enum TickType {
MAJOR,
MEDIUM,
MINOR
}
export interface Tick {
type: TickType;
time: TPTime;
}
const MIN_PX_PER_STEP = 80;
export function getMaxMajorTicks(width: number) {
return Math.max(1, Math.floor(width / MIN_PX_PER_STEP));
}
function roundDownNearest(time: TPTime, stepSize: TPDuration): TPTime {
return stepSize * (time / stepSize);
}
// An iterable which generates a series of ticks for a given timescale.
export class TickGenerator implements Iterable<Tick> {
private _tickPattern: TickType[];
private _patternSize: TPDuration;
private _timeSpan: Span<TPTime>;
private _offset: TPTime;
constructor(
timeSpan: Span<TPTime>, maxMajorTicks: number, offset: TPTime = 0n) {
assertTrue(timeSpan.duration > 0n, 'timeSpan.duration cannot be lte 0');
assertTrue(maxMajorTicks > 0, 'maxMajorTicks cannot be lte 0');
this._timeSpan = timeSpan.add(-offset);
this._offset = offset;
const minStepSize =
BigInt(Math.floor(Number(timeSpan.duration) / maxMajorTicks));
const [size, pattern] = getPattern(minStepSize);
this._patternSize = size;
this._tickPattern = tickPatternToArray(pattern);
}
// Returns an iterable, so this object can be iterated over directly using the
// `for x of y` notation. The use of a generator here is just to make things
// more elegant compared to creating an array of ticks and building an
// iterator for it.
* [Symbol.iterator](): Generator<Tick> {
const stepSize = this._patternSize / BigInt(this._tickPattern.length);
const start = roundDownNearest(this._timeSpan.start, this._patternSize);
const end = this._timeSpan.end;
let patternIndex = 0;
for (let time = start; time < end; time += stepSize, patternIndex++) {
if (time >= this._timeSpan.start) {
patternIndex = patternIndex % this._tickPattern.length;
const type = this._tickPattern[patternIndex];
yield {type, time: time + this._offset};
}
}
}
get digits(): number {
return guessDecimalPlaces(this._patternSize);
}
}
// Gets the timescale associated with the current visible window.
export function timeScaleForVisibleWindow(
startPx: number, endPx: number): TimeScale {
return globals.frontendLocalState.getTimeScale(startPx, endPx);
}
export function drawGridLines(
ctx: CanvasRenderingContext2D,
width: number,
height: number): void {
ctx.strokeStyle = TRACK_BORDER_COLOR;
ctx.lineWidth = 1;
const {earliest, latest} = globals.frontendLocalState.visibleWindow;
const span = new TPTimeSpan(earliest, latest);
if (width > TRACK_SHELL_WIDTH && span.duration > 0n) {
const maxMajorTicks = getMaxMajorTicks(width - TRACK_SHELL_WIDTH);
const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width);
for (const {type, time} of new TickGenerator(
span, maxMajorTicks, globals.state.traceTime.start)) {
const px = Math.floor(map.tpTimeToPx(time));
if (type === TickType.MAJOR) {
ctx.beginPath();
ctx.moveTo(px + 0.5, 0);
ctx.lineTo(px + 0.5, height);
ctx.stroke();
}
}
}
}