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