// 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 * as m from 'mithril'; import {assertExists} from '../base/logging'; import {hueForCpu} from '../common/colorizer'; import {TimeSpan, timeToString} from '../common/time'; import {SIDEBAR_WIDTH, 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 './drag_gesture_handler'; import {globals} from './globals'; import {Panel, PanelSize} from './panel'; import {TimeScale} from './time_scale'; export class OverviewTimelinePanel extends Panel { private static HANDLE_SIZE_PX = 7; private width = 0; private gesture?: DragGestureHandler; private timeScale?: TimeScale; private totTime = new TimeSpan(0, 0); private dragStrategy?: DragStrategy; private readonly boundOnMouseMove = this.onMouseMove.bind(this); // 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; this.totTime = new TimeSpan( globals.state.traceTime.startSec, globals.state.traceTime.endSec); this.timeScale = new TimeScale( this.totTime, [TRACK_SHELL_WIDTH, assertExists(this.width)]); if (this.gesture === undefined) { this.gesture = new DragGestureHandler( dom as HTMLElement, this.onDrag.bind(this), this.onDragStart.bind(this), this.onDragEnd.bind(this)); } } oncreate(vnode: m.CVnodeDOM) { this.onupdate(vnode); (vnode.dom as HTMLElement) .addEventListener('mousemove', this.boundOnMouseMove); } onremove({dom}: m.CVnodeDOM) { (dom as HTMLElement) .removeEventListener('mousemove', this.boundOnMouseMove); } view() { return m('.overview-timeline'); } renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) { if (this.width === undefined) return; if (this.timeScale === undefined) return; const headerHeight = 25; const tracksHeight = size.height - headerHeight; // Draw time labels on the top header. ctx.font = '10px Roboto Condensed'; ctx.fillStyle = '#999'; for (let i = 0; i < 100; i++) { const xPos = (i * (this.width - TRACK_SHELL_WIDTH) / 100) + TRACK_SHELL_WIDTH; const t = this.timeScale.pxToTime(xPos); if (xPos <= 0) continue; if (xPos > this.width) break; if (i % 10 === 0) { ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5); ctx.fillText(timeToString(t - this.totTime.start), xPos + 5, 18); } else { ctx.fillRect(xPos - 1, 0, 1, 5); } } // Draw mini-tracks with quanitzed density for each process. if (globals.overviewStore.size > 0) { const numTracks = globals.overviewStore.size; let y = 0; const trackHeight = (tracksHeight - 1) / numTracks; for (const key of globals.overviewStore.keys()) { const loads = globals.overviewStore.get(key)!; for (let i = 0; i < loads.length; i++) { const xStart = Math.floor(this.timeScale.timeToPx(loads[i].startSec)); const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].endSec)); const yOff = Math.floor(headerHeight + y * trackHeight); const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100); ctx.fillStyle = `hsl(${hueForCpu(y)}, 50%, ${lightness}%)`; 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] = OverviewTimelinePanel.extractBounds(this.timeScale); ctx.fillStyle = 'rgba(200, 200, 200, 0.8)'; 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 hbarDivisionFactor = 3.5; // Draw handlebar ctx.fillRect( vizStartPx - Math.floor(hbarWidth / 2) - 1, headerHeight, hbarWidth, tracksHeight / hbarDivisionFactor); ctx.fillRect( vizEndPx - Math.floor(hbarWidth / 2), headerHeight, hbarWidth, tracksHeight / hbarDivisionFactor); } private onMouseMove(e: MouseEvent) { if (this.gesture === undefined || this.gesture.isDragging) { return; } (e.target as HTMLElement).style.cursor = this.chooseCursor(e.x); } private chooseCursor(x: number) { if (this.timeScale === undefined) return 'default'; const [vizStartPx, vizEndPx] = OverviewTimelinePanel.extractBounds(this.timeScale); const startBound = vizStartPx - 1 + SIDEBAR_WIDTH; const endBound = vizEndPx + SIDEBAR_WIDTH; if (OverviewTimelinePanel.inBorderRange(x, startBound) || OverviewTimelinePanel.inBorderRange(x, endBound)) { return 'ew-resize'; } else if (x < SIDEBAR_WIDTH + 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 pixelBounds = OverviewTimelinePanel.extractBounds(this.timeScale); if (OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) || OverviewTimelinePanel.inBorderRange(x, pixelBounds[1])) { this.dragStrategy = new BorderDragStrategy(this.timeScale, pixelBounds); } else if (x < pixelBounds[0] || pixelBounds[1] < x) { this.dragStrategy = new OuterDragStrategy(this.timeScale); } else { this.dragStrategy = new InnerDragStrategy(this.timeScale, pixelBounds); } this.dragStrategy.onDragStart(x); } onDragEnd() { this.dragStrategy = undefined; } private static extractBounds(timeScale: TimeScale): [number, number] { const vizTime = globals.frontendLocalState.getVisibleStateBounds(); return [ Math.floor(timeScale.timeToPx(vizTime[0])), Math.ceil(timeScale.timeToPx(vizTime[1])) ]; } private static inBorderRange(a: number, b: number): boolean { return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2; } }