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 {ArrowHeadStyle, drawBezierArrow} from '../../base/bezier_arrow'; 16import { 17 HorizontalBounds, 18 Point2D, 19 Size2D, 20 VerticalBounds, 21} from '../../base/geom'; 22import {TimeScale} from '../../base/time_scale'; 23import {ALL_CATEGORIES, Flow, getFlowCategories} from '../../core/flow_types'; 24import {TraceImpl} from '../../core/trace_impl'; 25import {TrackNode} from '../../public/workspace'; 26 27const TRACK_GROUP_CONNECTION_OFFSET = 5; 28const TRIANGLE_SIZE = 5; 29const CIRCLE_RADIUS = 3; 30const BEZIER_OFFSET = 30; 31 32const CONNECTED_FLOW_HUE = 10; 33const SELECTED_FLOW_HUE = 230; 34 35const DEFAULT_FLOW_WIDTH = 2; 36const FOCUSED_FLOW_WIDTH = 3; 37 38const HIGHLIGHTED_FLOW_INTENSITY = 45; 39const FOCUSED_FLOW_INTENSITY = 55; 40const DEFAULT_FLOW_INTENSITY = 70; 41 42type VerticalEdgeOrPoint = 43 | ({kind: 'vertical_edge'} & Point2D) 44 | ({kind: 'point'} & Point2D); 45 46export interface TrackInfo { 47 readonly node: TrackNode; 48 readonly verticalBounds: VerticalBounds; 49} 50 51/** 52 * Renders the flows overlay on top of the timeline, given the set of panels and 53 * a canvas to draw on. 54 * 55 * Note: the actual flow data is retrieved from trace.flows, which are produced 56 * by FlowManager. 57 * 58 * @param trace - The Trace instance, which holds onto the FlowManager. 59 * @param ctx - The canvas to draw on. 60 * @param size - The size of the canvas. 61 * @param tracks - A list of tracks and their vertical positions on the canvas. 62 * @param trackRoot - The root node of the tracks - used to find tracks quickly 63 * by URI. 64 * @param timescale - The current timescale used to convert flow timings into 65 * canvas positions. 66 * 67 */ 68export function renderFlows( 69 trace: TraceImpl, 70 ctx: CanvasRenderingContext2D, 71 size: Size2D, 72 tracks: ReadonlyArray<TrackInfo>, 73 trackRoot: TrackNode, 74 timescale: TimeScale, 75): void { 76 // Create an index of track node instances to panels. This doesn't need to be 77 // a WeakMap because it's thrown away every render cycle. 78 const trackInfoByNode = new Map( 79 tracks.map((trackInfo) => [trackInfo.node, trackInfo]), 80 ); 81 82 const drawFlow = (flow: Flow, hue: number) => { 83 const flowStartTs = 84 flow.flowToDescendant || flow.begin.sliceStartTs >= flow.end.sliceStartTs 85 ? flow.begin.sliceStartTs 86 : flow.begin.sliceEndTs; 87 88 const flowEndTs = flow.end.sliceStartTs; 89 90 const startX = timescale.timeToPx(flowStartTs); 91 const endX = timescale.timeToPx(flowEndTs); 92 93 const flowBounds = { 94 left: Math.min(startX, endX), 95 right: Math.max(startX, endX), 96 }; 97 98 if (!isInViewport(flowBounds, size)) { 99 return; 100 } 101 102 const highlighted = 103 flow.end.sliceId === trace.timeline.highlightedSliceId || 104 flow.begin.sliceId === trace.timeline.highlightedSliceId; 105 const focused = 106 flow.id === trace.flows.focusedFlowIdLeft || 107 flow.id === trace.flows.focusedFlowIdRight; 108 109 let intensity = DEFAULT_FLOW_INTENSITY; 110 let width = DEFAULT_FLOW_WIDTH; 111 if (focused) { 112 intensity = FOCUSED_FLOW_INTENSITY; 113 width = FOCUSED_FLOW_WIDTH; 114 } 115 if (highlighted) { 116 intensity = HIGHLIGHTED_FLOW_INTENSITY; 117 } 118 119 const start = getConnectionTarget( 120 flow.begin.trackUri, 121 flow.begin.depth, 122 startX, 123 ); 124 const end = getConnectionTarget(flow.end.trackUri, flow.end.depth, endX); 125 126 if (start && end) { 127 drawArrow(ctx, start, end, intensity, hue, width); 128 } 129 }; 130 131 const getConnectionTarget = ( 132 trackUri: string | undefined, 133 depth: number, 134 x: number, 135 ): VerticalEdgeOrPoint | undefined => { 136 if (trackUri === undefined) { 137 return undefined; 138 } 139 140 const track = trackRoot.getTrackByUri(trackUri); 141 if (!track) { 142 return undefined; 143 } 144 145 const trackPanel = trackInfoByNode.get(track); 146 if (trackPanel) { 147 const trackRect = trackPanel.verticalBounds; 148 const sliceRectRaw = trace.tracks 149 .getTrack(trackUri) 150 ?.track.getSliceVerticalBounds?.(depth); 151 if (sliceRectRaw) { 152 const sliceRect = { 153 top: sliceRectRaw.top + trackRect.top, 154 bottom: sliceRectRaw.bottom + trackRect.top, 155 }; 156 return { 157 kind: 'vertical_edge', 158 x, 159 y: (sliceRect.top + sliceRect.bottom) / 2, 160 }; 161 } else { 162 // Slice bounds are not available for this track, so just put the target 163 // in the middle of the track 164 return { 165 kind: 'vertical_edge', 166 x, 167 y: (trackRect.top + trackRect.bottom) / 2, 168 }; 169 } 170 } else { 171 // If we didn't find a track, it might inside a group, so check for the group 172 const containerNode = track.findClosestVisibleAncestor(); 173 const groupPanel = trackInfoByNode.get(containerNode); 174 if (groupPanel) { 175 return { 176 kind: 'point', 177 x, 178 y: groupPanel.verticalBounds.bottom - TRACK_GROUP_CONNECTION_OFFSET, 179 }; 180 } 181 } 182 183 return undefined; 184 }; 185 186 // Render the connected flows 187 trace.flows.connectedFlows.forEach((flow) => { 188 drawFlow(flow, CONNECTED_FLOW_HUE); 189 }); 190 191 // Render the selected flows 192 trace.flows.selectedFlows.forEach((flow) => { 193 const categories = getFlowCategories(flow); 194 for (const cat of categories) { 195 if ( 196 trace.flows.visibleCategories.get(cat) || 197 trace.flows.visibleCategories.get(ALL_CATEGORIES) 198 ) { 199 drawFlow(flow, SELECTED_FLOW_HUE); 200 break; 201 } 202 } 203 }); 204} 205 206// Check if an object defined by the horizontal bounds |bounds| is inside the 207// viewport defined by |viewportSizeZ. 208function isInViewport(bounds: HorizontalBounds, viewportSize: Size2D): boolean { 209 return bounds.right >= 0 && bounds.left < viewportSize.width; 210} 211 212function drawArrow( 213 ctx: CanvasRenderingContext2D, 214 start: VerticalEdgeOrPoint, 215 end: VerticalEdgeOrPoint, 216 intensity: number, 217 hue: number, 218 width: number, 219): void { 220 ctx.strokeStyle = `hsl(${hue}, 50%, ${intensity}%)`; 221 ctx.fillStyle = `hsl(${hue}, 50%, ${intensity}%)`; 222 ctx.lineWidth = width; 223 224 // TODO(stevegolton): Consider vertical distance too 225 const roomForArrowHead = Math.abs(start.x - end.x) > 3 * TRIANGLE_SIZE; 226 227 let startStyle: ArrowHeadStyle; 228 if (start.kind === 'vertical_edge') { 229 startStyle = { 230 orientation: 'east', 231 shape: 'none', 232 }; 233 } else { 234 startStyle = { 235 orientation: 'auto_vertical', 236 shape: 'circle', 237 size: CIRCLE_RADIUS, 238 }; 239 } 240 241 let endStyle: ArrowHeadStyle; 242 if (end.kind === 'vertical_edge') { 243 endStyle = { 244 orientation: 'west', 245 shape: roomForArrowHead ? 'triangle' : 'none', 246 size: TRIANGLE_SIZE, 247 }; 248 } else { 249 endStyle = { 250 orientation: 'auto_vertical', 251 shape: 'circle', 252 size: CIRCLE_RADIUS, 253 }; 254 } 255 256 drawBezierArrow(ctx, start, end, BEZIER_OFFSET, startStyle, endStyle); 257} 258