blob: 909dcb7f3bf9c20b990e7a0afc935d4eb8827bbb [file] [log] [blame]
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {TRACK_SHELL_WIDTH} from './css_constants';
import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel';
import {Flow, FlowPoint, globals} from './globals';
import {PanelVNode} from './panel';
import {SliceRect} from './track';
import {TrackGroupPanel} from './track_group_panel';
import {TrackPanel} from './track_panel';
const TRACK_GROUP_CONNECTION_OFFSET = 5;
const TRIANGLE_SIZE = 5;
const CIRCLE_RADIUS = 3;
const BEZIER_OFFSET = 30;
const CONNECTED_FLOW_HUE = 10;
const SELECTED_FLOW_HUE = 230;
const DEFAULT_FLOW_WIDTH = 2;
const FOCUSED_FLOW_WIDTH = 3;
const HIGHLIGHTED_FLOW_INTENSITY = 45;
const FOCUSED_FLOW_INTENSITY = 55;
const DEFAULT_FLOW_INTENSITY = 70;
type LineDirection = 'LEFT'|'RIGHT'|'UP'|'DOWN';
type ConnectionType = 'TRACK'|'TRACK_GROUP';
interface TrackPanelInfo {
panel: TrackPanel;
yStart: number;
}
interface TrackGroupPanelInfo {
panel: TrackGroupPanel;
yStart: number;
height: number;
}
function hasTrackId(obj: {}): obj is {trackId: number} {
return (obj as {trackId?: number}).trackId !== undefined;
}
function hasManyTrackIds(obj: {}): obj is {trackIds: number[]} {
return (obj as {trackIds?: number}).trackIds !== undefined;
}
function hasId(obj: {}): obj is {id: number} {
return (obj as {id?: number}).id !== undefined;
}
function hasTrackGroupId(obj: {}): obj is {trackGroupId: string} {
return (obj as {trackGroupId?: string}).trackGroupId !== undefined;
}
export class FlowEventsRendererArgs {
trackIdToTrackPanel: Map<number, TrackPanelInfo>;
groupIdToTrackGroupPanel: Map<string, TrackGroupPanelInfo>;
constructor(public canvasWidth: number, public canvasHeight: number) {
this.trackIdToTrackPanel = new Map<number, TrackPanelInfo>();
this.groupIdToTrackGroupPanel = new Map<string, TrackGroupPanelInfo>();
}
registerPanel(panel: PanelVNode, yStart: number, height: number) {
if (panel.state instanceof TrackPanel && hasId(panel.attrs)) {
const config = globals.state.tracks[panel.attrs.id].config;
if (hasTrackId(config)) {
this.trackIdToTrackPanel.set(
config.trackId, {panel: panel.state, yStart});
}
if (hasManyTrackIds(config)) {
for (const trackId of config.trackIds) {
this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart});
}
}
} else if (
panel.state instanceof TrackGroupPanel &&
hasTrackGroupId(panel.attrs)) {
this.groupIdToTrackGroupPanel.set(
panel.attrs.trackGroupId, {panel: panel.state, yStart, height});
}
}
}
export class FlowEventsRenderer {
private getTrackGroupIdByTrackId(trackId: number): string|undefined {
const uiTrackId = globals.state.uiTrackIdByTraceTrackId[trackId];
return uiTrackId ? globals.state.tracks[uiTrackId].trackGroup : undefined;
}
private getTrackGroupYCoordinate(
args: FlowEventsRendererArgs, trackId: number): number|undefined {
const trackGroupId = this.getTrackGroupIdByTrackId(trackId);
if (!trackGroupId) {
return undefined;
}
const trackGroupInfo = args.groupIdToTrackGroupPanel.get(trackGroupId);
if (!trackGroupInfo) {
return undefined;
}
return trackGroupInfo.yStart + trackGroupInfo.height -
TRACK_GROUP_CONNECTION_OFFSET;
}
private getTrackYCoordinate(args: FlowEventsRendererArgs, trackId: number):
number|undefined {
return args.trackIdToTrackPanel.get(trackId) ?.yStart;
}
private getYConnection(
args: FlowEventsRendererArgs, trackId: number,
rect?: SliceRect): {y: number, connection: ConnectionType}|undefined {
if (!rect) {
const y = this.getTrackGroupYCoordinate(args, trackId);
if (y === undefined) {
return undefined;
}
return {y, connection: 'TRACK_GROUP'};
}
const y = (this.getTrackYCoordinate(args, trackId) || 0) + rect.top +
rect.height * 0.5;
return {
y: Math.min(Math.max(0, y), args.canvasHeight),
connection: 'TRACK',
};
}
private getXCoordinate(ts: number): number {
return globals.frontendLocalState.timeScale.timeToPx(ts);
}
private getSliceRect(args: FlowEventsRendererArgs, point: FlowPoint):
SliceRect|undefined {
const trackPanel = args.trackIdToTrackPanel.get(point.trackId) ?.panel;
if (!trackPanel) {
return undefined;
}
return trackPanel.getSliceRect(
point.sliceStartTs, point.sliceEndTs, point.depth);
}
render(ctx: CanvasRenderingContext2D, args: FlowEventsRendererArgs) {
ctx.save();
ctx.translate(TRACK_SHELL_WIDTH, 0);
ctx.rect(0, 0, args.canvasWidth - TRACK_SHELL_WIDTH, args.canvasHeight);
ctx.clip();
globals.connectedFlows.forEach((flow) => {
this.drawFlow(ctx, args, flow, CONNECTED_FLOW_HUE);
});
globals.selectedFlows.forEach((flow) => {
const categories = getFlowCategories(flow);
for (const cat of categories) {
if (globals.visibleFlowCategories.get(cat) ||
globals.visibleFlowCategories.get(ALL_CATEGORIES)) {
this.drawFlow(ctx, args, flow, SELECTED_FLOW_HUE);
break;
}
}
});
ctx.restore();
}
private drawFlow(
ctx: CanvasRenderingContext2D, args: FlowEventsRendererArgs, flow: Flow,
hue: number) {
const beginSliceRect = this.getSliceRect(args, flow.begin);
const endSliceRect = this.getSliceRect(args, flow.end);
const beginYConnection =
this.getYConnection(args, flow.begin.trackId, beginSliceRect);
const endYConnection =
this.getYConnection(args, flow.end.trackId, endSliceRect);
if (!beginYConnection || !endYConnection) {
return;
}
let beginDir: LineDirection = 'LEFT';
let endDir: LineDirection = 'RIGHT';
if (beginYConnection.connection === 'TRACK_GROUP') {
beginDir = beginYConnection.y > endYConnection.y ? 'DOWN' : 'UP';
}
if (endYConnection.connection === 'TRACK_GROUP') {
endDir = endYConnection.y > beginYConnection.y ? 'DOWN' : 'UP';
}
const begin = {
x: this.getXCoordinate(flow.begin.sliceEndTs),
y: beginYConnection.y,
dir: beginDir,
};
const end = {
x: this.getXCoordinate(flow.end.sliceStartTs),
y: endYConnection.y,
dir: endDir,
};
const highlighted = flow.end.sliceId === globals.state.highlightedSliceId ||
flow.begin.sliceId === globals.state.highlightedSliceId;
const focused = flow.id === globals.state.focusedFlowIdLeft ||
flow.id === globals.state.focusedFlowIdRight;
let intensity = DEFAULT_FLOW_INTENSITY;
let width = DEFAULT_FLOW_WIDTH;
if (focused) {
intensity = FOCUSED_FLOW_INTENSITY;
width = FOCUSED_FLOW_WIDTH;
}
if (highlighted) {
intensity = HIGHLIGHTED_FLOW_INTENSITY;
}
this.drawFlowArrow(ctx, begin, end, hue, intensity, width);
}
private getDeltaX(dir: LineDirection, offset: number): number {
switch (dir) {
case 'LEFT':
return -offset;
case 'RIGHT':
return offset;
case 'UP':
return 0;
case 'DOWN':
return 0;
default:
return 0;
}
}
private getDeltaY(dir: LineDirection, offset: number): number {
switch (dir) {
case 'LEFT':
return 0;
case 'RIGHT':
return 0;
case 'UP':
return -offset;
case 'DOWN':
return offset;
default:
return 0;
}
}
private drawFlowArrow(
ctx: CanvasRenderingContext2D,
begin: {x: number, y: number, dir: LineDirection},
end: {x: number, y: number, dir: LineDirection}, hue: number,
intensity: number, width: number) {
const hasArrowHead = Math.abs(begin.x - end.x) > 3 * TRIANGLE_SIZE;
const END_OFFSET =
(((end.dir === 'RIGHT' || end.dir === 'LEFT') && hasArrowHead) ?
TRIANGLE_SIZE :
0);
const color = `hsl(${hue}, 50%, ${intensity}%)`;
// draw curved line from begin to end (bezier curve)
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.beginPath();
ctx.moveTo(begin.x, begin.y);
ctx.bezierCurveTo(
begin.x - this.getDeltaX(begin.dir, BEZIER_OFFSET),
begin.y - this.getDeltaY(begin.dir, BEZIER_OFFSET),
end.x - this.getDeltaX(end.dir, BEZIER_OFFSET + END_OFFSET),
end.y - this.getDeltaY(end.dir, BEZIER_OFFSET + END_OFFSET),
end.x - this.getDeltaX(end.dir, END_OFFSET),
end.y - this.getDeltaY(end.dir, END_OFFSET));
ctx.stroke();
// TODO (andrewbb): probably we should add a parameter 'MarkerType' to be
// able to choose what marker we want to draw _before_ the function call.
// e.g. triangle, circle, square?
if (begin.dir !== 'RIGHT' && begin.dir !== 'LEFT') {
// draw a circle if we the line has a vertical connection
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(begin.x, begin.y, 3, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
if (end.dir !== 'RIGHT' && end.dir !== 'LEFT') {
// draw a circle if we the line has a vertical connection
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(end.x, end.y, CIRCLE_RADIUS, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
} else if (hasArrowHead) {
this.drawArrowHead(end, ctx, color);
}
}
private drawArrowHead(
end: {x: number; y: number; dir: LineDirection},
ctx: CanvasRenderingContext2D, color: string) {
const dx = this.getDeltaX(end.dir, TRIANGLE_SIZE);
const dy = this.getDeltaY(end.dir, TRIANGLE_SIZE);
// draw small triangle
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(end.x, end.y);
ctx.lineTo(end.x - dx - dy, end.y + dx - dy);
ctx.lineTo(end.x - dx + dy, end.y - dx - dy);
ctx.closePath();
ctx.fill();
}
}