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