• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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