• 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 {hueForCpu} from '../common/colorizer';
19import {TimeSpan, timeToString} from '../common/time';
20
21import {SIDEBAR_WIDTH, TRACK_SHELL_WIDTH} from './css_constants';
22import {BorderDragStrategy} from './drag/border_drag_strategy';
23import {DragStrategy} from './drag/drag_strategy';
24import {InnerDragStrategy} from './drag/inner_drag_strategy';
25import {OuterDragStrategy} from './drag/outer_drag_strategy';
26import {DragGestureHandler} from './drag_gesture_handler';
27import {globals} from './globals';
28import {Panel, PanelSize} from './panel';
29import {TimeScale} from './time_scale';
30
31export class OverviewTimelinePanel extends Panel {
32  private static HANDLE_SIZE_PX = 7;
33
34  private width = 0;
35  private gesture?: DragGestureHandler;
36  private timeScale?: TimeScale;
37  private totTime = new TimeSpan(0, 0);
38  private dragStrategy?: DragStrategy;
39  private readonly boundOnMouseMove = this.onMouseMove.bind(this);
40
41  // Must explicitly type now; arguments types are no longer auto-inferred.
42  // https://github.com/Microsoft/TypeScript/issues/1373
43  onupdate({dom}: m.CVnodeDOM) {
44    this.width = dom.getBoundingClientRect().width;
45    this.totTime = new TimeSpan(
46        globals.state.traceTime.startSec, globals.state.traceTime.endSec);
47    this.timeScale = new TimeScale(
48        this.totTime, [TRACK_SHELL_WIDTH, assertExists(this.width)]);
49
50    if (this.gesture === undefined) {
51      this.gesture = new DragGestureHandler(
52          dom as HTMLElement,
53          this.onDrag.bind(this),
54          this.onDragStart.bind(this),
55          this.onDragEnd.bind(this));
56    }
57  }
58
59  oncreate(vnode: m.CVnodeDOM) {
60    this.onupdate(vnode);
61    (vnode.dom as HTMLElement)
62        .addEventListener('mousemove', this.boundOnMouseMove);
63  }
64
65  onremove({dom}: m.CVnodeDOM) {
66    (dom as HTMLElement)
67        .removeEventListener('mousemove', this.boundOnMouseMove);
68  }
69
70  view() {
71    return m('.overview-timeline');
72  }
73
74  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
75    if (this.width === undefined) return;
76    if (this.timeScale === undefined) return;
77    const headerHeight = 25;
78    const tracksHeight = size.height - headerHeight;
79
80    // Draw time labels on the top header.
81    ctx.font = '10px Roboto Condensed';
82    ctx.fillStyle = '#999';
83    for (let i = 0; i < 100; i++) {
84      const xPos =
85          (i * (this.width - TRACK_SHELL_WIDTH) / 100) + TRACK_SHELL_WIDTH;
86      const t = this.timeScale.pxToTime(xPos);
87      if (xPos <= 0) continue;
88      if (xPos > this.width) break;
89      if (i % 10 === 0) {
90        ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
91        ctx.fillText(timeToString(t - this.totTime.start), xPos + 5, 18);
92      } else {
93        ctx.fillRect(xPos - 1, 0, 1, 5);
94      }
95    }
96
97    // Draw mini-tracks with quanitzed density for each process.
98    if (globals.overviewStore.size > 0) {
99      const numTracks = globals.overviewStore.size;
100      let y = 0;
101      const trackHeight = (tracksHeight - 1) / numTracks;
102      for (const key of globals.overviewStore.keys()) {
103        const loads = globals.overviewStore.get(key)!;
104        for (let i = 0; i < loads.length; i++) {
105          const xStart = Math.floor(this.timeScale.timeToPx(loads[i].startSec));
106          const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].endSec));
107          const yOff = Math.floor(headerHeight + y * trackHeight);
108          const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100);
109          ctx.fillStyle = `hsl(${hueForCpu(y)}, 50%, ${lightness}%)`;
110          ctx.fillRect(xStart, yOff, xEnd - xStart, Math.ceil(trackHeight));
111        }
112        y++;
113      }
114    }
115
116    // Draw bottom border.
117    ctx.fillStyle = '#dadada';
118    ctx.fillRect(0, size.height - 1, this.width, 1);
119
120    // Draw semi-opaque rects that occlude the non-visible time range.
121    const [vizStartPx, vizEndPx] =
122        OverviewTimelinePanel.extractBounds(this.timeScale);
123
124    ctx.fillStyle = 'rgba(200, 200, 200, 0.8)';
125    ctx.fillRect(
126        TRACK_SHELL_WIDTH - 1,
127        headerHeight,
128        vizStartPx - TRACK_SHELL_WIDTH,
129        tracksHeight);
130    ctx.fillRect(vizEndPx, headerHeight, this.width - vizEndPx, tracksHeight);
131
132    // Draw brushes.
133    ctx.fillStyle = '#999';
134    ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight);
135    ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight);
136
137    const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX;
138    const hbarDivisionFactor = 3.5;
139    // Draw handlebar
140    ctx.fillRect(
141        vizStartPx - Math.floor(hbarWidth / 2) - 1,
142        headerHeight,
143        hbarWidth,
144        tracksHeight / hbarDivisionFactor);
145    ctx.fillRect(
146        vizEndPx - Math.floor(hbarWidth / 2),
147        headerHeight,
148        hbarWidth,
149        tracksHeight / hbarDivisionFactor);
150  }
151
152  private onMouseMove(e: MouseEvent) {
153    if (this.gesture === undefined || this.gesture.isDragging) {
154      return;
155    }
156    (e.target as HTMLElement).style.cursor = this.chooseCursor(e.x);
157  }
158
159  private chooseCursor(x: number) {
160    if (this.timeScale === undefined) return 'default';
161    const [vizStartPx, vizEndPx] =
162        OverviewTimelinePanel.extractBounds(this.timeScale);
163    const startBound = vizStartPx - 1 + SIDEBAR_WIDTH;
164    const endBound = vizEndPx + SIDEBAR_WIDTH;
165    if (OverviewTimelinePanel.inBorderRange(x, startBound) ||
166        OverviewTimelinePanel.inBorderRange(x, endBound)) {
167      return 'ew-resize';
168    } else if (x < SIDEBAR_WIDTH + TRACK_SHELL_WIDTH) {
169      return 'default';
170    } else if (x < startBound || endBound < x) {
171      return 'crosshair';
172    } else {
173      return 'all-scroll';
174    }
175  }
176
177  onDrag(x: number) {
178    if (this.dragStrategy === undefined) return;
179    this.dragStrategy.onDrag(x);
180  }
181
182  onDragStart(x: number) {
183    if (this.timeScale === undefined) return;
184    const pixelBounds = OverviewTimelinePanel.extractBounds(this.timeScale);
185    if (OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) ||
186        OverviewTimelinePanel.inBorderRange(x, pixelBounds[1])) {
187      this.dragStrategy = new BorderDragStrategy(this.timeScale, pixelBounds);
188    } else if (x < pixelBounds[0] || pixelBounds[1] < x) {
189      this.dragStrategy = new OuterDragStrategy(this.timeScale);
190    } else {
191      this.dragStrategy = new InnerDragStrategy(this.timeScale, pixelBounds);
192    }
193    this.dragStrategy.onDragStart(x);
194  }
195
196  onDragEnd() {
197    this.dragStrategy = undefined;
198  }
199
200  private static extractBounds(timeScale: TimeScale): [number, number] {
201    const vizTime = globals.frontendLocalState.getVisibleStateBounds();
202    return [
203      Math.floor(timeScale.timeToPx(vizTime[0])),
204      Math.ceil(timeScale.timeToPx(vizTime[1]))
205    ];
206  }
207
208  private static inBorderRange(a: number, b: number): boolean {
209    return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2;
210  }
211}
212