• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {TimeRange} from 'app/timeline_data';
18import {TRACE_INFO} from 'app/trace_info';
19import {Timestamp, TimestampType} from 'trace/timestamp';
20import {Trace} from 'trace/trace';
21import {Traces} from 'trace/traces';
22import {TraceType} from 'trace/trace_type';
23import {Color} from '../../colors';
24import {CanvasDrawer} from '../canvas/canvas_drawer';
25import {CanvasMouseHandler} from '../canvas/canvas_mouse_handler';
26import {DraggableCanvasObject} from '../canvas/draggable_canvas_object';
27import {Segment} from './utils';
28
29export class MiniCanvasDrawerInput {
30  constructor(
31    public fullRange: TimeRange,
32    public selectedPosition: Timestamp,
33    public selection: TimeRange,
34    public traces: Traces
35  ) {}
36
37  transform(mapToRange: Segment): MiniCanvasDrawerData {
38    const transformer = new Transformer(this.fullRange, mapToRange);
39    return new MiniCanvasDrawerData(
40      transformer.transform(this.selectedPosition),
41      {
42        from: transformer.transform(this.selection.from),
43        to: transformer.transform(this.selection.to),
44      },
45      this.transformTracesTimestamps(transformer),
46      transformer
47    );
48  }
49
50  private transformTracesTimestamps(transformer: Transformer): Map<TraceType, number[]> {
51    const transformedTraceSegments = new Map<TraceType, number[]>();
52
53    this.traces.forEachTrace((trace) => {
54      transformedTraceSegments.set(trace.type, this.transformTraceTimestamps(transformer, trace));
55    });
56
57    return transformedTraceSegments;
58  }
59
60  private transformTraceTimestamps(transformer: Transformer, trace: Trace<{}>): number[] {
61    const result: number[] = [];
62
63    trace.forEachTimestamp((timestamp) => {
64      result.push(transformer.transform(timestamp));
65    });
66
67    return result;
68  }
69}
70
71export class Transformer {
72  private timestampType: TimestampType;
73
74  private fromWidth: bigint;
75  private targetWidth: number;
76
77  private fromOffset: bigint;
78  private toOffset: number;
79
80  constructor(private fromRange: TimeRange, private toRange: Segment) {
81    this.timestampType = fromRange.from.getType();
82
83    this.fromWidth = this.fromRange.to.getValueNs() - this.fromRange.from.getValueNs();
84    // Needs to be a whole number to be compatible with bigints
85    this.targetWidth = Math.round(this.toRange.to - this.toRange.from);
86
87    this.fromOffset = this.fromRange.from.getValueNs();
88    // Needs to be a whole number to be compatible with bigints
89    this.toOffset = Math.round(this.toRange.from);
90  }
91
92  transform(x: Timestamp): number {
93    return (
94      this.toOffset +
95      (this.targetWidth * Number(x.getValueNs() - this.fromOffset)) / Number(this.fromWidth)
96    );
97  }
98
99  untransform(x: number): Timestamp {
100    x = Math.round(x);
101    const valueNs =
102      this.fromOffset + (BigInt(x - this.toOffset) * this.fromWidth) / BigInt(this.targetWidth);
103    return new Timestamp(this.timestampType, valueNs);
104  }
105}
106
107class MiniCanvasDrawerOutput {
108  constructor(public selectedPosition: Timestamp, public selection: TimeRange) {}
109}
110
111class MiniCanvasDrawerData {
112  constructor(
113    public selectedPosition: number,
114    public selection: Segment,
115    public timelineEntries: Map<TraceType, number[]>,
116    public transformer: Transformer
117  ) {}
118
119  toOutput(): MiniCanvasDrawerOutput {
120    return new MiniCanvasDrawerOutput(this.transformer.untransform(this.selectedPosition), {
121      from: this.transformer.untransform(this.selection.from),
122      to: this.transformer.untransform(this.selection.to),
123    });
124  }
125}
126
127export class MiniCanvasDrawer implements CanvasDrawer {
128  ctx: CanvasRenderingContext2D;
129  handler: CanvasMouseHandler;
130
131  private activePointer: DraggableCanvasObject;
132  private leftFocusSectionSelector: DraggableCanvasObject;
133  private rightFocusSectionSelector: DraggableCanvasObject;
134
135  private get pointerWidth() {
136    return this.getHeight() / 6;
137  }
138
139  getXScale() {
140    return this.ctx.getTransform().m11;
141  }
142
143  getYScale() {
144    return this.ctx.getTransform().m22;
145  }
146
147  getWidth() {
148    return this.canvas.width / this.getXScale();
149  }
150
151  getHeight() {
152    return this.canvas.height / this.getYScale();
153  }
154
155  get usableRange() {
156    return {
157      from: this.padding.left,
158      to: this.getWidth() - this.padding.left - this.padding.right,
159    };
160  }
161
162  get input() {
163    return this.inputGetter().transform(this.usableRange);
164  }
165
166  constructor(
167    public canvas: HTMLCanvasElement,
168    private inputGetter: () => MiniCanvasDrawerInput,
169    private onPointerPositionDragging: (pos: Timestamp) => void,
170    private onPointerPositionChanged: (pos: Timestamp) => void,
171    private onSelectionChanged: (selection: TimeRange) => void,
172    private onUnhandledClick: (pos: Timestamp) => void
173  ) {
174    const ctx = canvas.getContext('2d');
175
176    if (ctx === null) {
177      throw Error('MiniTimeline canvas context was null!');
178    }
179
180    this.ctx = ctx;
181
182    const onUnhandledClickInternal = (x: number, y: number) => {
183      this.onUnhandledClick(this.input.transformer.untransform(x));
184    };
185    this.handler = new CanvasMouseHandler(this, 'pointer', onUnhandledClickInternal);
186
187    this.activePointer = new DraggableCanvasObject(
188      this,
189      () => this.selectedPosition,
190      (ctx: CanvasRenderingContext2D, position: number) => {
191        const barWidth = 3;
192        const triangleHeight = this.pointerWidth / 2;
193
194        ctx.beginPath();
195        ctx.moveTo(position - triangleHeight, 0);
196        ctx.lineTo(position + triangleHeight, 0);
197        ctx.lineTo(position + barWidth / 2, triangleHeight);
198        ctx.lineTo(position + barWidth / 2, this.getHeight());
199        ctx.lineTo(position - barWidth / 2, this.getHeight());
200        ctx.lineTo(position - barWidth / 2, triangleHeight);
201        ctx.closePath();
202      },
203      {
204        fillStyle: Color.ACTIVE_POINTER,
205        fill: true,
206      },
207      (x) => {
208        this.input.selectedPosition = x;
209        this.onPointerPositionDragging(this.input.transformer.untransform(x));
210      },
211      (x) => {
212        this.input.selectedPosition = x;
213        this.onPointerPositionChanged(this.input.transformer.untransform(x));
214      },
215      () => this.usableRange
216    );
217
218    const focusSelectorDrawConfig = {
219      fillStyle: Color.SELECTOR_COLOR,
220      fill: true,
221    };
222
223    const onLeftSelectionChanged = (x: number) => {
224      this.selection.from = x;
225      this.onSelectionChanged({
226        from: this.input.transformer.untransform(x),
227        to: this.input.transformer.untransform(this.selection.to),
228      });
229    };
230    const onRightSelectionChanged = (x: number) => {
231      this.selection.to = x;
232      this.onSelectionChanged({
233        from: this.input.transformer.untransform(this.selection.from),
234        to: this.input.transformer.untransform(x),
235      });
236    };
237
238    const barWidth = 6;
239    const selectorArrowWidth = this.innerHeight / 12;
240    const selectorArrowHeight = selectorArrowWidth * 2;
241
242    this.leftFocusSectionSelector = new DraggableCanvasObject(
243      this,
244      () => this.selection.from,
245      (ctx: CanvasRenderingContext2D, position: number) => {
246        ctx.beginPath();
247        ctx.moveTo(position - barWidth, this.padding.top);
248        ctx.lineTo(position, this.padding.top);
249        ctx.lineTo(position + selectorArrowWidth, this.padding.top + selectorArrowWidth);
250        ctx.lineTo(position, this.padding.top + selectorArrowHeight);
251        ctx.lineTo(position, this.padding.top + this.innerHeight);
252        ctx.lineTo(position - barWidth, this.padding.top + this.innerHeight);
253        ctx.lineTo(position - barWidth, this.padding.top);
254        ctx.closePath();
255      },
256      focusSelectorDrawConfig,
257      onLeftSelectionChanged,
258      onLeftSelectionChanged,
259      () => {
260        return {
261          from: this.usableRange.from,
262          to: this.rightFocusSectionSelector.position - selectorArrowWidth - barWidth,
263        };
264      }
265    );
266
267    this.rightFocusSectionSelector = new DraggableCanvasObject(
268      this,
269      () => this.selection.to,
270      (ctx: CanvasRenderingContext2D, position: number) => {
271        ctx.beginPath();
272        ctx.moveTo(position + barWidth, this.padding.top);
273        ctx.lineTo(position, this.padding.top);
274        ctx.lineTo(position - selectorArrowWidth, this.padding.top + selectorArrowWidth);
275        ctx.lineTo(position, this.padding.top + selectorArrowHeight);
276        ctx.lineTo(position, this.padding.top + this.innerHeight);
277        ctx.lineTo(position + barWidth, this.padding.top + this.innerHeight);
278        ctx.closePath();
279      },
280      focusSelectorDrawConfig,
281      onRightSelectionChanged,
282      onRightSelectionChanged,
283      () => {
284        return {
285          from: this.leftFocusSectionSelector.position + selectorArrowWidth + barWidth,
286          to: this.usableRange.to,
287        };
288      }
289    );
290  }
291
292  get selectedPosition() {
293    return this.input.selectedPosition;
294  }
295
296  get selection() {
297    return this.input.selection;
298  }
299
300  get timelineEntries() {
301    return this.input.timelineEntries;
302  }
303
304  get padding() {
305    return {
306      top: Math.ceil(this.getHeight() / 5),
307      bottom: Math.ceil(this.getHeight() / 5),
308      left: Math.ceil(this.pointerWidth / 2),
309      right: Math.ceil(this.pointerWidth / 2),
310    };
311  }
312
313  get innerHeight() {
314    return this.getHeight() - this.padding.top - this.padding.bottom;
315  }
316
317  draw() {
318    this.ctx.clearRect(0, 0, this.getWidth(), this.getHeight());
319
320    this.drawSelectionBackground();
321
322    this.drawTraceLines();
323
324    this.drawTimelineGuides();
325
326    this.leftFocusSectionSelector.draw(this.ctx);
327    this.rightFocusSectionSelector.draw(this.ctx);
328
329    this.activePointer.draw(this.ctx);
330  }
331
332  private drawSelectionBackground() {
333    const triangleHeight = this.innerHeight / 6;
334
335    // Selection background
336    this.ctx.globalAlpha = 0.8;
337    this.ctx.fillStyle = Color.SELECTION_BACKGROUND;
338    const width = this.selection.to - this.selection.from;
339    this.ctx.fillRect(
340      this.selection.from,
341      this.padding.top + triangleHeight / 2,
342      width,
343      this.innerHeight - triangleHeight / 2
344    );
345    this.ctx.restore();
346  }
347
348  private drawTraceLines() {
349    const lineHeight = this.innerHeight / 8;
350
351    let fromTop = this.padding.top + (this.innerHeight * 2) / 3 - lineHeight;
352
353    this.timelineEntries.forEach((entries, traceType) => {
354      // TODO: Only if active or a selected trace
355      for (const entry of entries) {
356        this.ctx.globalAlpha = 0.7;
357        this.ctx.fillStyle = TRACE_INFO[traceType].color;
358
359        const width = 5;
360        this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight);
361        this.ctx.globalAlpha = 1.0;
362      }
363
364      fromTop -= (lineHeight * 4) / 3;
365    });
366  }
367
368  private drawTimelineGuides() {
369    const edgeBarHeight = (this.innerHeight * 1) / 2;
370    const edgeBarWidth = 4;
371
372    const boldBarHeight = (this.innerHeight * 1) / 5;
373    const boldBarWidth = edgeBarWidth;
374
375    const lightBarHeight = (this.innerHeight * 1) / 6;
376    const lightBarWidth = 2;
377
378    const minSpacing = lightBarWidth * 7;
379    const barsInSetWidth = 9 * lightBarWidth + boldBarWidth;
380    const barSets = Math.floor(
381      (this.getWidth() - edgeBarWidth * 2 - minSpacing) / (barsInSetWidth + 10 * minSpacing)
382    );
383    const bars = barSets * 10;
384
385    // Draw start bar
386    this.ctx.fillStyle = Color.GUIDE_BAR;
387    this.ctx.fillRect(
388      0,
389      this.padding.top + this.innerHeight - edgeBarHeight,
390      edgeBarWidth,
391      edgeBarHeight
392    );
393
394    // Draw end bar
395    this.ctx.fillStyle = Color.GUIDE_BAR;
396    this.ctx.fillRect(
397      this.getWidth() - edgeBarWidth,
398      this.padding.top + this.innerHeight - edgeBarHeight,
399      edgeBarWidth,
400      edgeBarHeight
401    );
402
403    const spacing = (this.getWidth() - barSets * barsInSetWidth - edgeBarWidth) / bars;
404    let start = edgeBarWidth + spacing;
405    for (let i = 1; i < bars; i++) {
406      if (i % 10 === 0) {
407        // Draw boldbar
408        this.ctx.fillStyle = Color.GUIDE_BAR;
409        this.ctx.fillRect(
410          start,
411          this.padding.top + this.innerHeight - boldBarHeight,
412          boldBarWidth,
413          boldBarHeight
414        );
415        start += boldBarWidth; // TODO: Shift a bit
416      } else {
417        // Draw lightbar
418        this.ctx.fillStyle = Color.GUIDE_BAR_LIGHT;
419        this.ctx.fillRect(
420          start,
421          this.padding.top + this.innerHeight - lightBarHeight,
422          lightBarWidth,
423          lightBarHeight
424        );
425        start += lightBarWidth;
426      }
427      start += spacing;
428    }
429  }
430}
431