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