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