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'; 16import {Duration, Time, TimeSpan, duration, time} from '../base/time'; 17import {colorForCpu} from '../components/colorizer'; 18import {timestampFormat} from '../core/timestamp_format'; 19import { 20 OVERVIEW_TIMELINE_NON_VISIBLE_COLOR, 21 TRACK_SHELL_WIDTH, 22} from './css_constants'; 23import {BorderDragStrategy} from './drag/border_drag_strategy'; 24import {DragStrategy} from './drag/drag_strategy'; 25import {InnerDragStrategy} from './drag/inner_drag_strategy'; 26import {OuterDragStrategy} from './drag/outer_drag_strategy'; 27import {DragGestureHandler} from '../base/drag_gesture_handler'; 28import { 29 getMaxMajorTicks, 30 MIN_PX_PER_STEP, 31 generateTicks, 32 TickType, 33} from './gridline_helper'; 34import {Size2D} from '../base/geom'; 35import {Panel} from './panel_container'; 36import {TimeScale} from '../base/time_scale'; 37import {HighPrecisionTimeSpan} from '../base/high_precision_time_span'; 38import {TraceImpl} from '../core/trace_impl'; 39import {LONG, NUM} from '../trace_processor/query_result'; 40import {raf} from '../core/raf_scheduler'; 41import {getOrCreate} from '../base/utils'; 42import {assertUnreachable} from '../base/logging'; 43import {TimestampFormat} from '../public/timeline'; 44 45const tracesData = new WeakMap<TraceImpl, OverviewDataLoader>(); 46 47export class OverviewTimelinePanel implements Panel { 48 private static HANDLE_SIZE_PX = 5; 49 readonly kind = 'panel'; 50 readonly selectable = false; 51 private width = 0; 52 private gesture?: DragGestureHandler; 53 private timeScale?: TimeScale; 54 private dragStrategy?: DragStrategy; 55 private readonly boundOnMouseMove = this.onMouseMove.bind(this); 56 private readonly overviewData: OverviewDataLoader; 57 58 constructor(private trace: TraceImpl) { 59 this.overviewData = getOrCreate( 60 tracesData, 61 trace, 62 () => new OverviewDataLoader(trace), 63 ); 64 } 65 66 // Must explicitly type now; arguments types are no longer auto-inferred. 67 // https://github.com/Microsoft/TypeScript/issues/1373 68 onupdate({dom}: m.CVnodeDOM) { 69 this.width = dom.getBoundingClientRect().width; 70 const traceTime = this.trace.traceInfo; 71 if (this.width > TRACK_SHELL_WIDTH) { 72 const pxBounds = {left: TRACK_SHELL_WIDTH, right: this.width}; 73 const hpTraceTime = HighPrecisionTimeSpan.fromTime( 74 traceTime.start, 75 traceTime.end, 76 ); 77 this.timeScale = new TimeScale(hpTraceTime, pxBounds); 78 if (this.gesture === undefined) { 79 this.gesture = new DragGestureHandler( 80 dom as HTMLElement, 81 this.onDrag.bind(this), 82 this.onDragStart.bind(this), 83 this.onDragEnd.bind(this), 84 ); 85 } 86 } else { 87 this.timeScale = undefined; 88 } 89 } 90 91 oncreate(vnode: m.CVnodeDOM) { 92 this.onupdate(vnode); 93 (vnode.dom as HTMLElement).addEventListener( 94 'mousemove', 95 this.boundOnMouseMove, 96 ); 97 } 98 99 onremove({dom}: m.CVnodeDOM) { 100 if (this.gesture) { 101 this.gesture[Symbol.dispose](); 102 this.gesture = undefined; 103 } 104 (dom as HTMLElement).removeEventListener( 105 'mousemove', 106 this.boundOnMouseMove, 107 ); 108 } 109 110 render(): m.Children { 111 return m('.overview-timeline', { 112 oncreate: (vnode) => this.oncreate(vnode), 113 onupdate: (vnode) => this.onupdate(vnode), 114 onremove: (vnode) => this.onremove(vnode), 115 }); 116 } 117 118 renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) { 119 if (this.width === undefined) return; 120 if (this.timeScale === undefined) return; 121 122 const headerHeight = 20; 123 const tracksHeight = size.height - headerHeight; 124 const traceContext = new TimeSpan( 125 this.trace.traceInfo.start, 126 this.trace.traceInfo.end, 127 ); 128 129 if (size.width > TRACK_SHELL_WIDTH && traceContext.duration > 0n) { 130 const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH); 131 const offset = this.trace.timeline.timestampOffset(); 132 const tickGen = generateTicks(traceContext, maxMajorTicks, offset); 133 134 // Draw time labels 135 ctx.font = '10px Roboto Condensed'; 136 ctx.fillStyle = '#999'; 137 for (const {type, time} of tickGen) { 138 const xPos = Math.floor(this.timeScale.timeToPx(time)); 139 if (xPos <= 0) continue; 140 if (xPos > this.width) break; 141 if (type === TickType.MAJOR) { 142 ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5); 143 const domainTime = this.trace.timeline.toDomainTime(time); 144 renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP); 145 } else if (type == TickType.MEDIUM) { 146 ctx.fillRect(xPos - 1, 0, 1, 8); 147 } else if (type == TickType.MINOR) { 148 ctx.fillRect(xPos - 1, 0, 1, 5); 149 } 150 } 151 } 152 153 // Draw mini-tracks with quanitzed density for each process. 154 const overviewData = this.overviewData.overviewData; 155 if (overviewData.size > 0) { 156 const numTracks = overviewData.size; 157 let y = 0; 158 const trackHeight = (tracksHeight - 1) / numTracks; 159 for (const key of overviewData.keys()) { 160 const loads = overviewData.get(key)!; 161 for (let i = 0; i < loads.length; i++) { 162 const xStart = Math.floor(this.timeScale.timeToPx(loads[i].start)); 163 const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].end)); 164 const yOff = Math.floor(headerHeight + y * trackHeight); 165 const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100); 166 const color = colorForCpu(y).setHSL({s: 50, l: lightness}); 167 ctx.fillStyle = color.cssString; 168 ctx.fillRect(xStart, yOff, xEnd - xStart, Math.ceil(trackHeight)); 169 } 170 y++; 171 } 172 } 173 174 // Draw bottom border. 175 ctx.fillStyle = '#dadada'; 176 ctx.fillRect(0, size.height - 1, this.width, 1); 177 178 // Draw semi-opaque rects that occlude the non-visible time range. 179 const [vizStartPx, vizEndPx] = this.extractBounds(this.timeScale); 180 181 ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR; 182 ctx.fillRect( 183 TRACK_SHELL_WIDTH - 1, 184 headerHeight, 185 vizStartPx - TRACK_SHELL_WIDTH, 186 tracksHeight, 187 ); 188 ctx.fillRect(vizEndPx, headerHeight, this.width - vizEndPx, tracksHeight); 189 190 // Draw brushes. 191 ctx.fillStyle = '#999'; 192 ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight); 193 ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight); 194 195 const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX; 196 const hbarHeight = tracksHeight * 0.4; 197 // Draw handlebar 198 ctx.fillRect( 199 vizStartPx - Math.floor(hbarWidth / 2) - 1, 200 headerHeight, 201 hbarWidth, 202 hbarHeight, 203 ); 204 ctx.fillRect( 205 vizEndPx - Math.floor(hbarWidth / 2), 206 headerHeight, 207 hbarWidth, 208 hbarHeight, 209 ); 210 } 211 212 private onMouseMove(e: MouseEvent) { 213 if (this.gesture === undefined || this.gesture.isDragging) { 214 return; 215 } 216 (e.target as HTMLElement).style.cursor = this.chooseCursor(e.offsetX); 217 } 218 219 private chooseCursor(x: number) { 220 if (this.timeScale === undefined) return 'default'; 221 const [startBound, endBound] = this.extractBounds(this.timeScale); 222 if ( 223 OverviewTimelinePanel.inBorderRange(x, startBound) || 224 OverviewTimelinePanel.inBorderRange(x, endBound) 225 ) { 226 return 'ew-resize'; 227 } else if (x < TRACK_SHELL_WIDTH) { 228 return 'default'; 229 } else if (x < startBound || endBound < x) { 230 return 'crosshair'; 231 } else { 232 return 'all-scroll'; 233 } 234 } 235 236 onDrag(x: number) { 237 if (this.dragStrategy === undefined) return; 238 this.dragStrategy.onDrag(x); 239 } 240 241 onDragStart(x: number) { 242 if (this.timeScale === undefined) return; 243 244 const cb = (vizTime: HighPrecisionTimeSpan) => { 245 this.trace.timeline.updateVisibleTimeHP(vizTime); 246 raf.scheduleCanvasRedraw(); 247 }; 248 const pixelBounds = this.extractBounds(this.timeScale); 249 const timeScale = this.timeScale; 250 if ( 251 OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) || 252 OverviewTimelinePanel.inBorderRange(x, pixelBounds[1]) 253 ) { 254 this.dragStrategy = new BorderDragStrategy(timeScale, pixelBounds, cb); 255 } else if (x < pixelBounds[0] || pixelBounds[1] < x) { 256 this.dragStrategy = new OuterDragStrategy(timeScale, cb); 257 } else { 258 this.dragStrategy = new InnerDragStrategy(timeScale, pixelBounds, cb); 259 } 260 this.dragStrategy.onDragStart(x); 261 } 262 263 onDragEnd() { 264 this.dragStrategy = undefined; 265 } 266 267 private extractBounds(timeScale: TimeScale): [number, number] { 268 const vizTime = this.trace.timeline.visibleWindow; 269 return [ 270 Math.floor(timeScale.hpTimeToPx(vizTime.start)), 271 Math.ceil(timeScale.hpTimeToPx(vizTime.end)), 272 ]; 273 } 274 275 private static inBorderRange(a: number, b: number): boolean { 276 return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2; 277 } 278} 279 280// Print a timestamp in the configured time format 281function renderTimestamp( 282 ctx: CanvasRenderingContext2D, 283 time: time, 284 x: number, 285 y: number, 286 minWidth: number, 287): void { 288 const fmt = timestampFormat(); 289 switch (fmt) { 290 case TimestampFormat.UTC: 291 case TimestampFormat.TraceTz: 292 case TimestampFormat.Timecode: 293 renderTimecode(ctx, time, x, y, minWidth); 294 break; 295 case TimestampFormat.TraceNs: 296 ctx.fillText(time.toString(), x, y, minWidth); 297 break; 298 case TimestampFormat.TraceNsLocale: 299 ctx.fillText(time.toLocaleString(), x, y, minWidth); 300 break; 301 case TimestampFormat.Seconds: 302 ctx.fillText(Time.formatSeconds(time), x, y, minWidth); 303 break; 304 case TimestampFormat.Milliseconds: 305 ctx.fillText(Time.formatMilliseconds(time), x, y, minWidth); 306 break; 307 case TimestampFormat.Microseconds: 308 ctx.fillText(Time.formatMicroseconds(time), x, y, minWidth); 309 break; 310 default: 311 assertUnreachable(fmt); 312 } 313} 314 315// Print a timecode over 2 lines with this formatting: 316// DdHH:MM:SS 317// mmm uuu nnn 318function renderTimecode( 319 ctx: CanvasRenderingContext2D, 320 time: time, 321 x: number, 322 y: number, 323 minWidth: number, 324): void { 325 const timecode = Time.toTimecode(time); 326 const {dhhmmss} = timecode; 327 ctx.fillText(dhhmmss, x, y, minWidth); 328} 329 330interface QuantizedLoad { 331 start: time; 332 end: time; 333 load: number; 334} 335 336// Kicks of a sequence of promises that load the overiew data in steps. 337// Each step schedules an animation frame. 338class OverviewDataLoader { 339 overviewData = new Map<string, QuantizedLoad[]>(); 340 341 constructor(private trace: TraceImpl) { 342 this.beginLoad(); 343 } 344 345 async beginLoad() { 346 const traceSpan = new TimeSpan( 347 this.trace.traceInfo.start, 348 this.trace.traceInfo.end, 349 ); 350 const engine = this.trace.engine; 351 const stepSize = Duration.max(1n, traceSpan.duration / 100n); 352 const hasSchedSql = 'select ts from sched limit 1'; 353 const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0; 354 if (hasSchedOverview) { 355 await this.loadSchedOverview(traceSpan, stepSize); 356 } else { 357 await this.loadSliceOverview(traceSpan, stepSize); 358 } 359 } 360 361 async loadSchedOverview(traceSpan: TimeSpan, stepSize: duration) { 362 const stepPromises = []; 363 for ( 364 let start = traceSpan.start; 365 start < traceSpan.end; 366 start = Time.add(start, stepSize) 367 ) { 368 const progress = start - traceSpan.start; 369 const ratio = Number(progress) / Number(traceSpan.duration); 370 this.trace.omnibox.showStatusMessage( 371 'Loading overview ' + `${Math.round(ratio * 100)}%`, 372 ); 373 const end = Time.add(start, stepSize); 374 // The (async() => {})() queues all the 100 async promises in one batch. 375 // Without that, we would wait for each step to be rendered before 376 // kicking off the next one. That would interleave an animation frame 377 // between each step, slowing down significantly the overall process. 378 stepPromises.push( 379 (async () => { 380 const schedResult = await this.trace.engine.query( 381 `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` + 382 `where ts >= ${start} and ts < ${end} and utid != 0 ` + 383 'group by cpu order by cpu', 384 ); 385 const schedData: {[key: string]: QuantizedLoad} = {}; 386 const it = schedResult.iter({load: NUM, cpu: NUM}); 387 for (; it.valid(); it.next()) { 388 const load = it.load; 389 const cpu = it.cpu; 390 schedData[cpu] = {start, end, load}; 391 } 392 this.appendData(schedData); 393 })(), 394 ); 395 } // for(start = ...) 396 await Promise.all(stepPromises); 397 } 398 399 async loadSliceOverview(traceSpan: TimeSpan, stepSize: duration) { 400 // Slices overview. 401 const sliceResult = await this.trace.engine.query(`select 402 bucket, 403 upid, 404 ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load 405 from thread 406 inner join ( 407 select 408 ifnull(cast((ts - ${traceSpan.start})/${stepSize} as int), 0) as bucket, 409 sum(dur) as utid_sum, 410 utid 411 from slice 412 inner join thread_track on slice.track_id = thread_track.id 413 group by bucket, utid 414 ) using(utid) 415 where upid is not null 416 group by bucket, upid`); 417 418 const slicesData: {[key: string]: QuantizedLoad[]} = {}; 419 const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM}); 420 for (; it.valid(); it.next()) { 421 const bucket = it.bucket; 422 const upid = it.upid; 423 const load = it.load; 424 425 const start = Time.add(traceSpan.start, stepSize * bucket); 426 const end = Time.add(start, stepSize); 427 428 const upidStr = upid.toString(); 429 let loadArray = slicesData[upidStr]; 430 if (loadArray === undefined) { 431 loadArray = slicesData[upidStr] = []; 432 } 433 loadArray.push({start, end, load}); 434 } 435 this.appendData(slicesData); 436 } 437 438 appendData(data: {[key: string]: QuantizedLoad | QuantizedLoad[]}) { 439 for (const [key, value] of Object.entries(data)) { 440 if (!this.overviewData.has(key)) { 441 this.overviewData.set(key, []); 442 } 443 if (value instanceof Array) { 444 this.overviewData.get(key)!.push(...value); 445 } else { 446 this.overviewData.get(key)!.push(value); 447 } 448 } 449 raf.scheduleCanvasRedraw(); 450 } 451} 452