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