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