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 {duration, Span, Time, time} from '../base/time'; 18import {colorForCpu} from '../core/colorizer'; 19import {timestampFormat, TimestampFormat} from '../core/timestamp_format'; 20 21import { 22 OVERVIEW_TIMELINE_NON_VISIBLE_COLOR, 23 TRACK_SHELL_WIDTH, 24} from './css_constants'; 25import {BorderDragStrategy} from './drag/border_drag_strategy'; 26import {DragStrategy} from './drag/drag_strategy'; 27import {InnerDragStrategy} from './drag/inner_drag_strategy'; 28import {OuterDragStrategy} from './drag/outer_drag_strategy'; 29import {DragGestureHandler} from './drag_gesture_handler'; 30import {globals} from './globals'; 31import { 32 getMaxMajorTicks, 33 MIN_PX_PER_STEP, 34 TickGenerator, 35 TickType, 36} from './gridline_helper'; 37import {PanelSize} from './panel'; 38import {Panel} from './panel_container'; 39import {PxSpan, TimeScale} from './time_scale'; 40 41export class OverviewTimelinePanel implements Panel { 42 private static HANDLE_SIZE_PX = 5; 43 readonly kind = 'panel'; 44 readonly selectable = false; 45 46 private width = 0; 47 private gesture?: DragGestureHandler; 48 private timeScale?: TimeScale; 49 private traceTime?: Span<time, duration>; 50 private dragStrategy?: DragStrategy; 51 private readonly boundOnMouseMove = this.onMouseMove.bind(this); 52 53 // Must explicitly type now; arguments types are no longer auto-inferred. 54 // https://github.com/Microsoft/TypeScript/issues/1373 55 onupdate({dom}: m.CVnodeDOM) { 56 this.width = dom.getBoundingClientRect().width; 57 this.traceTime = globals.stateTraceTimeTP(); 58 const traceTime = globals.stateTraceTime(); 59 if (this.width > TRACK_SHELL_WIDTH) { 60 const pxSpan = new PxSpan(TRACK_SHELL_WIDTH, this.width); 61 this.timeScale = TimeScale.fromHPTimeSpan(traceTime, pxSpan); 62 if (this.gesture === undefined) { 63 this.gesture = new DragGestureHandler( 64 dom as HTMLElement, 65 this.onDrag.bind(this), 66 this.onDragStart.bind(this), 67 this.onDragEnd.bind(this), 68 ); 69 } 70 } else { 71 this.timeScale = undefined; 72 } 73 } 74 75 oncreate(vnode: m.CVnodeDOM) { 76 this.onupdate(vnode); 77 (vnode.dom as HTMLElement).addEventListener( 78 'mousemove', 79 this.boundOnMouseMove, 80 ); 81 } 82 83 onremove({dom}: m.CVnodeDOM) { 84 if (this.gesture) { 85 this.gesture.dispose(); 86 this.gesture = undefined; 87 } 88 (dom as HTMLElement).removeEventListener( 89 'mousemove', 90 this.boundOnMouseMove, 91 ); 92 } 93 94 render(): m.Children { 95 return m('.overview-timeline', { 96 oncreate: (vnode) => this.oncreate(vnode), 97 onupdate: (vnode) => this.onupdate(vnode), 98 onremove: (vnode) => this.onremove(vnode), 99 }); 100 } 101 102 renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { 103 if (this.width === undefined) return; 104 if (this.traceTime === undefined) return; 105 if (this.timeScale === undefined) return; 106 const headerHeight = 20; 107 const tracksHeight = size.height - headerHeight; 108 109 if (size.width > TRACK_SHELL_WIDTH && this.traceTime.duration > 0n) { 110 const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH); 111 const offset = globals.timestampOffset(); 112 const tickGen = new TickGenerator(this.traceTime, maxMajorTicks, offset); 113 114 // Draw time labels 115 ctx.font = '10px Roboto Condensed'; 116 ctx.fillStyle = '#999'; 117 for (const {type, time} of tickGen) { 118 const xPos = Math.floor(this.timeScale.timeToPx(time)); 119 if (xPos <= 0) continue; 120 if (xPos > this.width) break; 121 if (type === TickType.MAJOR) { 122 ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5); 123 const domainTime = globals.toDomainTime(time); 124 renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP); 125 } else if (type == TickType.MEDIUM) { 126 ctx.fillRect(xPos - 1, 0, 1, 8); 127 } else if (type == TickType.MINOR) { 128 ctx.fillRect(xPos - 1, 0, 1, 5); 129 } 130 } 131 } 132 133 // Draw mini-tracks with quanitzed density for each process. 134 if (globals.overviewStore.size > 0) { 135 const numTracks = globals.overviewStore.size; 136 let y = 0; 137 const trackHeight = (tracksHeight - 1) / numTracks; 138 for (const key of globals.overviewStore.keys()) { 139 const loads = globals.overviewStore.get(key)!; 140 for (let i = 0; i < loads.length; i++) { 141 const xStart = Math.floor(this.timeScale.timeToPx(loads[i].start)); 142 const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].end)); 143 const yOff = Math.floor(headerHeight + y * trackHeight); 144 const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100); 145 const color = colorForCpu(y).setHSL({s: 50, l: lightness}); 146 ctx.fillStyle = color.cssString; 147 ctx.fillRect(xStart, yOff, xEnd - xStart, Math.ceil(trackHeight)); 148 } 149 y++; 150 } 151 } 152 153 // Draw bottom border. 154 ctx.fillStyle = '#dadada'; 155 ctx.fillRect(0, size.height - 1, this.width, 1); 156 157 // Draw semi-opaque rects that occlude the non-visible time range. 158 const [vizStartPx, vizEndPx] = OverviewTimelinePanel.extractBounds( 159 this.timeScale, 160 ); 161 162 ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR; 163 ctx.fillRect( 164 TRACK_SHELL_WIDTH - 1, 165 headerHeight, 166 vizStartPx - TRACK_SHELL_WIDTH, 167 tracksHeight, 168 ); 169 ctx.fillRect(vizEndPx, headerHeight, this.width - vizEndPx, tracksHeight); 170 171 // Draw brushes. 172 ctx.fillStyle = '#999'; 173 ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight); 174 ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight); 175 176 const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX; 177 const hbarHeight = tracksHeight * 0.4; 178 // Draw handlebar 179 ctx.fillRect( 180 vizStartPx - Math.floor(hbarWidth / 2) - 1, 181 headerHeight, 182 hbarWidth, 183 hbarHeight, 184 ); 185 ctx.fillRect( 186 vizEndPx - Math.floor(hbarWidth / 2), 187 headerHeight, 188 hbarWidth, 189 hbarHeight, 190 ); 191 } 192 193 private onMouseMove(e: MouseEvent) { 194 if (this.gesture === undefined || this.gesture.isDragging) { 195 return; 196 } 197 (e.target as HTMLElement).style.cursor = this.chooseCursor(e.offsetX); 198 } 199 200 private chooseCursor(x: number) { 201 if (this.timeScale === undefined) return 'default'; 202 const [startBound, endBound] = OverviewTimelinePanel.extractBounds( 203 this.timeScale, 204 ); 205 if ( 206 OverviewTimelinePanel.inBorderRange(x, startBound) || 207 OverviewTimelinePanel.inBorderRange(x, endBound) 208 ) { 209 return 'ew-resize'; 210 } else if (x < TRACK_SHELL_WIDTH) { 211 return 'default'; 212 } else if (x < startBound || endBound < x) { 213 return 'crosshair'; 214 } else { 215 return 'all-scroll'; 216 } 217 } 218 219 onDrag(x: number) { 220 if (this.dragStrategy === undefined) return; 221 this.dragStrategy.onDrag(x); 222 } 223 224 onDragStart(x: number) { 225 if (this.timeScale === undefined) return; 226 const pixelBounds = OverviewTimelinePanel.extractBounds(this.timeScale); 227 if ( 228 OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) || 229 OverviewTimelinePanel.inBorderRange(x, pixelBounds[1]) 230 ) { 231 this.dragStrategy = new BorderDragStrategy(this.timeScale, pixelBounds); 232 } else if (x < pixelBounds[0] || pixelBounds[1] < x) { 233 this.dragStrategy = new OuterDragStrategy(this.timeScale); 234 } else { 235 this.dragStrategy = new InnerDragStrategy(this.timeScale, pixelBounds); 236 } 237 this.dragStrategy.onDragStart(x); 238 } 239 240 onDragEnd() { 241 this.dragStrategy = undefined; 242 } 243 244 private static extractBounds(timeScale: TimeScale): [number, number] { 245 const vizTime = globals.timeline.visibleWindowTime; 246 return [ 247 Math.floor(timeScale.hpTimeToPx(vizTime.start)), 248 Math.ceil(timeScale.hpTimeToPx(vizTime.end)), 249 ]; 250 } 251 252 private static inBorderRange(a: number, b: number): boolean { 253 return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2; 254 } 255} 256 257// Print a timestamp in the configured time format 258function renderTimestamp( 259 ctx: CanvasRenderingContext2D, 260 time: time, 261 x: number, 262 y: number, 263 minWidth: number, 264): void { 265 const fmt = timestampFormat(); 266 switch (fmt) { 267 case TimestampFormat.UTC: 268 case TimestampFormat.TraceTz: 269 case TimestampFormat.Timecode: 270 renderTimecode(ctx, time, x, y, minWidth); 271 break; 272 case TimestampFormat.Raw: 273 ctx.fillText(time.toString(), x, y, minWidth); 274 break; 275 case TimestampFormat.RawLocale: 276 ctx.fillText(time.toLocaleString(), x, y, minWidth); 277 break; 278 case TimestampFormat.Seconds: 279 ctx.fillText(Time.formatSeconds(time), x, y, minWidth); 280 break; 281 default: 282 const z: never = fmt; 283 throw new Error(`Invalid timestamp ${z}`); 284 } 285} 286 287// Print a timecode over 2 lines with this formatting: 288// DdHH:MM:SS 289// mmm uuu nnn 290function renderTimecode( 291 ctx: CanvasRenderingContext2D, 292 time: time, 293 x: number, 294 y: number, 295 minWidth: number, 296): void { 297 const timecode = Time.toTimecode(time); 298 const {dhhmmss} = timecode; 299 ctx.fillText(dhhmmss, x, y, minWidth); 300} 301