1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use size 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 {canvasClip} from '../../base/canvas_utils'; 17import {Size2D} from '../../base/geom'; 18import {assertUnreachable} from '../../base/logging'; 19import {time, Time} from '../../base/time'; 20import {TimeScale} from '../../base/time_scale'; 21import {formatDuration} from '../../components/time_utils'; 22import {timestampFormat} from '../../core/timestamp_format'; 23import {TraceImpl} from '../../core/trace_impl'; 24import {TimestampFormat} from '../../public/timeline'; 25import { 26 BACKGROUND_COLOR, 27 FOREGROUND_COLOR, 28 TRACK_SHELL_WIDTH, 29} from '../css_constants'; 30import {generateTicks, getMaxMajorTicks, TickType} from './gridline_helper'; 31 32export interface BBox { 33 x: number; 34 y: number; 35 width: number; 36 height: number; 37} 38 39// Draws a vertical line with two horizontal tails at the left and right and 40// a label in the middle. It looks a bit like a stretched H: 41// |--- Label ---| 42// The |target| bounding box determines where to draw the H. 43// The |bounds| bounding box gives the visible region, this is used to adjust 44// the positioning of the label to ensure it is on screen. 45function drawHBar( 46 ctx: CanvasRenderingContext2D, 47 target: BBox, 48 bounds: BBox, 49 label: string, 50) { 51 ctx.fillStyle = FOREGROUND_COLOR; 52 53 const xLeft = Math.floor(target.x); 54 const xRight = Math.floor(target.x + target.width); 55 const yMid = Math.floor(target.height / 2 + target.y); 56 const xWidth = xRight - xLeft; 57 58 // Don't draw in the track shell. 59 ctx.beginPath(); 60 ctx.rect(bounds.x, bounds.y, bounds.width, bounds.height); 61 ctx.clip(); 62 63 // Draw horizontal bar of the H. 64 ctx.fillRect(xLeft, yMid, xWidth, 1); 65 // Draw left vertical bar of the H. 66 ctx.fillRect(xLeft, target.y, 1, target.height); 67 // Draw right vertical bar of the H. 68 ctx.fillRect(xRight, target.y, 1, target.height); 69 70 const labelWidth = ctx.measureText(label).width; 71 72 // Find a good position for the label: 73 // By default put the label in the middle of the H: 74 let labelXLeft = Math.floor(xWidth / 2 - labelWidth / 2 + xLeft); 75 76 if ( 77 labelWidth > target.width || 78 labelXLeft < bounds.x || 79 labelXLeft + labelWidth > bounds.x + bounds.width 80 ) { 81 // It won't fit in the middle or would be at least partly out of bounds 82 // so put it either to the left or right: 83 if (xRight > bounds.x + bounds.width) { 84 // If the H extends off the right side of the screen the label 85 // goes on the left of the H. 86 labelXLeft = xLeft - labelWidth - 3; 87 } else { 88 // Otherwise the label goes on the right of the H. 89 labelXLeft = xRight + 3; 90 } 91 } 92 93 ctx.fillStyle = BACKGROUND_COLOR; 94 ctx.fillRect(labelXLeft - 1, 0, labelWidth + 1, target.height); 95 96 ctx.textBaseline = 'middle'; 97 ctx.fillStyle = FOREGROUND_COLOR; 98 ctx.font = '10px Roboto Condensed'; 99 ctx.fillText(label, labelXLeft, yMid); 100} 101 102function drawIBar( 103 ctx: CanvasRenderingContext2D, 104 xPos: number, 105 bounds: BBox, 106 label: string, 107) { 108 if (xPos < bounds.x) return; 109 110 ctx.fillStyle = FOREGROUND_COLOR; 111 ctx.fillRect(xPos, 0, 1, bounds.width); 112 113 const yMid = Math.floor(bounds.height / 2 + bounds.y); 114 const labelWidth = ctx.measureText(label).width; 115 const padding = 3; 116 117 let xPosLabel; 118 if (xPos + padding + labelWidth > bounds.width) { 119 xPosLabel = xPos - padding; 120 ctx.textAlign = 'right'; 121 } else { 122 xPosLabel = xPos + padding; 123 ctx.textAlign = 'left'; 124 } 125 126 ctx.fillStyle = BACKGROUND_COLOR; 127 ctx.fillRect(xPosLabel - 1, 0, labelWidth + 2, bounds.height); 128 129 ctx.textBaseline = 'middle'; 130 ctx.fillStyle = FOREGROUND_COLOR; 131 ctx.font = '10px Roboto Condensed'; 132 ctx.fillText(label, xPosLabel, yMid); 133} 134 135export class TimeSelectionPanel { 136 readonly height = 10; 137 138 constructor(private readonly trace: TraceImpl) {} 139 140 render(): m.Children { 141 return m('', {style: {height: `${this.height}px`}}); 142 } 143 144 renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) { 145 ctx.fillStyle = '#999'; 146 ctx.fillRect(TRACK_SHELL_WIDTH - 1, 0, 1, size.height); 147 148 const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH}; 149 150 ctx.save(); 151 ctx.translate(TRACK_SHELL_WIDTH, 0); 152 canvasClip(ctx, 0, 0, trackSize.width, trackSize.height); 153 this.renderPanel(ctx, trackSize); 154 ctx.restore(); 155 } 156 157 private renderPanel(ctx: CanvasRenderingContext2D, size: Size2D): void { 158 const visibleWindow = this.trace.timeline.visibleWindow; 159 const timescale = new TimeScale(visibleWindow, { 160 left: 0, 161 right: size.width, 162 }); 163 const timespan = visibleWindow.toTimeSpan(); 164 165 if (size.width > 0 && timespan.duration > 0n) { 166 const maxMajorTicks = getMaxMajorTicks(size.width); 167 const offset = this.trace.timeline.timestampOffset(); 168 const tickGen = generateTicks(timespan, maxMajorTicks, offset); 169 for (const {type, time} of tickGen) { 170 const px = Math.floor(timescale.timeToPx(time)); 171 if (type === TickType.MAJOR) { 172 ctx.fillRect(px, 0, 1, size.height); 173 } 174 } 175 } 176 177 const localSpan = this.trace.timeline.selectedSpan; 178 const selection = this.trace.selection.selection; 179 if (localSpan !== undefined) { 180 const start = Time.min(localSpan.start, localSpan.end); 181 const end = Time.max(localSpan.start, localSpan.end); 182 this.renderSpan(ctx, timescale, size, start, end); 183 } else { 184 if (selection.kind === 'area') { 185 const start = Time.min(selection.start, selection.end); 186 const end = Time.max(selection.start, selection.end); 187 this.renderSpan(ctx, timescale, size, start, end); 188 } else if ( 189 selection.kind === 'track_event' && 190 selection.dur !== undefined 191 ) { 192 const start = selection.ts; 193 const end = Time.add(selection.ts, selection.dur); 194 if (end > start) { 195 this.renderSpan(ctx, timescale, size, start, end); 196 } 197 } 198 } 199 200 if (this.trace.timeline.hoverCursorTimestamp !== undefined) { 201 this.renderHover( 202 ctx, 203 timescale, 204 size, 205 this.trace.timeline.hoverCursorTimestamp, 206 ); 207 } 208 209 for (const note of this.trace.notes.notes.values()) { 210 const noteIsSelected = 211 selection.kind === 'note' && selection.id === note.id; 212 if (note.noteType === 'SPAN' && noteIsSelected) { 213 this.renderSpan(ctx, timescale, size, note.start, note.end); 214 } 215 } 216 217 ctx.restore(); 218 } 219 220 renderHover( 221 ctx: CanvasRenderingContext2D, 222 timescale: TimeScale, 223 size: Size2D, 224 ts: time, 225 ) { 226 const xPos = Math.floor(timescale.timeToPx(ts)); 227 const domainTime = this.trace.timeline.toDomainTime(ts); 228 const label = stringifyTimestamp(domainTime); 229 drawIBar(ctx, xPos, this.getBBoxFromSize(size), label); 230 } 231 232 renderSpan( 233 ctx: CanvasRenderingContext2D, 234 timescale: TimeScale, 235 trackSize: Size2D, 236 start: time, 237 end: time, 238 ) { 239 const xLeft = timescale.timeToPx(start); 240 const xRight = timescale.timeToPx(end); 241 const label = formatDuration(this.trace, end - start); 242 drawHBar( 243 ctx, 244 { 245 x: xLeft, 246 y: 0, 247 width: xRight - xLeft, 248 height: trackSize.height, 249 }, 250 this.getBBoxFromSize(trackSize), 251 label, 252 ); 253 } 254 255 private getBBoxFromSize(size: Size2D): BBox { 256 return { 257 x: 0, 258 y: 0, 259 width: size.width, 260 height: size.height, 261 }; 262 } 263} 264 265function stringifyTimestamp(time: time): string { 266 const fmt = timestampFormat(); 267 switch (fmt) { 268 case TimestampFormat.UTC: 269 case TimestampFormat.TraceTz: 270 case TimestampFormat.Timecode: 271 const THIN_SPACE = '\u2009'; 272 return Time.toTimecode(time).toString(THIN_SPACE); 273 case TimestampFormat.TraceNs: 274 return time.toString(); 275 case TimestampFormat.TraceNsLocale: 276 return time.toLocaleString(); 277 case TimestampFormat.Seconds: 278 return Time.formatSeconds(time); 279 case TimestampFormat.Milliseconds: 280 return Time.formatMilliseconds(time); 281 case TimestampFormat.Microseconds: 282 return Time.formatMicroseconds(time); 283 default: 284 assertUnreachable(fmt); 285 } 286} 287