| // 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 '../public/lib/colorizer'; |
| import {timestampFormat, 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'; |
| |
| const tracesData = new WeakMap<TraceImpl, OverviewDataLoader>(); |
| |
| 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.scheduleRedraw(); |
| }; |
| 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.Milliseoncds: |
| ctx.fillText(Time.formatMilliseconds(time), x, y, minWidth); |
| break; |
| case TimestampFormat.Microseconds: |
| ctx.fillText(Time.formatMicroseconds(time), x, y, minWidth); |
| break; |
| default: |
| const z: never = fmt; |
| throw new Error(`Invalid timestamp ${z}`); |
| } |
| } |
| |
| // 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<string, QuantizedLoad[]>(); |
| |
| 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.scheduleRedraw(); |
| } |
| } |