// Copyright (C) 2018 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import m from 'mithril'; import {Duration, Time, TimeSpan, duration, time} from '../base/time'; import {colorForCpu} from '../components/colorizer'; import {timestampFormat} from '../core/timestamp_format'; import { OVERVIEW_TIMELINE_NON_VISIBLE_COLOR, TRACK_SHELL_WIDTH, } from './css_constants'; import {BorderDragStrategy} from './drag/border_drag_strategy'; import {DragStrategy} from './drag/drag_strategy'; import {InnerDragStrategy} from './drag/inner_drag_strategy'; import {OuterDragStrategy} from './drag/outer_drag_strategy'; import {DragGestureHandler} from '../base/drag_gesture_handler'; import { getMaxMajorTicks, MIN_PX_PER_STEP, generateTicks, TickType, } from './gridline_helper'; import {Size2D} from '../base/geom'; import {Panel} from './panel_container'; import {TimeScale} from '../base/time_scale'; import {HighPrecisionTimeSpan} from '../base/high_precision_time_span'; import {TraceImpl} from '../core/trace_impl'; import {LONG, NUM} from '../trace_processor/query_result'; import {raf} from '../core/raf_scheduler'; import {getOrCreate} from '../base/utils'; import {assertUnreachable} from '../base/logging'; import {TimestampFormat} from '../public/timeline'; const tracesData = new WeakMap(); export class OverviewTimelinePanel implements Panel { private static HANDLE_SIZE_PX = 5; readonly kind = 'panel'; readonly selectable = false; private width = 0; private gesture?: DragGestureHandler; private timeScale?: TimeScale; private dragStrategy?: DragStrategy; private readonly boundOnMouseMove = this.onMouseMove.bind(this); private readonly overviewData: OverviewDataLoader; constructor(private trace: TraceImpl) { this.overviewData = getOrCreate( tracesData, trace, () => new OverviewDataLoader(trace), ); } // Must explicitly type now; arguments types are no longer auto-inferred. // https://github.com/Microsoft/TypeScript/issues/1373 onupdate({dom}: m.CVnodeDOM) { this.width = dom.getBoundingClientRect().width; const traceTime = this.trace.traceInfo; if (this.width > TRACK_SHELL_WIDTH) { const pxBounds = {left: TRACK_SHELL_WIDTH, right: this.width}; const hpTraceTime = HighPrecisionTimeSpan.fromTime( traceTime.start, traceTime.end, ); this.timeScale = new TimeScale(hpTraceTime, pxBounds); if (this.gesture === undefined) { this.gesture = new DragGestureHandler( dom as HTMLElement, this.onDrag.bind(this), this.onDragStart.bind(this), this.onDragEnd.bind(this), ); } } else { this.timeScale = undefined; } } oncreate(vnode: m.CVnodeDOM) { this.onupdate(vnode); (vnode.dom as HTMLElement).addEventListener( 'mousemove', this.boundOnMouseMove, ); } onremove({dom}: m.CVnodeDOM) { if (this.gesture) { this.gesture[Symbol.dispose](); this.gesture = undefined; } (dom as HTMLElement).removeEventListener( 'mousemove', this.boundOnMouseMove, ); } render(): m.Children { return m('.overview-timeline', { oncreate: (vnode) => this.oncreate(vnode), onupdate: (vnode) => this.onupdate(vnode), onremove: (vnode) => this.onremove(vnode), }); } renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) { if (this.width === undefined) return; if (this.timeScale === undefined) return; const headerHeight = 20; const tracksHeight = size.height - headerHeight; const traceContext = new TimeSpan( this.trace.traceInfo.start, this.trace.traceInfo.end, ); if (size.width > TRACK_SHELL_WIDTH && traceContext.duration > 0n) { const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH); const offset = this.trace.timeline.timestampOffset(); const tickGen = generateTicks(traceContext, maxMajorTicks, offset); // Draw time labels ctx.font = '10px Roboto Condensed'; ctx.fillStyle = '#999'; for (const {type, time} of tickGen) { const xPos = Math.floor(this.timeScale.timeToPx(time)); if (xPos <= 0) continue; if (xPos > this.width) break; if (type === TickType.MAJOR) { ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5); const domainTime = this.trace.timeline.toDomainTime(time); renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP); } else if (type == TickType.MEDIUM) { ctx.fillRect(xPos - 1, 0, 1, 8); } else if (type == TickType.MINOR) { ctx.fillRect(xPos - 1, 0, 1, 5); } } } // Draw mini-tracks with quanitzed density for each process. const overviewData = this.overviewData.overviewData; if (overviewData.size > 0) { const numTracks = overviewData.size; let y = 0; const trackHeight = (tracksHeight - 1) / numTracks; for (const key of overviewData.keys()) { const loads = overviewData.get(key)!; for (let i = 0; i < loads.length; i++) { const xStart = Math.floor(this.timeScale.timeToPx(loads[i].start)); const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].end)); const yOff = Math.floor(headerHeight + y * trackHeight); const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100); const color = colorForCpu(y).setHSL({s: 50, l: lightness}); ctx.fillStyle = color.cssString; ctx.fillRect(xStart, yOff, xEnd - xStart, Math.ceil(trackHeight)); } y++; } } // Draw bottom border. ctx.fillStyle = '#dadada'; ctx.fillRect(0, size.height - 1, this.width, 1); // Draw semi-opaque rects that occlude the non-visible time range. const [vizStartPx, vizEndPx] = this.extractBounds(this.timeScale); ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR; ctx.fillRect( TRACK_SHELL_WIDTH - 1, headerHeight, vizStartPx - TRACK_SHELL_WIDTH, tracksHeight, ); ctx.fillRect(vizEndPx, headerHeight, this.width - vizEndPx, tracksHeight); // Draw brushes. ctx.fillStyle = '#999'; ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight); ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight); const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX; const hbarHeight = tracksHeight * 0.4; // Draw handlebar ctx.fillRect( vizStartPx - Math.floor(hbarWidth / 2) - 1, headerHeight, hbarWidth, hbarHeight, ); ctx.fillRect( vizEndPx - Math.floor(hbarWidth / 2), headerHeight, hbarWidth, hbarHeight, ); } private onMouseMove(e: MouseEvent) { if (this.gesture === undefined || this.gesture.isDragging) { return; } (e.target as HTMLElement).style.cursor = this.chooseCursor(e.offsetX); } private chooseCursor(x: number) { if (this.timeScale === undefined) return 'default'; const [startBound, endBound] = this.extractBounds(this.timeScale); if ( OverviewTimelinePanel.inBorderRange(x, startBound) || OverviewTimelinePanel.inBorderRange(x, endBound) ) { return 'ew-resize'; } else if (x < TRACK_SHELL_WIDTH) { return 'default'; } else if (x < startBound || endBound < x) { return 'crosshair'; } else { return 'all-scroll'; } } onDrag(x: number) { if (this.dragStrategy === undefined) return; this.dragStrategy.onDrag(x); } onDragStart(x: number) { if (this.timeScale === undefined) return; const cb = (vizTime: HighPrecisionTimeSpan) => { this.trace.timeline.updateVisibleTimeHP(vizTime); raf.scheduleCanvasRedraw(); }; const pixelBounds = this.extractBounds(this.timeScale); const timeScale = this.timeScale; if ( OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) || OverviewTimelinePanel.inBorderRange(x, pixelBounds[1]) ) { this.dragStrategy = new BorderDragStrategy(timeScale, pixelBounds, cb); } else if (x < pixelBounds[0] || pixelBounds[1] < x) { this.dragStrategy = new OuterDragStrategy(timeScale, cb); } else { this.dragStrategy = new InnerDragStrategy(timeScale, pixelBounds, cb); } this.dragStrategy.onDragStart(x); } onDragEnd() { this.dragStrategy = undefined; } private extractBounds(timeScale: TimeScale): [number, number] { const vizTime = this.trace.timeline.visibleWindow; return [ Math.floor(timeScale.hpTimeToPx(vizTime.start)), Math.ceil(timeScale.hpTimeToPx(vizTime.end)), ]; } private static inBorderRange(a: number, b: number): boolean { return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2; } } // Print a timestamp in the configured time format function renderTimestamp( ctx: CanvasRenderingContext2D, time: time, x: number, y: number, minWidth: number, ): void { const fmt = timestampFormat(); switch (fmt) { case TimestampFormat.UTC: case TimestampFormat.TraceTz: case TimestampFormat.Timecode: renderTimecode(ctx, time, x, y, minWidth); break; case TimestampFormat.TraceNs: ctx.fillText(time.toString(), x, y, minWidth); break; case TimestampFormat.TraceNsLocale: ctx.fillText(time.toLocaleString(), x, y, minWidth); break; case TimestampFormat.Seconds: ctx.fillText(Time.formatSeconds(time), x, y, minWidth); break; case TimestampFormat.Milliseconds: ctx.fillText(Time.formatMilliseconds(time), x, y, minWidth); break; case TimestampFormat.Microseconds: ctx.fillText(Time.formatMicroseconds(time), x, y, minWidth); break; default: assertUnreachable(fmt); } } // Print a timecode over 2 lines with this formatting: // DdHH:MM:SS // mmm uuu nnn function renderTimecode( ctx: CanvasRenderingContext2D, time: time, x: number, y: number, minWidth: number, ): void { const timecode = Time.toTimecode(time); const {dhhmmss} = timecode; ctx.fillText(dhhmmss, x, y, minWidth); } interface QuantizedLoad { start: time; end: time; load: number; } // Kicks of a sequence of promises that load the overiew data in steps. // Each step schedules an animation frame. class OverviewDataLoader { overviewData = new Map(); constructor(private trace: TraceImpl) { this.beginLoad(); } async beginLoad() { const traceSpan = new TimeSpan( this.trace.traceInfo.start, this.trace.traceInfo.end, ); const engine = this.trace.engine; const stepSize = Duration.max(1n, traceSpan.duration / 100n); const hasSchedSql = 'select ts from sched limit 1'; const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0; if (hasSchedOverview) { await this.loadSchedOverview(traceSpan, stepSize); } else { await this.loadSliceOverview(traceSpan, stepSize); } } async loadSchedOverview(traceSpan: TimeSpan, stepSize: duration) { const stepPromises = []; for ( let start = traceSpan.start; start < traceSpan.end; start = Time.add(start, stepSize) ) { const progress = start - traceSpan.start; const ratio = Number(progress) / Number(traceSpan.duration); this.trace.omnibox.showStatusMessage( 'Loading overview ' + `${Math.round(ratio * 100)}%`, ); const end = Time.add(start, stepSize); // The (async() => {})() queues all the 100 async promises in one batch. // Without that, we would wait for each step to be rendered before // kicking off the next one. That would interleave an animation frame // between each step, slowing down significantly the overall process. stepPromises.push( (async () => { const schedResult = await this.trace.engine.query( `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` + `where ts >= ${start} and ts < ${end} and utid != 0 ` + 'group by cpu order by cpu', ); const schedData: {[key: string]: QuantizedLoad} = {}; const it = schedResult.iter({load: NUM, cpu: NUM}); for (; it.valid(); it.next()) { const load = it.load; const cpu = it.cpu; schedData[cpu] = {start, end, load}; } this.appendData(schedData); })(), ); } // for(start = ...) await Promise.all(stepPromises); } async loadSliceOverview(traceSpan: TimeSpan, stepSize: duration) { // Slices overview. const sliceResult = await this.trace.engine.query(`select bucket, upid, ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load from thread inner join ( select ifnull(cast((ts - ${traceSpan.start})/${stepSize} as int), 0) as bucket, sum(dur) as utid_sum, utid from slice inner join thread_track on slice.track_id = thread_track.id group by bucket, utid ) using(utid) where upid is not null group by bucket, upid`); const slicesData: {[key: string]: QuantizedLoad[]} = {}; const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM}); for (; it.valid(); it.next()) { const bucket = it.bucket; const upid = it.upid; const load = it.load; const start = Time.add(traceSpan.start, stepSize * bucket); const end = Time.add(start, stepSize); const upidStr = upid.toString(); let loadArray = slicesData[upidStr]; if (loadArray === undefined) { loadArray = slicesData[upidStr] = []; } loadArray.push({start, end, load}); } this.appendData(slicesData); } appendData(data: {[key: string]: QuantizedLoad | QuantizedLoad[]}) { for (const [key, value] of Object.entries(data)) { if (!this.overviewData.has(key)) { this.overviewData.set(key, []); } if (value instanceof Array) { this.overviewData.get(key)!.push(...value); } else { this.overviewData.get(key)!.push(value); } } raf.scheduleCanvasRedraw(); } }