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