• 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 {duration, Span, Time, time} from '../base/time';
18import {colorForCpu} from '../core/colorizer';
19import {timestampFormat, TimestampFormat} from '../core/timestamp_format';
20
21import {
22  OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
23  TRACK_SHELL_WIDTH,
24} from './css_constants';
25import {BorderDragStrategy} from './drag/border_drag_strategy';
26import {DragStrategy} from './drag/drag_strategy';
27import {InnerDragStrategy} from './drag/inner_drag_strategy';
28import {OuterDragStrategy} from './drag/outer_drag_strategy';
29import {DragGestureHandler} from './drag_gesture_handler';
30import {globals} from './globals';
31import {
32  getMaxMajorTicks,
33  MIN_PX_PER_STEP,
34  TickGenerator,
35  TickType,
36} from './gridline_helper';
37import {PanelSize} from './panel';
38import {Panel} from './panel_container';
39import {PxSpan, TimeScale} from './time_scale';
40
41export class OverviewTimelinePanel implements Panel {
42  private static HANDLE_SIZE_PX = 5;
43  readonly kind = 'panel';
44  readonly selectable = false;
45
46  private width = 0;
47  private gesture?: DragGestureHandler;
48  private timeScale?: TimeScale;
49  private traceTime?: Span<time, duration>;
50  private dragStrategy?: DragStrategy;
51  private readonly boundOnMouseMove = this.onMouseMove.bind(this);
52
53  // Must explicitly type now; arguments types are no longer auto-inferred.
54  // https://github.com/Microsoft/TypeScript/issues/1373
55  onupdate({dom}: m.CVnodeDOM) {
56    this.width = dom.getBoundingClientRect().width;
57    this.traceTime = globals.stateTraceTimeTP();
58    const traceTime = globals.stateTraceTime();
59    if (this.width > TRACK_SHELL_WIDTH) {
60      const pxSpan = new PxSpan(TRACK_SHELL_WIDTH, this.width);
61      this.timeScale = TimeScale.fromHPTimeSpan(traceTime, pxSpan);
62      if (this.gesture === undefined) {
63        this.gesture = new DragGestureHandler(
64          dom as HTMLElement,
65          this.onDrag.bind(this),
66          this.onDragStart.bind(this),
67          this.onDragEnd.bind(this),
68        );
69      }
70    } else {
71      this.timeScale = undefined;
72    }
73  }
74
75  oncreate(vnode: m.CVnodeDOM) {
76    this.onupdate(vnode);
77    (vnode.dom as HTMLElement).addEventListener(
78      'mousemove',
79      this.boundOnMouseMove,
80    );
81  }
82
83  onremove({dom}: m.CVnodeDOM) {
84    if (this.gesture) {
85      this.gesture.dispose();
86      this.gesture = undefined;
87    }
88    (dom as HTMLElement).removeEventListener(
89      'mousemove',
90      this.boundOnMouseMove,
91    );
92  }
93
94  render(): m.Children {
95    return m('.overview-timeline', {
96      oncreate: (vnode) => this.oncreate(vnode),
97      onupdate: (vnode) => this.onupdate(vnode),
98      onremove: (vnode) => this.onremove(vnode),
99    });
100  }
101
102  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
103    if (this.width === undefined) return;
104    if (this.traceTime === undefined) return;
105    if (this.timeScale === undefined) return;
106    const headerHeight = 20;
107    const tracksHeight = size.height - headerHeight;
108
109    if (size.width > TRACK_SHELL_WIDTH && this.traceTime.duration > 0n) {
110      const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH);
111      const offset = globals.timestampOffset();
112      const tickGen = new TickGenerator(this.traceTime, maxMajorTicks, offset);
113
114      // Draw time labels
115      ctx.font = '10px Roboto Condensed';
116      ctx.fillStyle = '#999';
117      for (const {type, time} of tickGen) {
118        const xPos = Math.floor(this.timeScale.timeToPx(time));
119        if (xPos <= 0) continue;
120        if (xPos > this.width) break;
121        if (type === TickType.MAJOR) {
122          ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
123          const domainTime = globals.toDomainTime(time);
124          renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP);
125        } else if (type == TickType.MEDIUM) {
126          ctx.fillRect(xPos - 1, 0, 1, 8);
127        } else if (type == TickType.MINOR) {
128          ctx.fillRect(xPos - 1, 0, 1, 5);
129        }
130      }
131    }
132
133    // Draw mini-tracks with quanitzed density for each process.
134    if (globals.overviewStore.size > 0) {
135      const numTracks = globals.overviewStore.size;
136      let y = 0;
137      const trackHeight = (tracksHeight - 1) / numTracks;
138      for (const key of globals.overviewStore.keys()) {
139        const loads = globals.overviewStore.get(key)!;
140        for (let i = 0; i < loads.length; i++) {
141          const xStart = Math.floor(this.timeScale.timeToPx(loads[i].start));
142          const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].end));
143          const yOff = Math.floor(headerHeight + y * trackHeight);
144          const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100);
145          const color = colorForCpu(y).setHSL({s: 50, l: lightness});
146          ctx.fillStyle = color.cssString;
147          ctx.fillRect(xStart, yOff, xEnd - xStart, Math.ceil(trackHeight));
148        }
149        y++;
150      }
151    }
152
153    // Draw bottom border.
154    ctx.fillStyle = '#dadada';
155    ctx.fillRect(0, size.height - 1, this.width, 1);
156
157    // Draw semi-opaque rects that occlude the non-visible time range.
158    const [vizStartPx, vizEndPx] = OverviewTimelinePanel.extractBounds(
159      this.timeScale,
160    );
161
162    ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR;
163    ctx.fillRect(
164      TRACK_SHELL_WIDTH - 1,
165      headerHeight,
166      vizStartPx - TRACK_SHELL_WIDTH,
167      tracksHeight,
168    );
169    ctx.fillRect(vizEndPx, headerHeight, this.width - vizEndPx, tracksHeight);
170
171    // Draw brushes.
172    ctx.fillStyle = '#999';
173    ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight);
174    ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight);
175
176    const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX;
177    const hbarHeight = tracksHeight * 0.4;
178    // Draw handlebar
179    ctx.fillRect(
180      vizStartPx - Math.floor(hbarWidth / 2) - 1,
181      headerHeight,
182      hbarWidth,
183      hbarHeight,
184    );
185    ctx.fillRect(
186      vizEndPx - Math.floor(hbarWidth / 2),
187      headerHeight,
188      hbarWidth,
189      hbarHeight,
190    );
191  }
192
193  private onMouseMove(e: MouseEvent) {
194    if (this.gesture === undefined || this.gesture.isDragging) {
195      return;
196    }
197    (e.target as HTMLElement).style.cursor = this.chooseCursor(e.offsetX);
198  }
199
200  private chooseCursor(x: number) {
201    if (this.timeScale === undefined) return 'default';
202    const [startBound, endBound] = OverviewTimelinePanel.extractBounds(
203      this.timeScale,
204    );
205    if (
206      OverviewTimelinePanel.inBorderRange(x, startBound) ||
207      OverviewTimelinePanel.inBorderRange(x, endBound)
208    ) {
209      return 'ew-resize';
210    } else if (x < TRACK_SHELL_WIDTH) {
211      return 'default';
212    } else if (x < startBound || endBound < x) {
213      return 'crosshair';
214    } else {
215      return 'all-scroll';
216    }
217  }
218
219  onDrag(x: number) {
220    if (this.dragStrategy === undefined) return;
221    this.dragStrategy.onDrag(x);
222  }
223
224  onDragStart(x: number) {
225    if (this.timeScale === undefined) return;
226    const pixelBounds = OverviewTimelinePanel.extractBounds(this.timeScale);
227    if (
228      OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) ||
229      OverviewTimelinePanel.inBorderRange(x, pixelBounds[1])
230    ) {
231      this.dragStrategy = new BorderDragStrategy(this.timeScale, pixelBounds);
232    } else if (x < pixelBounds[0] || pixelBounds[1] < x) {
233      this.dragStrategy = new OuterDragStrategy(this.timeScale);
234    } else {
235      this.dragStrategy = new InnerDragStrategy(this.timeScale, pixelBounds);
236    }
237    this.dragStrategy.onDragStart(x);
238  }
239
240  onDragEnd() {
241    this.dragStrategy = undefined;
242  }
243
244  private static extractBounds(timeScale: TimeScale): [number, number] {
245    const vizTime = globals.timeline.visibleWindowTime;
246    return [
247      Math.floor(timeScale.hpTimeToPx(vizTime.start)),
248      Math.ceil(timeScale.hpTimeToPx(vizTime.end)),
249    ];
250  }
251
252  private static inBorderRange(a: number, b: number): boolean {
253    return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2;
254  }
255}
256
257// Print a timestamp in the configured time format
258function renderTimestamp(
259  ctx: CanvasRenderingContext2D,
260  time: time,
261  x: number,
262  y: number,
263  minWidth: number,
264): void {
265  const fmt = timestampFormat();
266  switch (fmt) {
267    case TimestampFormat.UTC:
268    case TimestampFormat.TraceTz:
269    case TimestampFormat.Timecode:
270      renderTimecode(ctx, time, x, y, minWidth);
271      break;
272    case TimestampFormat.Raw:
273      ctx.fillText(time.toString(), x, y, minWidth);
274      break;
275    case TimestampFormat.RawLocale:
276      ctx.fillText(time.toLocaleString(), x, y, minWidth);
277      break;
278    case TimestampFormat.Seconds:
279      ctx.fillText(Time.formatSeconds(time), x, y, minWidth);
280      break;
281    default:
282      const z: never = fmt;
283      throw new Error(`Invalid timestamp ${z}`);
284  }
285}
286
287// Print a timecode over 2 lines with this formatting:
288// DdHH:MM:SS
289// mmm uuu nnn
290function renderTimecode(
291  ctx: CanvasRenderingContext2D,
292  time: time,
293  x: number,
294  y: number,
295  minWidth: number,
296): void {
297  const timecode = Time.toTimecode(time);
298  const {dhhmmss} = timecode;
299  ctx.fillText(dhhmmss, x, y, minWidth);
300}
301