• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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