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