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