xref: /aosp_15_r20/external/perfetto/ui/src/frontend/flow_events_renderer.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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