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 {TimeSpan, timeToString} from '../common/time';
19
20import {hueForCpu} from './colorizer';
21import {TRACK_SHELL_WIDTH} from './css_constants';
22import {DragGestureHandler} from './drag_gesture_handler';
23import {globals} from './globals';
24import {Panel, PanelSize} from './panel';
25import {TimeScale} from './time_scale';
26
27export class OverviewTimelinePanel extends Panel {
28  private width = 0;
29  private dragStartPx = 0;
30  private gesture?: DragGestureHandler;
31  private timeScale?: TimeScale;
32  private totTime = new TimeSpan(0, 0);
33
34  // Must explicitly type now; arguments types are no longer auto-inferred.
35  // https://github.com/Microsoft/TypeScript/issues/1373
36  onupdate({dom}: m.CVnodeDOM) {
37    this.width = dom.getBoundingClientRect().width;
38    this.totTime = new TimeSpan(
39        globals.state.traceTime.startSec, globals.state.traceTime.endSec);
40    this.timeScale = new TimeScale(
41        this.totTime, [TRACK_SHELL_WIDTH, assertExists(this.width)]);
42
43    if (this.gesture === undefined) {
44      this.gesture = new DragGestureHandler(
45          dom as HTMLElement,
46          this.onDrag.bind(this),
47          this.onDragStart.bind(this),
48          this.onDragEnd.bind(this));
49    }
50  }
51
52  oncreate(vnode: m.CVnodeDOM) {
53    this.onupdate(vnode);
54  }
55
56  view() {
57    return m('.overview-timeline');
58  }
59
60  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
61    if (this.width === undefined) return;
62    if (this.timeScale === undefined) return;
63    const headerHeight = 25;
64    const tracksHeight = size.height - headerHeight;
65
66    // Draw time labels on the top header.
67    ctx.font = '10px Roboto Condensed';
68    ctx.fillStyle = '#999';
69    for (let i = 0; i < 100; i++) {
70      const xPos =
71          (i * (this.width - TRACK_SHELL_WIDTH) / 100) + TRACK_SHELL_WIDTH;
72      const t = this.timeScale.pxToTime(xPos);
73      if (xPos <= 0) continue;
74      if (xPos > this.width) break;
75      if (i % 10 === 0) {
76        ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
77        ctx.fillText(timeToString(t - this.totTime.start), xPos + 5, 18);
78      } else {
79        ctx.fillRect(xPos - 1, 0, 1, 5);
80      }
81    }
82
83    // Draw mini-tracks with quanitzed density for each process.
84    if (globals.overviewStore.size > 0) {
85      const numTracks = globals.overviewStore.size;
86      let y = 0;
87      const trackHeight = (tracksHeight - 1) / numTracks;
88      for (const key of globals.overviewStore.keys()) {
89        const loads = globals.overviewStore.get(key)!;
90        for (let i = 0; i < loads.length; i++) {
91          const xStart = Math.floor(this.timeScale.timeToPx(loads[i].startSec));
92          const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].endSec));
93          const yOff = Math.floor(headerHeight + y * trackHeight);
94          const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100);
95          ctx.fillStyle = `hsl(${hueForCpu(y)}, 50%, ${lightness}%)`;
96          ctx.fillRect(xStart, yOff, xEnd - xStart, Math.ceil(trackHeight));
97        }
98        y++;
99      }
100    }
101
102    // Draw bottom border.
103    ctx.fillStyle = '#dadada';
104    ctx.fillRect(0, size.height - 1, this.width, 1);
105
106    // Draw semi-opaque rects that occlude the non-visible time range.
107    const vizTime = globals.frontendLocalState.visibleWindowTime;
108    const vizStartPx = Math.floor(this.timeScale.timeToPx(vizTime.start));
109    const vizEndPx = Math.ceil(this.timeScale.timeToPx(vizTime.end));
110
111    ctx.fillStyle = 'rgba(200, 200, 200, 0.8)';
112    ctx.fillRect(
113        TRACK_SHELL_WIDTH - 1,
114        headerHeight,
115        vizStartPx - TRACK_SHELL_WIDTH,
116        tracksHeight);
117    ctx.fillRect(vizEndPx, headerHeight, this.width - vizEndPx, tracksHeight);
118
119    // Draw brushes.
120    ctx.fillStyle = '#999';
121    ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight);
122    ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight);
123  }
124
125  onDrag(x: number) {
126    // Set visible time limits from selection.
127    if (this.timeScale === undefined) return;
128    let tStart = this.timeScale.pxToTime(this.dragStartPx);
129    let tEnd = this.timeScale.pxToTime(x);
130    if (tStart > tEnd) [tStart, tEnd] = [tEnd, tStart];
131    const vizTime = new TimeSpan(tStart, tEnd);
132    globals.frontendLocalState.updateVisibleTime(vizTime);
133    globals.rafScheduler.scheduleRedraw();
134  }
135
136  onDragStart(x: number) {
137    this.dragStartPx = x;
138  }
139
140  onDragEnd() {
141    this.dragStartPx = 0;
142  }
143}
144