• 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 * as m from 'mithril';
16
17import {assertExists} from '../base/logging';
18import {Engine} from '../common/engine';
19import {TrackState} from '../common/state';
20import {TrackData} from '../common/track_data';
21
22import {checkerboard} from './checkerboard';
23import {globals} from './globals';
24import {TrackButtonAttrs} from './track_panel';
25
26/**
27 * Args passed to the track constructors when creating a new track.
28 */
29export interface NewTrackArgs {
30  trackId: string;
31  engine: Engine;
32}
33
34/**
35 * This interface forces track implementations to have some static properties.
36 * Typescript does not have abstract static members, which is why this needs to
37 * be in a separate interface.
38 */
39export interface TrackCreator {
40  // Store the kind explicitly as a string as opposed to using class.kind in
41  // case we ever minify our code.
42  readonly kind: string;
43
44  // We need the |create| method because the stored value in the registry can be
45  // an abstract class, and we cannot call 'new' on an abstract class.
46  create(args: NewTrackArgs): Track;
47}
48
49export interface SliceRect {
50  left: number;
51  width: number;
52  top: number;
53  height: number;
54  visible: boolean;
55}
56
57/**
58 * The abstract class that needs to be implemented by all tracks.
59 */
60export abstract class Track<Config = {}, Data extends TrackData = TrackData> {
61  // The UI-generated track ID (not to be confused with the SQL track.id).
62  protected readonly trackId: string;
63  protected readonly engine: Engine;
64
65  // When true this is a new controller-less track type.
66  // TODO(hjd): eventually all tracks will be controller-less and this
67  // should be removed then.
68  protected frontendOnly = false;
69
70  // Caches the last state.track[this.trackId]. This is to deal with track
71  // deletion, see comments in trackState() below.
72  private lastTrackState: TrackState;
73
74  constructor(args: NewTrackArgs) {
75    this.trackId = args.trackId;
76    this.engine = args.engine;
77    this.lastTrackState = assertExists(globals.state.tracks[this.trackId]);
78  }
79
80  // Last call the track will receive. Called just before the last reference to
81  // this object is removed.
82  onDestroy() {}
83
84  protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void;
85
86  protected get trackState(): TrackState {
87    // We can end up in a state where a Track is still in the mithril renderer
88    // tree but its corresponding state has been deleted. This can happen in the
89    // interval of time between a track being removed from the state and the
90    // next animation frame that would remove the Track object. If a mouse event
91    // is dispatched in the meanwhile (or a promise is resolved), we need to be
92    // able to access the state. Hence the caching logic here.
93    const trackState = globals.state.tracks[this.trackId];
94    if (trackState === undefined) {
95      return this.lastTrackState;
96    }
97    this.lastTrackState = trackState;
98    return trackState;
99  }
100
101  get config(): Config {
102    return this.trackState.config as Config;
103  }
104
105  data(): Data|undefined {
106    if (this.frontendOnly) {
107      return undefined;
108    }
109    return globals.trackDataStore.get(this.trackId) as Data;
110  }
111
112  getHeight(): number {
113    return 40;
114  }
115
116  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
117    return [];
118  }
119
120  onMouseMove(_position: {x: number, y: number}) {}
121
122  /**
123   * Returns whether the mouse click has selected something.
124   * Used to prevent further propagation if necessary.
125   */
126  onMouseClick(_position: {x: number, y: number}): boolean {
127    return false;
128  }
129
130  onMouseOut(): void {}
131
132  onFullRedraw(): void {}
133
134  render(ctx: CanvasRenderingContext2D) {
135    globals.frontendLocalState.addVisibleTrack(this.trackState.id);
136    if (this.data() === undefined && !this.frontendOnly) {
137      const {visibleWindowTime, timeScale} = globals.frontendLocalState;
138      const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
139      const endPx = Math.ceil(timeScale.timeToPx(visibleWindowTime.end));
140      checkerboard(ctx, this.getHeight(), startPx, endPx);
141    } else {
142      this.renderCanvas(ctx);
143    }
144  }
145
146  drawTrackHoverTooltip(
147      ctx: CanvasRenderingContext2D, pos: {x: number, y: number}, text: string,
148      text2?: string) {
149    ctx.font = '10px Roboto Condensed';
150    ctx.textBaseline = 'middle';
151    ctx.textAlign = 'left';
152
153    // TODO(hjd): Avoid measuring text all the time (just use monospace?)
154    const textMetrics = ctx.measureText(text);
155    const text2Metrics = ctx.measureText(text2 || '');
156
157    // Padding on each side of the box containing the tooltip:
158    const paddingPx = 4;
159
160    // Figure out the width of the tool tip box:
161    let width = Math.max(textMetrics.width, text2Metrics.width);
162    width += paddingPx * 2;
163
164    // and the height:
165    let height = 0;
166    height += textMetrics.fontBoundingBoxAscent;
167    height += textMetrics.fontBoundingBoxDescent;
168    if (text2 !== undefined) {
169      height += text2Metrics.fontBoundingBoxAscent;
170      height += text2Metrics.fontBoundingBoxDescent;
171    }
172    height += paddingPx * 2;
173
174    let x = pos.x;
175    let y = pos.y;
176
177    // Move box to the top right of the mouse:
178    x += 10;
179    y -= 10;
180
181    // Ensure the box is on screen:
182    const endPx = globals.frontendLocalState.timeScale.endPx;
183    if (x + width > endPx) {
184      x -= x + width - endPx;
185    }
186    if (y < 0) {
187      y = 0;
188    }
189    if (y + height > this.getHeight()) {
190      y -= y + height - this.getHeight();
191    }
192
193    // Draw everything:
194    ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
195    ctx.fillRect(x, y, width, height);
196
197    ctx.fillStyle = 'hsl(200, 50%, 40%)';
198    ctx.fillText(
199        text, x + paddingPx, y + paddingPx + textMetrics.fontBoundingBoxAscent);
200    if (text2 !== undefined) {
201      const yOffsetPx = textMetrics.fontBoundingBoxAscent +
202          textMetrics.fontBoundingBoxDescent +
203          text2Metrics.fontBoundingBoxAscent;
204      ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx);
205    }
206  }
207
208  /**
209   * Returns a place where a given slice should be drawn. Should be implemented
210   * only for track types that support slices e.g. chrome_slice, async_slices
211   * tStart - slice start time in seconds, tEnd - slice end time in seconds,
212   * depth - slice depth
213   */
214  getSliceRect(_tStart: number, _tEnd: number, _depth: number): SliceRect
215      |undefined {
216    return undefined;
217  }
218}
219