1// Copyright (C) 2020 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 {time} from '../base/time'; 16import {exists} from '../base/utils'; 17import {TrackState} from '../common/state'; 18import {SliceRect} from '../public'; 19 20import {TRACK_SHELL_WIDTH} from './css_constants'; 21import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel'; 22import {Flow, FlowPoint, globals} from './globals'; 23import {Panel} from './panel_container'; 24 25const TRACK_GROUP_CONNECTION_OFFSET = 5; 26const TRIANGLE_SIZE = 5; 27const CIRCLE_RADIUS = 3; 28const BEZIER_OFFSET = 30; 29 30const CONNECTED_FLOW_HUE = 10; 31const SELECTED_FLOW_HUE = 230; 32 33const DEFAULT_FLOW_WIDTH = 2; 34const FOCUSED_FLOW_WIDTH = 3; 35 36const HIGHLIGHTED_FLOW_INTENSITY = 45; 37const FOCUSED_FLOW_INTENSITY = 55; 38const DEFAULT_FLOW_INTENSITY = 70; 39 40type LineDirection = 'LEFT' | 'RIGHT' | 'UP' | 'DOWN'; 41type ConnectionType = 'TRACK' | 'TRACK_GROUP'; 42 43interface TrackPanelInfo { 44 panel: Panel; 45 yStart: number; 46} 47 48interface TrackGroupPanelInfo { 49 panel: Panel; 50 yStart: number; 51 height: number; 52} 53 54function getTrackIds(track: TrackState): number[] { 55 const trackDesc = globals.trackManager.resolveTrackInfo(track.uri); 56 return trackDesc?.trackIds ?? []; 57} 58 59export class FlowEventsRendererArgs { 60 trackIdToTrackPanel: Map<number, TrackPanelInfo>; 61 groupIdToTrackGroupPanel: Map<string, TrackGroupPanelInfo>; 62 63 constructor(public canvasWidth: number, public canvasHeight: number) { 64 this.trackIdToTrackPanel = new Map<number, TrackPanelInfo>(); 65 this.groupIdToTrackGroupPanel = new Map<string, TrackGroupPanelInfo>(); 66 } 67 68 registerPanel(panel: Panel, yStart: number, height: number) { 69 if (exists(panel.trackKey)) { 70 const track = globals.state.tracks[panel.trackKey]; 71 for (const trackId of getTrackIds(track)) { 72 this.trackIdToTrackPanel.set(trackId, {panel, yStart}); 73 } 74 } else if (exists(panel.groupKey)) { 75 this.groupIdToTrackGroupPanel.set(panel.groupKey, { 76 panel, 77 yStart, 78 height, 79 }); 80 } 81 } 82} 83 84export class FlowEventsRenderer { 85 private getTrackGroupIdByTrackId(trackId: number): string | undefined { 86 const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId); 87 return trackKey ? globals.state.tracks[trackKey].trackGroup : undefined; 88 } 89 90 private getTrackGroupYCoordinate( 91 args: FlowEventsRendererArgs, 92 trackId: number, 93 ): number | undefined { 94 const trackGroupId = this.getTrackGroupIdByTrackId(trackId); 95 if (!trackGroupId) { 96 return undefined; 97 } 98 const trackGroupInfo = args.groupIdToTrackGroupPanel.get(trackGroupId); 99 if (!trackGroupInfo) { 100 return undefined; 101 } 102 return ( 103 trackGroupInfo.yStart + 104 trackGroupInfo.height - 105 TRACK_GROUP_CONNECTION_OFFSET 106 ); 107 } 108 109 private getTrackYCoordinate( 110 args: FlowEventsRendererArgs, 111 trackId: number, 112 ): number | undefined { 113 return args.trackIdToTrackPanel.get(trackId)?.yStart; 114 } 115 116 private getYConnection( 117 args: FlowEventsRendererArgs, 118 trackId: number, 119 rect?: SliceRect, 120 ): {y: number; connection: ConnectionType} | undefined { 121 if (!rect) { 122 const y = this.getTrackGroupYCoordinate(args, trackId); 123 if (y === undefined) { 124 return undefined; 125 } 126 return {y, connection: 'TRACK_GROUP'}; 127 } 128 const y = 129 (this.getTrackYCoordinate(args, trackId) ?? 0) + 130 rect.top + 131 rect.height * 0.5; 132 133 return { 134 y: Math.min(Math.max(0, y), args.canvasHeight), 135 connection: 'TRACK', 136 }; 137 } 138 139 private getXCoordinate(ts: time): number { 140 return globals.timeline.visibleTimeScale.timeToPx(ts); 141 } 142 143 private getSliceRect( 144 args: FlowEventsRendererArgs, 145 point: FlowPoint, 146 ): SliceRect | undefined { 147 const trackPanel = args.trackIdToTrackPanel.get(point.trackId)?.panel; 148 if (!trackPanel) { 149 return undefined; 150 } 151 return trackPanel.getSliceRect?.( 152 point.sliceStartTs, 153 point.sliceEndTs, 154 point.depth, 155 ); 156 } 157 158 render(ctx: CanvasRenderingContext2D, args: FlowEventsRendererArgs) { 159 ctx.save(); 160 ctx.translate(TRACK_SHELL_WIDTH, 0); 161 ctx.rect(0, 0, args.canvasWidth - TRACK_SHELL_WIDTH, args.canvasHeight); 162 ctx.clip(); 163 164 globals.connectedFlows.forEach((flow) => { 165 this.drawFlow(ctx, args, flow, CONNECTED_FLOW_HUE); 166 }); 167 168 globals.selectedFlows.forEach((flow) => { 169 const categories = getFlowCategories(flow); 170 for (const cat of categories) { 171 if ( 172 globals.visibleFlowCategories.get(cat) || 173 globals.visibleFlowCategories.get(ALL_CATEGORIES) 174 ) { 175 this.drawFlow(ctx, args, flow, SELECTED_FLOW_HUE); 176 break; 177 } 178 } 179 }); 180 181 ctx.restore(); 182 } 183 184 private drawFlow( 185 ctx: CanvasRenderingContext2D, 186 args: FlowEventsRendererArgs, 187 flow: Flow, 188 hue: number, 189 ) { 190 const beginSliceRect = this.getSliceRect(args, flow.begin); 191 const endSliceRect = this.getSliceRect(args, flow.end); 192 193 const beginYConnection = this.getYConnection( 194 args, 195 flow.begin.trackId, 196 beginSliceRect, 197 ); 198 const endYConnection = this.getYConnection( 199 args, 200 flow.end.trackId, 201 endSliceRect, 202 ); 203 204 if (!beginYConnection || !endYConnection) { 205 return; 206 } 207 208 let beginDir: LineDirection = 'LEFT'; 209 let endDir: LineDirection = 'RIGHT'; 210 if (beginYConnection.connection === 'TRACK_GROUP') { 211 beginDir = beginYConnection.y > endYConnection.y ? 'DOWN' : 'UP'; 212 } 213 if (endYConnection.connection === 'TRACK_GROUP') { 214 endDir = endYConnection.y > beginYConnection.y ? 'DOWN' : 'UP'; 215 } 216 217 const begin = { 218 // If the flow goes to a descendant, we want to draw the arrow from the 219 // beginning of the slice 220 // rather from the end to avoid the flow arrow going backwards. 221 x: this.getXCoordinate( 222 flow.flowToDescendant || 223 flow.begin.sliceStartTs >= flow.end.sliceStartTs 224 ? flow.begin.sliceStartTs 225 : flow.begin.sliceEndTs, 226 ), 227 y: beginYConnection.y, 228 dir: beginDir, 229 }; 230 const end = { 231 x: this.getXCoordinate(flow.end.sliceStartTs), 232 y: endYConnection.y, 233 dir: endDir, 234 }; 235 const highlighted = 236 flow.end.sliceId === globals.state.highlightedSliceId || 237 flow.begin.sliceId === globals.state.highlightedSliceId; 238 const focused = 239 flow.id === globals.state.focusedFlowIdLeft || 240 flow.id === globals.state.focusedFlowIdRight; 241 242 let intensity = DEFAULT_FLOW_INTENSITY; 243 let width = DEFAULT_FLOW_WIDTH; 244 if (focused) { 245 intensity = FOCUSED_FLOW_INTENSITY; 246 width = FOCUSED_FLOW_WIDTH; 247 } 248 if (highlighted) { 249 intensity = HIGHLIGHTED_FLOW_INTENSITY; 250 } 251 this.drawFlowArrow(ctx, begin, end, hue, intensity, width); 252 } 253 254 private getDeltaX(dir: LineDirection, offset: number): number { 255 switch (dir) { 256 case 'LEFT': 257 return -offset; 258 case 'RIGHT': 259 return offset; 260 case 'UP': 261 return 0; 262 case 'DOWN': 263 return 0; 264 default: 265 return 0; 266 } 267 } 268 269 private getDeltaY(dir: LineDirection, offset: number): number { 270 switch (dir) { 271 case 'LEFT': 272 return 0; 273 case 'RIGHT': 274 return 0; 275 case 'UP': 276 return -offset; 277 case 'DOWN': 278 return offset; 279 default: 280 return 0; 281 } 282 } 283 284 private drawFlowArrow( 285 ctx: CanvasRenderingContext2D, 286 begin: {x: number; y: number; dir: LineDirection}, 287 end: {x: number; y: number; dir: LineDirection}, 288 hue: number, 289 intensity: number, 290 width: number, 291 ) { 292 const hasArrowHead = Math.abs(begin.x - end.x) > 3 * TRIANGLE_SIZE; 293 const END_OFFSET = 294 (end.dir === 'RIGHT' || end.dir === 'LEFT') && hasArrowHead 295 ? TRIANGLE_SIZE 296 : 0; 297 const color = `hsl(${hue}, 50%, ${intensity}%)`; 298 // draw curved line from begin to end (bezier curve) 299 ctx.strokeStyle = color; 300 ctx.lineWidth = width; 301 ctx.beginPath(); 302 ctx.moveTo(begin.x, begin.y); 303 ctx.bezierCurveTo( 304 begin.x - this.getDeltaX(begin.dir, BEZIER_OFFSET), 305 begin.y - this.getDeltaY(begin.dir, BEZIER_OFFSET), 306 end.x - this.getDeltaX(end.dir, BEZIER_OFFSET + END_OFFSET), 307 end.y - this.getDeltaY(end.dir, BEZIER_OFFSET + END_OFFSET), 308 end.x - this.getDeltaX(end.dir, END_OFFSET), 309 end.y - this.getDeltaY(end.dir, END_OFFSET), 310 ); 311 ctx.stroke(); 312 313 // TODO (andrewbb): probably we should add a parameter 'MarkerType' to be 314 // able to choose what marker we want to draw _before_ the function call. 315 // e.g. triangle, circle, square? 316 if (begin.dir !== 'RIGHT' && begin.dir !== 'LEFT') { 317 // draw a circle if we the line has a vertical connection 318 ctx.fillStyle = color; 319 ctx.beginPath(); 320 ctx.arc(begin.x, begin.y, 3, 0, 2 * Math.PI); 321 ctx.closePath(); 322 ctx.fill(); 323 } 324 325 if (end.dir !== 'RIGHT' && end.dir !== 'LEFT') { 326 // draw a circle if we the line has a vertical connection 327 ctx.fillStyle = color; 328 ctx.beginPath(); 329 ctx.arc(end.x, end.y, CIRCLE_RADIUS, 0, 2 * Math.PI); 330 ctx.closePath(); 331 ctx.fill(); 332 } else if (hasArrowHead) { 333 this.drawArrowHead(end, ctx, color); 334 } 335 } 336 337 private drawArrowHead( 338 end: {x: number; y: number; dir: LineDirection}, 339 ctx: CanvasRenderingContext2D, 340 color: string, 341 ) { 342 const dx = this.getDeltaX(end.dir, TRIANGLE_SIZE); 343 const dy = this.getDeltaY(end.dir, TRIANGLE_SIZE); 344 // draw small triangle 345 ctx.fillStyle = color; 346 ctx.beginPath(); 347 ctx.moveTo(end.x, end.y); 348 ctx.lineTo(end.x - dx - dy, end.y + dx - dy); 349 ctx.lineTo(end.x - dx + dy, end.y - dx - dy); 350 ctx.closePath(); 351 ctx.fill(); 352 } 353} 354