1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {assertTrue} from '../base/logging'; 16import {Span, tpDurationToSeconds} from '../common/time'; 17import {TPDuration, TPTime, TPTimeSpan} from '../common/time'; 18 19import {TRACK_BORDER_COLOR, TRACK_SHELL_WIDTH} from './css_constants'; 20import {globals} from './globals'; 21import {TimeScale} from './time_scale'; 22 23const micros = 1000n; 24const millis = 1000n * micros; 25const seconds = 1000n * millis; 26const minutes = 60n * seconds; 27const hours = 60n * minutes; 28const days = 24n * hours; 29 30// These patterns cover the entire range of 0 - 2^63-1 nanoseconds 31const patterns: [bigint, string][] = [ 32 [1n, '|'], 33 [2n, '|:'], 34 [5n, '|....'], 35 [10n, '|....:....'], 36 [20n, '|.:.'], 37 [50n, '|....'], 38 [100n, '|....:....'], 39 [200n, '|.:.'], 40 [500n, '|....'], 41 [1n * micros, '|....:....'], 42 [2n * micros, '|.:.'], 43 [5n * micros, '|....'], 44 [10n * micros, '|....:....'], 45 [20n * micros, '|.:.'], 46 [50n * micros, '|....'], 47 [100n * micros, '|....:....'], 48 [200n * micros, '|.:.'], 49 [500n * micros, '|....'], 50 [1n * millis, '|....:....'], 51 [2n * millis, '|.:.'], 52 [5n * millis, '|....'], 53 [10n * millis, '|....:....'], 54 [20n * millis, '|.:.'], 55 [50n * millis, '|....'], 56 [100n * millis, '|....:....'], 57 [200n * millis, '|.:.'], 58 [500n * millis, '|....'], 59 [1n * seconds, '|....:....'], 60 [2n * seconds, '|.:.'], 61 [5n * seconds, '|....'], 62 [10n * seconds, '|....:....'], 63 [30n * seconds, '|.:.:.'], 64 [1n * minutes, '|.....'], 65 [2n * minutes, '|.:.'], 66 [5n * minutes, '|.....'], 67 [10n * minutes, '|....:....'], 68 [30n * minutes, '|.:.:.'], 69 [1n * hours, '|.....'], 70 [2n * hours, '|.:.'], 71 [6n * hours, '|.....'], 72 [12n * hours, '|.....:.....'], 73 [1n * days, '|.:.'], 74 [2n * days, '|.:.'], 75 [5n * days, '|....'], 76 [10n * days, '|....:....'], 77 [20n * days, '|.:.'], 78 [50n * days, '|....'], 79 [100n * days, '|....:....'], 80 [200n * days, '|.:.'], 81 [500n * days, '|....'], 82 [1000n * days, '|....:....'], 83 [2000n * days, '|.:.'], 84 [5000n * days, '|....'], 85 [10000n * days, '|....:....'], 86 [20000n * days, '|.:.'], 87 [50000n * days, '|....'], 88 [100000n * days, '|....:....'], 89 [200000n * days, '|.:.'], 90]; 91 92// Returns the optimal step size and pattern of ticks within the step. 93export function getPattern(minPatternSize: bigint): [TPDuration, string] { 94 for (const [size, pattern] of patterns) { 95 if (size >= minPatternSize) { 96 return [size, pattern]; 97 } 98 } 99 100 throw new Error('Pattern not defined for this minsize'); 101} 102 103function tickPatternToArray(pattern: string): TickType[] { 104 const array = Array.from(pattern); 105 return array.map((char) => { 106 switch (char) { 107 case '|': 108 return TickType.MAJOR; 109 case ':': 110 return TickType.MEDIUM; 111 case '.': 112 return TickType.MINOR; 113 default: 114 // This is almost certainly a developer/fat-finger error 115 throw Error(`Invalid char "${char}" in pattern "${pattern}"`); 116 } 117 }); 118} 119 120// Get the number of decimal places we would have to print a time to for a given 121// min step size. For example, if we know the min step size is 0.1 and all 122// values are going to be aligned to integral multiples of 0.1, there's no 123// point printing these values with more than 1 decimal place. 124// Note: It's assumed that stepSize only has one significant figure. 125// E.g. 0.3 and 0.00002 are fine, but 0.123 will be treated as if it were 0.1. 126// Some examples: (seconds -> decimal places) 127// 1.0 -> 0 128// 0.5 -> 1 129// 0.009 -> 3 130// 0.00007 -> 5 131// 30000 -> 0 132// 0.30000000000000004 -> 1 133export function guessDecimalPlaces(stepSize: TPDuration): number { 134 const stepSizeSeconds = tpDurationToSeconds(stepSize); 135 const decimalPlaces = -Math.floor(Math.log10(stepSizeSeconds)); 136 return Math.max(0, decimalPlaces); 137} 138 139export enum TickType { 140 MAJOR, 141 MEDIUM, 142 MINOR 143} 144 145export interface Tick { 146 type: TickType; 147 time: TPTime; 148} 149 150const MIN_PX_PER_STEP = 80; 151export function getMaxMajorTicks(width: number) { 152 return Math.max(1, Math.floor(width / MIN_PX_PER_STEP)); 153} 154 155function roundDownNearest(time: TPTime, stepSize: TPDuration): TPTime { 156 return stepSize * (time / stepSize); 157} 158 159// An iterable which generates a series of ticks for a given timescale. 160export class TickGenerator implements Iterable<Tick> { 161 private _tickPattern: TickType[]; 162 private _patternSize: TPDuration; 163 private _timeSpan: Span<TPTime>; 164 private _offset: TPTime; 165 166 constructor( 167 timeSpan: Span<TPTime>, maxMajorTicks: number, offset: TPTime = 0n) { 168 assertTrue(timeSpan.duration > 0n, 'timeSpan.duration cannot be lte 0'); 169 assertTrue(maxMajorTicks > 0, 'maxMajorTicks cannot be lte 0'); 170 171 this._timeSpan = timeSpan.add(-offset); 172 this._offset = offset; 173 const minStepSize = 174 BigInt(Math.floor(Number(timeSpan.duration) / maxMajorTicks)); 175 const [size, pattern] = getPattern(minStepSize); 176 this._patternSize = size; 177 this._tickPattern = tickPatternToArray(pattern); 178 } 179 180 // Returns an iterable, so this object can be iterated over directly using the 181 // `for x of y` notation. The use of a generator here is just to make things 182 // more elegant compared to creating an array of ticks and building an 183 // iterator for it. 184 * [Symbol.iterator](): Generator<Tick> { 185 const stepSize = this._patternSize / BigInt(this._tickPattern.length); 186 const start = roundDownNearest(this._timeSpan.start, this._patternSize); 187 const end = this._timeSpan.end; 188 let patternIndex = 0; 189 190 for (let time = start; time < end; time += stepSize, patternIndex++) { 191 if (time >= this._timeSpan.start) { 192 patternIndex = patternIndex % this._tickPattern.length; 193 const type = this._tickPattern[patternIndex]; 194 yield {type, time: time + this._offset}; 195 } 196 } 197 } 198 199 get digits(): number { 200 return guessDecimalPlaces(this._patternSize); 201 } 202} 203 204// Gets the timescale associated with the current visible window. 205export function timeScaleForVisibleWindow( 206 startPx: number, endPx: number): TimeScale { 207 return globals.frontendLocalState.getTimeScale(startPx, endPx); 208} 209 210export function drawGridLines( 211 ctx: CanvasRenderingContext2D, 212 width: number, 213 height: number): void { 214 ctx.strokeStyle = TRACK_BORDER_COLOR; 215 ctx.lineWidth = 1; 216 217 const {earliest, latest} = globals.frontendLocalState.visibleWindow; 218 const span = new TPTimeSpan(earliest, latest); 219 if (width > TRACK_SHELL_WIDTH && span.duration > 0n) { 220 const maxMajorTicks = getMaxMajorTicks(width - TRACK_SHELL_WIDTH); 221 const map = timeScaleForVisibleWindow(TRACK_SHELL_WIDTH, width); 222 for (const {type, time} of new TickGenerator( 223 span, maxMajorTicks, globals.state.traceTime.start)) { 224 const px = Math.floor(map.tpTimeToPx(time)); 225 if (type === TickType.MAJOR) { 226 ctx.beginPath(); 227 ctx.moveTo(px + 0.5, 0); 228 ctx.lineTo(px + 0.5, height); 229 ctx.stroke(); 230 } 231 } 232 } 233} 234