Commit f61d1afe authored by Danylo Boiko's avatar Danylo Boiko Committed by V8 LUCI CQ

[turbolizer] Turboshaft view initial commit

General:
- Graph view refactoring
Turboshaft:
- Blocks representation
- Inline nodes representation
- Minimum required turboshaft toolbox actions
- Layout caching

Bug: v8:7327
Change-Id: I2ac07965ac775c68c522cfc9367b7ce0ff18672a
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3726287Reviewed-by: 's avatarNico Hartmann <nicohartmann@chromium.org>
Commit-Queue: Danylo Boiko <danielboyko02@gmail.com>
Cr-Commit-Position: refs/heads/main@{#81553}
parent 6639962a
g.turboshaft-block rect {
stroke-dasharray: 20;
stroke-width: 7;
}
g.turboshaft-block:hover rect {
stroke-dasharray: 0;
stroke-width: 10;
}
g.block rect {
fill: #ecf3fe;
stroke: #4285f4;
}
g.merge rect {
fill: #e9fcee;
stroke: #2bde5a;
}
g.loop rect {
fill: #fdecea;
stroke: #e94235;
}
.block-label tspan {
font-weight: bold;
}
.block .block-label tspan {
fill: #4285f4;
}
.merge .block-label tspan {
fill: #34a853;
}
.loop .block-label tspan {
fill: #ea4335;
}
.inline-node-properties tspan {
fill: #9227b0;
}
This diff was suppressed by a .gitattributes entry.
......@@ -10,6 +10,7 @@ code is governed by a BSD-style license that can be found in the LICENSE file.
<link rel="stylesheet" href="css/turbo-visualizer.css">
<link rel="stylesheet" href="css/turbo-visualizer-ranges.css">
<link rel="stylesheet" href="css/tabs.css">
<link rel="stylesheet" href="css/turboshaft.css">
<link rel="icon" href="turbolizer.png">
</head>
......
......@@ -4,8 +4,10 @@
import { GraphNode } from "./phases/graph-phase/graph-node";
import { TurboshaftGraphNode } from "./phases/turboshaft-graph-phase/turboshaft-graph-node";
import { TurboshaftGraphBlock } from "./phases/turboshaft-graph-phase/turboshaft-graph-block";
export abstract class Edge<NodeType extends GraphNode | TurboshaftGraphNode> {
export abstract class Edge<NodeType extends GraphNode | TurboshaftGraphNode
| TurboshaftGraphBlock> {
target: NodeType;
source: NodeType;
backEdgeNumber: number;
......
......@@ -36,7 +36,7 @@ export class GraphLayout {
this.graph.graphPhase.rendered = true;
}
public fullRebuild(showTypes: boolean): void {
private fullRebuild(showTypes: boolean): void {
this.startTime = performance.now();
this.maxRank = 0;
this.visitOrderWithinRank = 0;
......@@ -56,7 +56,7 @@ export class GraphLayout {
this.graph.graphPhase.stateType = GraphStateType.Cached;
}
public cachedRebuild(): void {
private cachedRebuild(): void {
this.calculateBackEdgeNumbers();
}
......@@ -176,7 +176,7 @@ export class GraphLayout {
const rankSets = new Array<Array<GraphNode>>();
for (const node of this.graph.nodes()) {
node.y = node.rank * (C.DEFAULT_NODE_ROW_SEPARATION +
node.getNodeHeight(showTypes) + 2 * C.DEFAULT_NODE_BUBBLE_RADIUS);
node.getHeight(showTypes) + 2 * C.DEFAULT_NODE_BUBBLE_RADIUS);
if (node.visible) {
if (!rankSets[node.rank]) {
rankSets[node.rank] = new Array<GraphNode>(node);
......@@ -204,8 +204,7 @@ export class GraphLayout {
for (const node of rankSet) {
if (node.visible) {
node.x = this.graphOccupation.occupyNode(node);
const nodeTotalWidth = node.getTotalNodeWidth();
this.trace(`Node ${node.id} is placed between [${node.x}, ${node.x + nodeTotalWidth})`);
this.trace(`Node ${node.id} is placed between [${node.x}, ${node.x + node.getWidth()})`);
const staggeredFlooredI = Math.floor(placedCount++ % 3);
const delta = C.MINIMUM_EDGE_SEPARATION * staggeredFlooredI;
node.outputApproach += delta;
......@@ -288,7 +287,7 @@ class GraphOccupation {
}
public occupyNode(node: GraphNode): number {
const width = node.getTotalNodeWidth();
const width = node.getWidth();
const margin = C.MINIMUM_EDGE_SEPARATION;
const paddedWidth = width + 2 * margin;
const [direction, position] = this.getPlacementHint(node);
......
......@@ -6,28 +6,15 @@ import * as C from "./common/constants";
import { GraphPhase, GraphStateType } from "./phases/graph-phase/graph-phase";
import { GraphEdge } from "./phases/graph-phase/graph-edge";
import { GraphNode } from "./phases/graph-phase/graph-node";
import { MovableContainer } from "./movable-container";
export class Graph {
graphPhase: GraphPhase;
export class Graph extends MovableContainer<GraphPhase> {
nodeMap: Array<GraphNode>;
minGraphX: number;
maxGraphX: number;
minGraphY: number;
maxGraphY: number;
maxGraphNodeX: number;
maxBackEdgeNumber: number;
width: number;
height: number;
constructor(graphPhase: GraphPhase) {
this.graphPhase = graphPhase;
super(graphPhase);
this.nodeMap = graphPhase.nodeIdToNodeMap;
this.minGraphX = 0;
this.maxGraphX = 1;
this.minGraphY = 0;
this.maxGraphY = 1;
this.width = 1;
this.height = 1;
}
public *nodes(func = (n: GraphNode) => true) {
......@@ -65,12 +52,11 @@ export class Graph {
if (!node.visible) continue;
this.minGraphX = Math.min(this.minGraphX, node.x);
this.maxGraphNodeX = Math.max(this.maxGraphNodeX,
node.x + node.getTotalNodeWidth());
this.maxGraphNodeX = Math.max(this.maxGraphNodeX, node.x + node.getWidth());
this.minGraphY = Math.min(this.minGraphY, node.y - C.NODE_INPUT_WIDTH);
this.maxGraphY = Math.max(this.maxGraphY,
node.y + node.getNodeHeight(showTypes) + C.NODE_INPUT_WIDTH);
this.maxGraphY = Math.max(this.maxGraphY, node.y + node.getHeight(showTypes)
+ C.NODE_INPUT_WIDTH);
}
this.maxGraphX = this.maxGraphNodeX + this.maxBackEdgeNumber
......@@ -97,8 +83,4 @@ export class Graph {
);
}
}
public isRendered(): boolean {
return this.graphPhase.rendered;
}
}
......@@ -13,6 +13,7 @@ import { GraphPhase } from "./phases/graph-phase/graph-phase";
import { GraphNode } from "./phases/graph-phase/graph-node";
import { storageGetItem, storageSetItem } from "./common/util";
import { PhaseType } from "./phases/phase";
import { TurboshaftGraphView } from "./views/turboshaft-graph-view";
const toolboxHTML = `
<div class="graph-toolbox">
......@@ -28,6 +29,7 @@ export class GraphMultiView extends View {
sourceResolver: SourceResolver;
selectionBroker: SelectionBroker;
graph: GraphView;
turboshaftGraph: TurboshaftGraphView;
schedule: ScheduleView;
sequence: SequenceView;
selectMenu: HTMLSelectElement;
......@@ -59,6 +61,8 @@ export class GraphMultiView extends View {
searchInput.setAttribute("value", storageGetItem("lastSearch", "", false));
this.graph = new GraphView(this.divNode, selectionBroker, view.displayPhaseByName.bind(this),
toolbox.querySelector(".graph-toolbox"));
this.turboshaftGraph = new TurboshaftGraphView(this.divNode, selectionBroker,
view.displayPhaseByName.bind(this), toolbox.querySelector(".graph-toolbox"));
this.schedule = new ScheduleView(this.divNode, selectionBroker);
this.sequence = new SequenceView(this.divNode, selectionBroker);
this.selectMenu = toolbox.querySelector("#phase-select") as HTMLSelectElement;
......@@ -96,6 +100,8 @@ export class GraphMultiView extends View {
private displayPhase(phase: GenericPhase, selection?: Map<string, GraphNode>): void {
if (phase.type == PhaseType.Graph) {
this.displayPhaseView(this.graph, phase, selection);
} else if (phase.type == PhaseType.TurboshaftGraph) {
this.displayPhaseView(this.turboshaftGraph, phase, selection);
} else if (phase.type == PhaseType.Schedule) {
this.displayPhaseView(this.schedule, phase, selection);
} else if (phase.type == PhaseType.Sequence) {
......@@ -120,7 +126,7 @@ export class GraphMultiView extends View {
let nextPhaseIndex = this.selectMenu.selectedIndex + 1;
while (nextPhaseIndex < this.sourceResolver.phases.length) {
const nextPhase = this.sourceResolver.getPhase(nextPhaseIndex);
if (nextPhase.type == PhaseType.Graph) {
if (nextPhase.isGraph()) {
this.selectMenu.selectedIndex = nextPhaseIndex;
storageSetItem("lastSelectedPhase", nextPhaseIndex);
this.displayPhase(nextPhase);
......@@ -134,7 +140,7 @@ export class GraphMultiView extends View {
let previousPhaseIndex = this.selectMenu.selectedIndex - 1;
while (previousPhaseIndex >= 0) {
const previousPhase = this.sourceResolver.getPhase(previousPhaseIndex);
if (previousPhase.type === PhaseType.Graph) {
if (previousPhase.isGraph()) {
this.selectMenu.selectedIndex = previousPhaseIndex;
storageSetItem("lastSelectedPhase", previousPhaseIndex);
this.displayPhase(previousPhase);
......
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { GraphPhase } from "./phases/graph-phase/graph-phase";
import { TurboshaftGraphPhase } from "./phases/turboshaft-graph-phase/turboshaft-graph-phase";
export abstract class MovableContainer<GraphPhaseType extends GraphPhase | TurboshaftGraphPhase> {
graphPhase: GraphPhaseType;
minGraphX: number;
maxGraphX: number;
maxGraphNodeX: number;
minGraphY: number;
maxGraphY: number;
width: number;
height: number;
public abstract redetermineGraphBoundingBox(extendHeight: boolean):
[[number, number], [number, number]];
constructor(graphPhase: GraphPhaseType) {
this.graphPhase = graphPhase;
this.minGraphX = 0;
this.maxGraphX = 1;
this.minGraphY = 0;
this.maxGraphY = 1;
this.width = 1;
this.height = 1;
}
public isRendered(): boolean {
return this.graphPhase.rendered;
}
}
......@@ -2,11 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as C from "./common/constants";
import { measureText } from "./common/util";
import { GraphEdge } from "./phases/graph-phase/graph-edge";
import { TurboshaftGraphEdge } from "./phases/turboshaft-graph-phase/turboshaft-graph-edge";
import { TurboshaftGraphNode } from "./phases/turboshaft-graph-phase/turboshaft-graph-node";
import { TurboshaftGraphBlock } from "./phases/turboshaft-graph-phase/turboshaft-graph-block";
export abstract class Node<EdgeType extends GraphEdge | TurboshaftGraphEdge> {
export abstract class Node<EdgeType extends GraphEdge | TurboshaftGraphEdge<TurboshaftGraphNode
| TurboshaftGraphBlock>> {
id: number;
displayLabel: string;
inputs: Array<EdgeType>;
......@@ -15,7 +19,9 @@ export abstract class Node<EdgeType extends GraphEdge | TurboshaftGraphEdge> {
x: number;
y: number;
labelBox: { width: number, height: number };
visitOrderWithinRank: number;
public abstract getHeight(extendHeight: boolean): number;
public abstract getWidth(): number;
constructor(id: number, displayLabel?: string) {
this.id = id;
......@@ -26,20 +32,22 @@ export abstract class Node<EdgeType extends GraphEdge | TurboshaftGraphEdge> {
this.x = 0;
this.y = 0;
this.labelBox = measureText(this.displayLabel);
this.visitOrderWithinRank = 0;
}
public areAnyOutputsVisible(): number {
// TODO (danylo boiko) Move 0, 1, 2 logic to enum
public areAnyOutputsVisible(): OutputVisibilityType {
let visibleCount = 0;
for (const edge of this.outputs) {
if (edge.isVisible()) {
++visibleCount;
}
}
if (this.outputs.length === visibleCount) return 2;
if (visibleCount !== 0) return 1;
return 0;
if (this.outputs.length == visibleCount) {
return OutputVisibilityType.AllNodesVisible;
}
if (visibleCount != 0) {
return OutputVisibilityType.SomeNodesVisible;
}
return OutputVisibilityType.NoVisibleNodes;
}
public setOutputVisibility(visibility: boolean): boolean {
......@@ -64,6 +72,15 @@ export abstract class Node<EdgeType extends GraphEdge | TurboshaftGraphEdge> {
return false;
}
public getInputX(index: number): number {
return this.getWidth() - (C.NODE_INPUT_WIDTH / 2) +
(index - this.inputs.length + 1) * C.NODE_INPUT_WIDTH;
}
public getOutputX(): number {
return this.getWidth() - (C.NODE_INPUT_WIDTH / 2);
}
public identifier(): string {
return `${this.id}`;
}
......@@ -72,3 +89,9 @@ export abstract class Node<EdgeType extends GraphEdge | TurboshaftGraphEdge> {
return `N${this.id}`;
}
}
export enum OutputVisibilityType {
NoVisibleNodes,
SomeNodesVisible,
AllNodesVisible
}
......@@ -32,7 +32,7 @@ export class GraphEdge extends Edge<GraphNode> {
}
const inputOffset = C.MINIMUM_EDGE_SEPARATION * (index + 1);
return target.x < source.x
? target.x + target.getTotalNodeWidth() + inputOffset
? target.x + target.getWidth() + inputOffset
: target.x - inputOffset;
}
......@@ -43,7 +43,7 @@ export class GraphEdge extends Edge<GraphNode> {
const arrowheadHeight = 7;
const inputY = target.y - 2 * C.DEFAULT_NODE_BUBBLE_RADIUS - arrowheadHeight;
const outputX = source.x + source.getOutputX();
const outputY = source.y + source.getNodeHeight(showTypes) + C.DEFAULT_NODE_BUBBLE_RADIUS;
const outputY = source.y + source.getHeight(showTypes) + C.DEFAULT_NODE_BUBBLE_RADIUS;
let inputApproach = target.getInputApproach(this.index);
const outputApproach = source.getOutputApproach(showTypes);
const horizontalPos = this.getInputHorizontalPosition(graph, showTypes);
......
......@@ -15,6 +15,7 @@ export class GraphNode extends Node<GraphEdge> {
cfg: boolean;
width: number;
normalHeight: number;
visitOrderWithinRank: number;
constructor(nodeLabel: NodeLabel) {
super(nodeLabel.id, nodeLabel.getDisplayLabel());
......@@ -28,6 +29,18 @@ export class GraphNode extends Node<GraphEdge> {
this.width = alignUp(innerWidth + C.NODE_INPUT_WIDTH * 2, C.NODE_INPUT_WIDTH);
const innerHeight = Math.max(this.labelBox.height, typeBox.height);
this.normalHeight = innerHeight + 20;
this.visitOrderWithinRank = 0;
}
public getHeight(showTypes: boolean): number {
if (showTypes) {
return this.normalHeight + this.labelBox.height;
}
return this.normalHeight;
}
public getWidth(): number {
return Math.max(this.inputs.length * C.NODE_INPUT_WIDTH, this.width);
}
public isControl(): boolean {
......@@ -68,10 +81,6 @@ export class GraphNode extends Node<GraphEdge> {
this.isJavaScript() || this.isSimplified());
}
public getTotalNodeWidth(): number {
return Math.max(this.inputs.length * C.NODE_INPUT_WIDTH, this.width);
}
public getTitle(): string {
return this.nodeLabel.getTitle();
}
......@@ -105,27 +114,11 @@ export class GraphNode extends Node<GraphEdge> {
(index % 4) * C.MINIMUM_EDGE_SEPARATION - C.DEFAULT_NODE_BUBBLE_RADIUS;
}
public getNodeHeight(showTypes: boolean): number {
if (showTypes) {
return this.normalHeight + this.labelBox.height;
}
return this.normalHeight;
}
public getOutputApproach(showTypes: boolean): number {
return this.y + this.outputApproach + this.getNodeHeight(showTypes) +
return this.y + this.outputApproach + this.getHeight(showTypes) +
+ C.DEFAULT_NODE_BUBBLE_RADIUS;
}
public getInputX(index: number): number {
return this.getTotalNodeWidth() - (C.NODE_INPUT_WIDTH / 2) +
(index - this.inputs.length + 1) * C.NODE_INPUT_WIDTH;
}
public getOutputX(): number {
return this.getTotalNodeWidth() - (C.NODE_INPUT_WIDTH / 2);
}
public hasBackEdges(): boolean {
return (this.nodeLabel.opcode === "Loop") ||
((this.nodeLabel.opcode === "Phi" || this.nodeLabel.opcode === "EffectPhi" ||
......
......@@ -10,6 +10,11 @@ export abstract class Phase {
this.name = name;
this.type = type;
}
public isGraph(): boolean {
return this.type == PhaseType.Graph ||
this.type == PhaseType.TurboshaftGraph;
}
}
export enum PhaseType {
......@@ -20,3 +25,8 @@ export enum PhaseType {
Sequence = "sequence",
Schedule = "schedule"
}
export enum GraphStateType {
NeedToFullRebuild,
Cached
}
......@@ -3,21 +3,38 @@
// found in the LICENSE file.
import { TurboshaftGraphNode } from "./turboshaft-graph-node";
import { Node } from "../../node";
import { TurboshaftGraphEdge } from "./turboshaft-graph-edge";
export class TurboshaftGraphBlock {
id: string;
export class TurboshaftGraphBlock extends Node<TurboshaftGraphEdge<TurboshaftGraphBlock>> {
type: TurboshaftGraphBlockType;
deferred: boolean;
predecessors: Array<string>;
nodes: Array<TurboshaftGraphNode>;
constructor(id: string, type: TurboshaftGraphBlockType, deferred: boolean,
constructor(id: number, type: TurboshaftGraphBlockType, deferred: boolean,
predecessors: Array<string>) {
this.id = id;
super(id, `${type} ${id}${deferred ? " (deferred)" : ""}`);
this.type = type;
this.deferred = deferred;
this.predecessors = predecessors ?? new Array<string>();
this.nodes = new Array<TurboshaftGraphNode>();
this.visible = true;
}
public getHeight(showProperties: boolean): number {
return this.nodes.reduce<number>((accumulator: number, node: TurboshaftGraphNode) => {
return accumulator + node.getHeight(showProperties);
}, this.labelBox.height);
}
public getWidth(): number {
const maxWidth = Math.max(...this.nodes.map((node: TurboshaftGraphNode) => node.getWidth()));
return Math.max(maxWidth, this.labelBox.width) + 50;
}
public toString(): string {
return `B${this.id}`;
}
}
......
......@@ -4,9 +4,18 @@
import { TurboshaftGraphNode } from "./turboshaft-graph-node";
import { Edge } from "../../edge";
import { TurboshaftGraphBlock } from "./turboshaft-graph-block";
export class TurboshaftGraphEdge extends Edge<TurboshaftGraphNode> {
constructor(target: TurboshaftGraphNode, source: TurboshaftGraphNode) {
export class TurboshaftGraphEdge<Type extends TurboshaftGraphNode | TurboshaftGraphBlock> extends
Edge<Type> {
constructor(target: Type, source: Type) {
super(target, source);
this.visible = target.visible && source.visible;
}
public toString(idx?: number): string {
if (idx !== null) return `${this.source.id},${idx},${this.target.id}`;
return `${this.source.id},${this.target.id}`;
}
}
......@@ -2,19 +2,78 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as C from "../../common/constants";
import { measureText } from "../../common/util";
import { TurboshaftGraphEdge } from "./turboshaft-graph-edge";
import { TurboshaftGraphBlock } from "./turboshaft-graph-block";
import { Node } from "../../node";
export class TurboshaftGraphNode extends Node<TurboshaftGraphEdge> {
export class TurboshaftGraphNode extends Node<TurboshaftGraphEdge<TurboshaftGraphNode>> {
title: string;
block: TurboshaftGraphBlock;
opPropertiesType: OpPropertiesType;
properties: string;
constructor(id: number, title: string, block: TurboshaftGraphBlock, properties: string) {
super(id);
constructor(id: number, title: string, block: TurboshaftGraphBlock,
opPropertiesType: OpPropertiesType, properties: string) {
super(id, `${id} ${title}`);
this.title = title;
this.block = block;
this.opPropertiesType = opPropertiesType;
this.properties = properties;
this.visible = true;
}
public getHeight(showProperties: boolean): number {
if (this.properties && showProperties) {
return this.labelBox.height * 2;
}
return this.labelBox.height;
}
public getWidth(): number {
const measure = measureText(
`${this.getInlineLabel()}[${this.getPropertiesTypeAbbreviation()}]`
);
return Math.max(this.inputs.length * C.NODE_INPUT_WIDTH, measure.width);
}
public getInlineLabel(): string {
if (this.inputs.length == 0) return `${this.id} ${this.title}`;
return `${this.id} ${this.title}(${this.inputs.map(i => i.source.id).join(",")})`;
}
public getReadableProperties(blockWidth: number): string {
const propertiesWidth = measureText(this.properties).width;
if (blockWidth > propertiesWidth) return this.properties;
const widthOfOneSymbol = Math.floor(propertiesWidth / this.properties.length);
const lengthOfReadableProperties = Math.floor(blockWidth / widthOfOneSymbol);
return `${this.properties.slice(0, lengthOfReadableProperties - 3)}..`;
}
public getPropertiesTypeAbbreviation(): string {
switch (this.opPropertiesType) {
case OpPropertiesType.Pure:
return "P";
case OpPropertiesType.Reading:
return "R";
case OpPropertiesType.Writing:
return "W";
case OpPropertiesType.CanDeopt:
return "CD";
case OpPropertiesType.AnySideEffects:
return "ASE";
case OpPropertiesType.BlockTerminator:
return "BT";
}
}
}
export enum OpPropertiesType {
Pure = "Pure",
Reading = "Reading",
Writing = "Writing",
CanDeopt = "CanDeopt",
AnySideEffects = "AnySideEffects",
BlockTerminator = "BlockTerminator"
}
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { Phase, PhaseType } from "../phase";
import { GraphStateType, Phase, PhaseType } from "../phase";
import { TurboshaftGraphNode } from "./turboshaft-graph-node";
import { TurboshaftGraphEdge } from "./turboshaft-graph-edge";
import { TurboshaftGraphBlock } from "./turboshaft-graph-block";
......@@ -10,8 +10,12 @@ import { TurboshaftGraphBlock } from "./turboshaft-graph-block";
export class TurboshaftGraphPhase extends Phase {
highestBlockId: number;
data: TurboshaftGraphData;
stateType: GraphStateType;
layoutType: TurboshaftLayoutType;
nodeIdToNodeMap: Array<TurboshaftGraphNode>;
blockIdToBlockMap: Array<TurboshaftGraphBlock>;
rendered: boolean;
transform: { x: number, y: number, scale: number };
constructor(name: string, highestBlockId: number, data?: TurboshaftGraphData,
nodeIdToNodeMap?: Array<TurboshaftGraphNode>,
......@@ -19,8 +23,11 @@ export class TurboshaftGraphPhase extends Phase {
super(name, PhaseType.TurboshaftGraph);
this.highestBlockId = highestBlockId;
this.data = data ?? new TurboshaftGraphData();
this.stateType = GraphStateType.NeedToFullRebuild;
this.layoutType = TurboshaftLayoutType.Inline;
this.nodeIdToNodeMap = nodeIdToNodeMap ?? new Array<TurboshaftGraphNode>();
this.blockIdToBlockMap = blockIdToBlockMap ?? new Array<TurboshaftGraphBlock>();
this.rendered = false;
}
public parseDataFromJSON(dataJson): void {
......@@ -32,18 +39,29 @@ export class TurboshaftGraphPhase extends Phase {
private parseBlocksFromJSON(blocksJson): void {
for (const blockJson of blocksJson) {
const block = new TurboshaftGraphBlock(blockJson.id, blockJson.type,
// TODO (danylo boiko) Change type of block id in JSON output
const numId = Number(blockJson.id.substring(1));
const block = new TurboshaftGraphBlock(numId, blockJson.type,
blockJson.deferred, blockJson.predecessors);
this.data.blocks.push(block);
this.blockIdToBlockMap[block.id] = block;
}
for (const block of this.blockIdToBlockMap) {
for (const predecessor of block.predecessors) {
const source = this.blockIdToBlockMap[Number(predecessor.substring(1))];
const edge = new TurboshaftGraphEdge(block, source);
block.inputs.push(edge);
source.outputs.push(edge);
}
}
}
private parseNodesFromJSON(nodesJson): void {
for (const nodeJson of nodesJson) {
const block = this.blockIdToBlockMap[nodeJson.block_id];
const numId = Number(nodeJson.block_id.substring(1));
const block = this.blockIdToBlockMap[numId];
const node = new TurboshaftGraphNode(nodeJson.id, nodeJson.title,
block, nodeJson.properties);
block, nodeJson.op_properties_type, nodeJson.properties);
block.nodes.push(node);
this.data.nodes.push(node);
this.nodeIdToNodeMap[node.id] = node;
......@@ -64,13 +82,19 @@ export class TurboshaftGraphPhase extends Phase {
export class TurboshaftGraphData {
nodes: Array<TurboshaftGraphNode>;
edges: Array<TurboshaftGraphEdge>;
edges: Array<TurboshaftGraphEdge<TurboshaftGraphNode>>;
blocks: Array<TurboshaftGraphBlock>;
constructor(nodes?: Array<TurboshaftGraphNode>, edges?: Array<TurboshaftGraphEdge>,
constructor(nodes?: Array<TurboshaftGraphNode>,
edges?: Array<TurboshaftGraphEdge<TurboshaftGraphNode>>,
blocks?: Array<TurboshaftGraphBlock>) {
this.nodes = nodes ?? new Array<TurboshaftGraphNode>();
this.edges = edges ?? new Array<TurboshaftGraphEdge>();
this.edges = edges ?? new Array<TurboshaftGraphEdge<TurboshaftGraphNode>>();
this.blocks = blocks ?? new Array<TurboshaftGraphBlock>();
}
}
export enum TurboshaftLayoutType {
Inline,
Nodes
}
import { TurboshaftGraph } from "./turboshaft-graph";
import { GraphStateType } from "./phases/phase";
import { TurboshaftLayoutType } from "./phases/turboshaft-graph-phase/turboshaft-graph-phase";
export class TurboshaftGraphLayout {
graph: TurboshaftGraph;
constructor(graph: TurboshaftGraph) {
this.graph = graph;
}
public rebuild(showProperties: boolean): void {
if (this.graph.graphPhase.stateType == GraphStateType.NeedToFullRebuild) {
switch (this.graph.graphPhase.layoutType) {
case TurboshaftLayoutType.Inline:
this.inlineRebuild(showProperties);
break;
default:
throw "Unsupported graph layout type";
}
this.graph.graphPhase.stateType = GraphStateType.Cached;
}
this.graph.graphPhase.rendered = true;
}
private inlineRebuild(showProperties): void {
// Useless logic to simulate blocks coordinates (will be replaced in the future)
let x = 0;
let y = 0;
for (const block of this.graph.blockMap) {
block.x = x;
block.y = y;
y += block.getHeight(showProperties) + 50;
if (y > 1800) {
x += block.getWidth() * 1.5;
y = 0;
}
}
}
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as C from "./common/constants";
import { MovableContainer } from "./movable-container";
import { TurboshaftGraphPhase } from "./phases/turboshaft-graph-phase/turboshaft-graph-phase";
import { TurboshaftGraphNode } from "./phases/turboshaft-graph-phase/turboshaft-graph-node";
import { TurboshaftGraphBlock } from "./phases/turboshaft-graph-phase/turboshaft-graph-block";
import { TurboshaftGraphEdge } from "./phases/turboshaft-graph-phase/turboshaft-graph-edge";
export class TurboshaftGraph extends MovableContainer<TurboshaftGraphPhase> {
blockMap: Array<TurboshaftGraphBlock>;
nodeMap: Array<TurboshaftGraphNode>;
constructor(graphPhase: TurboshaftGraphPhase) {
super(graphPhase);
this.blockMap = graphPhase.blockIdToBlockMap;
this.nodeMap = graphPhase.nodeIdToNodeMap;
}
public *blocks(func = (b: TurboshaftGraphBlock) => true) {
for (const block of this.blockMap) {
if (!block || !func(block)) continue;
yield block;
}
}
public *nodes(func = (n: TurboshaftGraphNode) => true) {
for (const node of this.nodeMap) {
if (!node || !func(node)) continue;
yield node;
}
}
public *blocksEdges(func = (e: TurboshaftGraphEdge<TurboshaftGraphBlock>) => true) {
for (const block of this.blockMap) {
if (!block) continue;
for (const edge of block.inputs) {
if (!edge || func(edge)) continue;
yield edge;
}
}
}
public redetermineGraphBoundingBox(showProperties: boolean):
[[number, number], [number, number]] {
this.minGraphX = 0;
this.maxGraphNodeX = 1;
this.minGraphY = 0;
this.maxGraphY = 1;
for (const block of this.blocks()) {
if (!block.visible) continue;
this.minGraphX = Math.min(this.minGraphX, block.x);
this.maxGraphNodeX = Math.max(this.maxGraphNodeX, block.x + block.getWidth());
this.minGraphY = Math.min(this.minGraphY, block.y - C.NODE_INPUT_WIDTH);
this.maxGraphY = Math.max(this.maxGraphY, block.y + block.getHeight(showProperties)
+ C.NODE_INPUT_WIDTH);
}
this.maxGraphX = this.maxGraphNodeX + 3 * C.MINIMUM_EDGE_SEPARATION;
this.width = this.maxGraphX - this.minGraphX;
this.height = this.maxGraphY - this.minGraphY;
return [
[this.minGraphX - this.width / 2, this.minGraphY - this.height / 2],
[this.maxGraphX + this.width / 2, this.maxGraphY + this.height / 2]
];
}
}
......@@ -4,315 +4,417 @@
import * as C from "../common/constants";
import * as d3 from "d3";
import { partial, storageGetItem, storageSetItem } from "../common/util";
import { PhaseView } from "./view";
import { partial, storageSetItem } from "../common/util";
import { SelectionMap } from "../selection/selection";
import { ClearableHandler, NodeSelectionHandler } from "../selection/selection-handler";
import { Graph } from "../graph";
import { SelectionBroker } from "../selection/selection-broker";
import { GraphNode } from "../phases/graph-phase/graph-node";
import { GraphEdge } from "../phases/graph-phase/graph-edge";
import { GraphLayout } from "../graph-layout";
import { GraphPhase, GraphStateType } from "../phases/graph-phase/graph-phase";
import { GraphData, GraphPhase, GraphStateType } from "../phases/graph-phase/graph-phase";
import { BytecodePosition } from "../position";
import { BytecodeOrigin } from "../origin";
import { MovableView } from "./movable-view";
import { ClearableHandler, NodeSelectionHandler } from "../selection/selection-handler";
import { GenericPosition } from "../source-resolver";
import { OutputVisibilityType } from "../node";
interface GraphState {
showTypes: boolean;
selection: SelectionMap;
mouseDownNode: any;
justDragged: boolean;
justScaleTransGraph: boolean;
hideDead: boolean;
}
export class GraphView extends PhaseView {
divElement: d3.Selection<any, any, any, any>;
svg: d3.Selection<any, any, any, any>;
showPhaseByName: (p: string, s: Set<any>) => void;
state: GraphState;
selectionHandler: NodeSelectionHandler & ClearableHandler;
graphElement: d3.Selection<any, any, any, any>;
export class GraphView extends MovableView<Graph> {
graphLayout: GraphLayout;
visibleNodes: d3.Selection<any, GraphNode, any, any>;
visibleEdges: d3.Selection<any, GraphEdge, any, any>;
drag: d3.DragBehavior<any, GraphNode, GraphNode>;
panZoom: d3.ZoomBehavior<SVGElement, any>;
visibleBubbles: d3.Selection<any, any, any, any>;
transitionTimout: number;
graph: Graph;
graphLayout: GraphLayout;
broker: SelectionBroker;
phaseName: string;
toolbox: HTMLElement;
createViewElement() {
const pane = document.createElement('div');
pane.setAttribute('id', "graph");
return pane;
}
drag: d3.DragBehavior<any, GraphNode, GraphNode>;
constructor(idOrContainer: string | HTMLElement, broker: SelectionBroker,
showPhaseByName: (s: string) => void, toolbox: HTMLElement) {
super(idOrContainer);
const view = this;
this.broker = broker;
this.showPhaseByName = showPhaseByName;
this.divElement = d3.select(this.divNode);
this.phaseName = "";
this.toolbox = toolbox;
const svg = this.divElement.append("svg")
.attr('version', '2.0')
.attr("width", "100%")
.attr("height", "100%");
svg.on("click", function (d) {
view.selectionHandler.clear();
});
// Listen for key events. Note that the focus handler seems
// to be important even if it does nothing.
svg
.on("focus", e => { })
.on("keydown", e => { view.svgKeyDown(); });
showPhaseByName: (name: string) => void, toolbox: HTMLElement) {
super(idOrContainer, broker, showPhaseByName, toolbox);
view.svg = svg;
this.state.selection = new SelectionMap(node => node.identifier(),
node => node.nodeLabel?.origin?.identifier());
this.state = {
selection: null,
mouseDownNode: null,
justDragged: false,
justScaleTransGraph: false,
showTypes: false,
hideDead: false
};
this.nodesSelectionHandler = this.initializeNodesSelectionHandler();
this.svg.on("click", () => this.nodesSelectionHandler.clear());
this.selectionHandler = {
clear: function () {
view.state.selection.clear();
broker.broadcastClear(this);
view.updateGraphVisibility();
},
select: function (nodes: Array<GraphNode>, selected: boolean) {
const locations = [];
for (const node of nodes) {
if (node.nodeLabel.sourcePosition) {
locations.push(node.nodeLabel.sourcePosition);
}
if (node.nodeLabel.origin && node.nodeLabel.origin instanceof BytecodeOrigin) {
locations.push(new BytecodePosition(node.nodeLabel.origin.bytecodePosition));
}
}
view.state.selection.select(nodes, selected);
broker.broadcastSourcePositionSelect(this, locations, selected);
view.updateGraphVisibility();
},
brokeredNodeSelect: function (locations, selected: boolean) {
if (!view.graph) return;
const selection = view.graph.nodes(n => {
return locations.has(n.identifier())
&& (!view.state.hideDead || n.isLive());
});
view.state.selection.select(selection, selected);
// Update edge visibility based on selection.
for (const n of view.graph.nodes()) {
if (view.state.selection.isSelected(n)) {
n.visible = true;
n.inputs.forEach(e => {
e.visible = e.visible || view.state.selection.isSelected(e.source);
});
n.outputs.forEach(e => {
e.visible = e.visible || view.state.selection.isSelected(e.target);
this.visibleEdges = this.graphElement.append("g");
this.visibleNodes = this.graphElement.append("g");
this.drag = d3.drag<any, GraphNode, GraphNode>()
.on("drag", (node: GraphNode) => {
node.x += d3.event.dx;
node.y += d3.event.dy;
this.updateGraphVisibility();
});
}
// TODO (danylo boiko) Extend selection type (add num, string?)
public initializeContent(data: GraphPhase, rememberedSelection: Map<string, GraphNode>): void {
this.show();
this.addImgInput("layout", "layout graph",
partial(this.layoutAction, this));
this.addImgInput("show-all", "show all nodes",
partial(this.showAllAction, this));
this.addImgInput("show-control", "show only control nodes",
partial(this.showControlAction, this));
this.addImgInput("hide-unselected", "hide unselected",
partial(this.hideUnselectedAction, this));
this.addImgInput("hide-selected", "hide selected",
partial(this.hideSelectedAction, this));
this.addImgInput("zoom-selection", "zoom selection",
partial(this.zoomSelectionAction, this));
this.addToggleImgInput("toggle-hide-dead", "toggle hide dead nodes",
this.state.hideDead, partial(this.toggleHideDeadAction, this));
this.addToggleImgInput("toggle-types", "toggle types",
this.state.showTypes, partial(this.toggleTypesAction, this));
this.addToggleImgInput("toggle-cache-layout", "toggle saving graph layout",
this.state.cacheLayout, partial(this.toggleLayoutCachingAction, this));
const adaptedSelection = this.adaptSelectionToCurrentPhase(data.data, rememberedSelection);
this.phaseName = data.name;
this.createGraph(data, adaptedSelection);
this.broker.addNodeHandler(this.nodesSelectionHandler);
const selectedNodes = adaptedSelection?.size > 0
? this.attachSelection(adaptedSelection)
: null;
if (selectedNodes?.length > 0) {
this.connectVisibleSelectedElements();
this.viewSelection();
} else {
this.viewWholeGraph();
if (this.state.cacheLayout && data.transform) {
this.svg.call(this.panZoom.transform, d3.zoomIdentity
.translate(data.transform.x, data.transform.y)
.scale(data.transform.scale));
}
}
view.updateGraphVisibility();
},
brokeredClear: function () {
view.state.selection.clear();
view.updateGraphVisibility();
}
};
view.state.selection = new SelectionMap(n => n.identifier(),
n => n.nodeLabel?.origin?.identifier());
public updateGraphVisibility(): void {
const view = this;
const graph = this.graph;
const state = this.state;
if (!graph) return;
const filteredEdges = [
...graph.filteredEdges(edge => graph.isRendered()
&& edge.source.visible && edge.target.visible)
];
const defs = svg.append('svg:defs');
defs.append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -4 8 8')
.attr('refX', 2)
.attr('markerWidth', 2.5)
.attr('markerHeight', 2.5)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-4L8,0L0,4');
const selEdges = view.visibleEdges
.selectAll<SVGPathElement, GraphEdge>("path")
.data(filteredEdges, edge => edge.toString());
this.graphElement = svg.append("g");
view.visibleEdges = this.graphElement.append("g");
view.visibleNodes = this.graphElement.append("g");
// remove old links
selEdges.exit().remove();
view.drag = d3.drag<any, GraphNode, GraphNode>()
.on("drag", function (d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
view.updateGraphVisibility();
// add new paths
const newEdges = selEdges
.enter()
.append("path")
.style("marker-end", "url(#end-arrow)")
.attr("id", edge => `e,${edge.toString()}`)
.on("click", edge => {
d3.event.stopPropagation();
if (!d3.event.shiftKey) {
view.nodesSelectionHandler.clear();
}
view.nodesSelectionHandler.select([edge.source, edge.target], true);
})
.attr("adjacentToHover", "false")
.classed("value", edge => edge.type === "value" || edge.type === "context")
.classed("control", edge => edge.type === "control")
.classed("effect", edge => edge.type === "effect")
.classed("frame-state", edge => edge.type === "frame-state")
.attr("stroke-dasharray", edge => {
if (edge.type === "frame-state") return "10,10";
return edge.type === "effect" ? "5,5" : "";
});
function zoomed() {
if (d3.event.shiftKey) return false;
view.graphElement.attr("transform", d3.event.transform);
return true;
}
const newAndOldEdges = newEdges.merge(selEdges);
const zoomSvg = d3.zoom<SVGElement, any>()
.scaleExtent([0.2, 40])
.on("zoom", zoomed)
.on("start", function () {
if (d3.event.shiftKey) return;
d3.select('body').style("cursor", "move");
newAndOldEdges.classed("hidden", edge => !edge.isVisible());
// select existing nodes
const filteredNodes = [...graph.nodes(node => graph.isRendered() && node.visible)];
const allNodes = view.visibleNodes.selectAll<SVGGElement, GraphNode>("g");
const selNodes = allNodes.data(filteredNodes, node => node.toString());
// remove old nodes
selNodes.exit().remove();
// add new nodes
const newGs = selNodes.enter()
.append("g")
.classed("turbonode", true)
.classed("control", node => node.isControl())
.classed("live", node => node.isLive())
.classed("dead", node => !node.isLive())
.classed("javascript", node => node.isJavaScript())
.classed("input", node => node.isInput())
.classed("simplified", node => node.isSimplified())
.classed("machine", node => node.isMachine())
.on("mouseenter", node => {
const visibleEdges = view.visibleEdges.selectAll<SVGPathElement, GraphEdge>("path");
const adjInputEdges = visibleEdges.filter(edge => edge.target === node);
const adjOutputEdges = visibleEdges.filter(edge => edge.source === node);
adjInputEdges.attr("relToHover", "input");
adjOutputEdges.attr("relToHover", "output");
const adjInputNodes = adjInputEdges.data().map(edge => edge.source);
const visibleNodes = view.visibleNodes.selectAll<SVGGElement, GraphNode>("g");
visibleNodes.data<GraphNode>(adjInputNodes, node => node.toString())
.attr("relToHover", "input");
const adjOutputNodes = adjOutputEdges.data().map(edge => edge.target);
visibleNodes.data<GraphNode>(adjOutputNodes, node => node.toString())
.attr("relToHover", "output");
view.updateGraphVisibility();
})
.on("end", function () {
d3.select('body').style("cursor", "auto");
});
.on("mouseleave", node => {
const visibleEdges = view.visibleEdges.selectAll<SVGPathElement, GraphEdge>("path");
const adjEdges = visibleEdges.filter(edge => edge.target === node || edge.source === node);
adjEdges.attr("relToHover", "none");
const adjNodes = adjEdges.data().map(edge => edge.target)
.concat(adjEdges.data().map(edge => edge.source));
const visibleNodes = view.visibleNodes.selectAll<SVGPathElement, GraphNode>("g");
visibleNodes.data(adjNodes, node => node.toString()).attr("relToHover", "none");
view.updateGraphVisibility();
})
.on("click", node => {
if (!d3.event.shiftKey) view.nodesSelectionHandler.clear();
view.nodesSelectionHandler.select([node], undefined);
d3.event.stopPropagation();
})
.call(view.drag);
newGs.each(function (node: GraphNode) {
const svg = d3.select<SVGGElement, GraphNode>(this);
svg.append("rect")
.attr("rx", 10)
.attr("ry", 10)
.attr("width", node => node.getWidth())
.attr("height", node => node.getHeight(view.state.showTypes));
svg.call(zoomSvg).on("dblclick.zoom", null);
svg.append("text")
.classed("label", true)
.attr("text-anchor", "right")
.attr("dx", 5)
.attr("dy", 5)
.append("tspan")
.text(node.getDisplayLabel())
.append("title")
.text(node.getTitle());
view.panZoom = zoomSvg;
if (node.nodeLabel.type) {
svg.append("text")
.classed("label", true)
.classed("type", true)
.attr("text-anchor", "right")
.attr("dx", 5)
.attr("dy", node.labelBox.height + 5)
.append("tspan")
.text(node.getDisplayType())
.append("title")
.text(node.getType());
}
view.appendInputAndOutputBubbles(svg, node);
});
getEdgeFrontier(nodes: Iterable<GraphNode>, inEdges: boolean,
edgeFilter: (e: GraphEdge, i: number) => boolean) {
const frontier: Set<GraphEdge> = new Set();
for (const n of nodes) {
const edges = inEdges ? n.inputs : n.outputs;
let edgeNumber = 0;
edges.forEach((edge: GraphEdge) => {
if (edgeFilter == undefined || edgeFilter(edge, edgeNumber)) {
frontier.add(edge);
}
++edgeNumber;
const newAndOldNodes = newGs.merge(selNodes);
newAndOldNodes.select<SVGTextElement>(".type").each(function () {
this.setAttribute("visibility", view.state.showTypes ? "visible" : "hidden");
});
}
return frontier;
newAndOldNodes
.classed("selected", node => state.selection.isSelected(node))
.attr("transform", node => `translate(${node.x},${node.y})`)
.select("rect")
.attr("height", node => node.getHeight(view.state.showTypes));
view.visibleBubbles = d3.selectAll("circle");
view.updateInputAndOutputBubbles();
graph.maxGraphX = graph.maxGraphNodeX;
newAndOldEdges.attr("d", node => node.generatePath(graph, view.state.showTypes));
}
getNodeFrontier(nodes: Iterable<GraphNode>, inEdges: boolean,
edgeFilter: (e: GraphEdge, i: number) => boolean) {
const view = this;
const frontier: Set<GraphNode> = new Set();
let newState = true;
const edgeFrontier = view.getEdgeFrontier(nodes, inEdges, edgeFilter);
// Control key toggles edges rather than just turning them on
if (d3.event.ctrlKey) {
edgeFrontier.forEach(function (edge: GraphEdge) {
if (edge.visible) {
newState = false;
public svgKeyDown(): void {
let eventHandled = true; // unless the below switch defaults
switch (d3.event.keyCode) {
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57: // '1'-'9'
this.showSelectionFrontierNodes(true,
(edge: GraphEdge, index: number) => index == (d3.event.keyCode - 49),
!d3.event.ctrlKey);
break;
case 97:
case 98:
case 99:
case 100:
case 101:
case 102:
case 103:
case 104:
case 105: // 'numpad 1'-'numpad 9'
this.showSelectionFrontierNodes(true,
(edge: GraphEdge, index) => index == (d3.event.keyCode - 97),
!d3.event.ctrlKey);
break;
case 67: // 'c'
this.showSelectionFrontierNodes(d3.event.altKey,
(edge: GraphEdge) => edge.type === "control",
true);
break;
case 69: // 'e'
this.showSelectionFrontierNodes(d3.event.altKey,
(edge: GraphEdge) => edge.type === "effect",
true);
break;
case 79: // 'o'
this.showSelectionFrontierNodes(false, undefined, false);
break;
case 73: // 'i'
if (!d3.event.ctrlKey && !d3.event.shiftKey) {
this.showSelectionFrontierNodes(true, undefined, false);
} else {
eventHandled = false;
}
});
break;
case 65: // 'a'
this.selectAllNodes();
break;
case 38: // UP
case 40: // DOWN
this.showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true);
break;
case 82: // 'r'
if (!d3.event.ctrlKey && !d3.event.shiftKey) {
this.layoutAction(this);
} else {
eventHandled = false;
}
edgeFrontier.forEach(function (edge: GraphEdge) {
edge.visible = newState;
if (newState) {
const node = inEdges ? edge.source : edge.target;
node.visible = true;
frontier.add(node);
break;
case 80: // 'p'
this.selectOrigins();
break;
default:
eventHandled = false;
break;
case 83: // 's'
if (!d3.event.ctrlKey && !d3.event.shiftKey) {
this.hideSelectedAction(this);
} else {
eventHandled = false;
}
});
view.updateGraphVisibility();
if (newState) {
return frontier;
break;
case 85: // 'u'
if (!d3.event.ctrlKey && !d3.event.shiftKey) {
this.hideUnselectedAction(this);
} else {
return undefined;
eventHandled = false;
}
break;
}
if (eventHandled) d3.event.preventDefault();
}
initializeContent(data: GraphPhase, rememberedSelection) {
this.show();
function createImgInput(id: string, title: string, onClick): HTMLElement {
const input = document.createElement("input");
input.setAttribute("id", id);
input.setAttribute("type", "image");
input.setAttribute("title", title);
input.setAttribute("src", `img/toolbox/${id}-icon.png`);
input.className = "button-input graph-toolbox-item";
input.addEventListener("click", onClick);
return input;
}
function createImgToggleInput(id: string, title: string, onClick): HTMLElement {
const input = createImgInput(id, title, onClick);
const toggled = storageGetItem(id, true);
input.classList.toggle("button-input-toggled", toggled);
return input;
}
this.toolbox.appendChild(createImgInput("layout", "layout graph",
partial(this.layoutAction, this)));
this.toolbox.appendChild(createImgInput("show-all", "show all nodes",
partial(this.showAllAction, this)));
this.toolbox.appendChild(createImgInput("show-control", "show only control nodes",
partial(this.showControlAction, this)));
this.toolbox.appendChild(createImgInput("toggle-hide-dead", "toggle hide dead nodes",
partial(this.toggleHideDead, this)));
this.toolbox.appendChild(createImgInput("hide-unselected", "hide unselected",
partial(this.hideUnselectedAction, this)));
this.toolbox.appendChild(createImgInput("hide-selected", "hide selected",
partial(this.hideSelectedAction, this)));
this.toolbox.appendChild(createImgInput("zoom-selection", "zoom selection",
partial(this.zoomSelectionAction, this)));
this.toolbox.appendChild(createImgInput("toggle-types", "toggle types",
partial(this.toggleTypesAction, this)));
this.toolbox.appendChild(createImgToggleInput("cache-graphs", "remember graph layout",
partial(this.toggleGraphCachingAction)));
const adaptedSelection = this.adaptSelectionToCurrentPhase(data.data, rememberedSelection);
public searchInputAction(searchBar: HTMLInputElement, e: KeyboardEvent, onlyVisible: boolean):
void {
if (e.keyCode == 13) {
this.nodesSelectionHandler.clear();
const query = searchBar.value;
storageSetItem("lastSearch", query);
if (query.length == 0) return;
this.phaseName = data.name;
this.createGraph(data, adaptedSelection);
this.broker.addNodeHandler(this.selectionHandler);
const reg = new RegExp(query);
const filterFunction = (node: GraphNode) => {
return (reg.exec(node.getDisplayLabel()) !== null ||
(this.state.showTypes && reg.exec(node.getDisplayType())) ||
(reg.exec(node.getTitle())) ||
reg.exec(node.nodeLabel.opcode) !== null);
};
const selectedNodes = adaptedSelection?.size > 0
? this.attachSelection(adaptedSelection)
: null;
const selection = [...this.graph.nodes(node => {
if ((e.ctrlKey || node.visible || !onlyVisible) && filterFunction(node)) {
if (e.ctrlKey || !onlyVisible) node.visible = true;
return true;
}
return false;
})];
if (selectedNodes?.length > 0) {
this.connectVisibleSelectedNodes();
this.nodesSelectionHandler.select(selection, true);
this.connectVisibleSelectedElements();
this.updateGraphVisibility();
searchBar.blur();
this.viewSelection();
} else {
this.viewWholeGraph();
if (this.isCachingEnabled() && data.transform) {
this.svg.call(this.panZoom.transform, d3.zoomIdentity
.translate(data.transform.x, data.transform.y)
.scale(data.transform.scale));
}
this.focusOnSvg();
}
e.stopPropagation();
}
deleteContent() {
for (const item of this.toolbox.querySelectorAll(".graph-toolbox-item")) {
item.parentElement.removeChild(item);
private initializeNodesSelectionHandler(): NodeSelectionHandler & ClearableHandler {
const view = this;
return {
clear: function () {
view.state.selection.clear();
view.broker.broadcastClear(this);
view.updateGraphVisibility();
},
select: function (selectedNodes: Array<GraphNode>, selected: boolean) {
const locations = new Array<GenericPosition>();
for (const node of selectedNodes) {
if (node.nodeLabel.sourcePosition) {
locations.push(node.nodeLabel.sourcePosition);
}
if (!this.isCachingEnabled()) {
this.updateGraphStateType(GraphStateType.NeedToFullRebuild);
if (node.nodeLabel.origin && node.nodeLabel.origin instanceof BytecodeOrigin) {
locations.push(new BytecodePosition(node.nodeLabel.origin.bytecodePosition));
}
this.graph.graphPhase.rendered = false;
this.updateGraphVisibility();
}
public hide(): void {
if (this.isCachingEnabled()) {
const matrix = this.graphElement.node().transform.baseVal.consolidate().matrix;
this.graph.graphPhase.transform = { scale: matrix.a, x: matrix.e, y: matrix.f };
} else {
this.graph.graphPhase.transform = null;
view.state.selection.select(selectedNodes, selected);
view.broker.broadcastSourcePositionSelect(this, locations, selected);
view.updateGraphVisibility();
},
brokeredNodeSelect: function (locations, selected: boolean) {
if (!view.graph) return;
const selection = view.graph.nodes(node =>
locations.has(node.identifier()) && (!view.state.hideDead || node.isLive()));
view.state.selection.select(selection, selected);
// Update edge visibility based on selection.
for (const node of view.graph.nodes()) {
if (view.state.selection.isSelected(node)) {
node.visible = true;
node.inputs.forEach(edge => {
edge.visible = edge.visible || view.state.selection.isSelected(edge.source);
});
node.outputs.forEach(edge => {
edge.visible = edge.visible || view.state.selection.isSelected(edge.target);
});
}
super.hide();
this.deleteContent();
}
view.updateGraphVisibility();
},
brokeredClear: function () {
view.state.selection.clear();
view.updateGraphVisibility();
}
};
}
createGraph(data, selection) {
private createGraph(data: GraphPhase, selection): void {
this.graph = new Graph(data);
this.graphLayout = new GraphLayout(this.graph);
if (!this.isCachingEnabled() ||
if (!this.state.cacheLayout ||
this.graph.graphPhase.stateType == GraphStateType.NeedToFullRebuild) {
this.updateGraphStateType(GraphStateType.NeedToFullRebuild);
this.showControlAction(this);
......@@ -321,8 +423,8 @@ export class GraphView extends PhaseView {
}
if (selection !== undefined) {
for (const node of this.graph.nodes()) {
node.visible = node.visible || selection.has(node.identifier());
for (const item of selection) {
if (this.graph.nodeMap[item]) this.graph.nodeMap[item].visible = true;
}
}
......@@ -332,69 +434,103 @@ export class GraphView extends PhaseView {
this.updateGraphVisibility();
}
public showVisible() {
this.updateGraphVisibility();
this.viewWholeGraph();
this.focusOnSvg();
private layoutGraph(): void {
const layoutMessage = this.graph.graphPhase.stateType == GraphStateType.Cached
? "Layout graph from cache"
: "Layout graph";
console.time(layoutMessage);
this.graphLayout.rebuild(this.state.showTypes);
const extent = this.graph.redetermineGraphBoundingBox(this.state.showTypes);
this.panZoom.translateExtent(extent);
this.minScale(this.graph.width, this.graph.height);
console.timeEnd(layoutMessage);
}
connectVisibleSelectedNodes() {
private appendInputAndOutputBubbles(svg: d3.Selection<SVGGElement, GraphNode, null, GraphNode>,
node: GraphNode): void {
const view = this;
for (const n of view.state.selection) {
n.inputs.forEach(function (edge: GraphEdge) {
if (edge.source.visible && edge.target.visible) {
edge.visible = true;
}
});
n.outputs.forEach(function (edge: GraphEdge) {
if (edge.source.visible && edge.target.visible) {
edge.visible = true;
}
for (let i = 0; i < node.inputs.length; ++i) {
const x = node.getInputX(i);
const y = -C.DEFAULT_NODE_BUBBLE_RADIUS;
svg.append("circle")
.classed("filledBubbleStyle", node.inputs[i].isVisible())
.classed("bubbleStyle", !node.inputs[i].isVisible())
.attr("id", `ib,${node.inputs[i]}`)
.attr("r", C.DEFAULT_NODE_BUBBLE_RADIUS)
.attr("transform", `translate(${x},${y})`)
.on("click", function (this: SVGCircleElement) {
const components = this.id.split(",");
const node = view.graph.nodeMap[components[3]];
const edge = node.inputs[components[2]];
const visible = !edge.isVisible();
node.setInputVisibility(components[2], visible);
d3.event.stopPropagation();
view.updateGraphVisibility();
});
}
if (node.outputs.length > 0) {
const x = node.getOutputX();
const y = node.getHeight(view.state.showTypes) + C.DEFAULT_NODE_BUBBLE_RADIUS;
svg.append("circle")
.classed("filledBubbleStyle", node.areAnyOutputsVisible()
== OutputVisibilityType.AllNodesVisible)
.classed("halFilledBubbleStyle", node.areAnyOutputsVisible()
== OutputVisibilityType.SomeNodesVisible)
.classed("bubbleStyle", node.areAnyOutputsVisible()
== OutputVisibilityType.NoVisibleNodes)
.attr("id", `ob,${node.id}`)
.attr("r", C.DEFAULT_NODE_BUBBLE_RADIUS)
.attr("transform", `translate(${x},${y})`)
.on("click", node => {
node.setOutputVisibility(node.areAnyOutputsVisible()
== OutputVisibilityType.NoVisibleNodes);
d3.event.stopPropagation();
view.updateGraphVisibility();
});
}
}
updateInputAndOutputBubbles() {
private updateInputAndOutputBubbles(): void {
const view = this;
const g = this.graph;
const s = this.visibleBubbles;
s.classed("filledBubbleStyle", function (c) {
const components = this.id.split(',');
if (components[0] == "ib") {
const edge = g.nodeMap[components[3]].inputs[components[2]];
const graph = this.graph;
const bubbles = this.visibleBubbles;
bubbles.classed("filledBubbleStyle", function () {
const components = this.id.split(",");
if (components[0] === "ib") {
const edge = graph.nodeMap[components[3]].inputs[components[2]];
return edge.isVisible();
} else {
return g.nodeMap[components[1]].areAnyOutputsVisible() == 2;
}
}).classed("halfFilledBubbleStyle", function (c) {
const components = this.id.split(',');
if (components[0] == "ib") {
return false;
} else {
return g.nodeMap[components[1]].areAnyOutputsVisible() == 1;
}
}).classed("bubbleStyle", function (c) {
const components = this.id.split(',');
if (components[0] == "ib") {
const edge = g.nodeMap[components[3]].inputs[components[2]];
return graph.nodeMap[components[1]].areAnyOutputsVisible()
== OutputVisibilityType.AllNodesVisible;
}).classed("halfFilledBubbleStyle", function () {
const components = this.id.split(",");
if (components[0] === "ib") return false;
return graph.nodeMap[components[1]].areAnyOutputsVisible()
== OutputVisibilityType.SomeNodesVisible;
}).classed("bubbleStyle", function () {
const components = this.id.split(",");
if (components[0] === "ib") {
const edge = graph.nodeMap[components[3]].inputs[components[2]];
return !edge.isVisible();
} else {
return g.nodeMap[components[1]].areAnyOutputsVisible() == 0;
}
return graph.nodeMap[components[1]].areAnyOutputsVisible()
== OutputVisibilityType.NoVisibleNodes;
});
s.each(function (c) {
const components = this.id.split(',');
if (components[0] == "ob") {
const from = g.nodeMap[components[1]];
bubbles.each(function () {
const components = this.id.split(",");
if (components[0] === "ob") {
const from = graph.nodeMap[components[1]];
const x = from.getOutputX();
const y = from.getNodeHeight(view.state.showTypes) + C.DEFAULT_NODE_BUBBLE_RADIUS;
const transform = "translate(" + x + "," + y + ")";
this.setAttribute('transform', transform);
const y = from.getHeight(view.state.showTypes) + C.DEFAULT_NODE_BUBBLE_RADIUS;
this.setAttribute("transform", `translate(${x},${y})`);
}
});
}
adaptSelectionToCurrentPhase(data, selection) {
private adaptSelectionToCurrentPhase(data: GraphData, selection: Map<string, GraphNode>):
Set<string> {
// TODO (danylo boiko) Speed up adapting
const updatedGraphSelection = new Set<string>();
if (!data || !(selection instanceof Map)) return updatedGraphSelection;
// Adding survived nodes (with the same id)
......@@ -421,311 +557,164 @@ export class GraphView extends PhaseView {
return updatedGraphSelection;
}
public attachSelection(selection: Set<string>): Array<GraphNode> {
private attachSelection(selection: Set<string>): Array<GraphNode> {
if (!(selection instanceof Set)) return new Array<GraphNode>();
this.selectionHandler.clear();
this.nodesSelectionHandler.clear();
const selected = [
...this.graph.nodes(node =>
selection.has(this.state.selection.stringKey(node))
&& (!this.state.hideDead || node.isLive()))
];
this.selectionHandler.select(selected, true);
this.nodesSelectionHandler.select(selected, true);
return selected;
}
detachSelection() {
return this.state.selection.detachSelection();
private viewSelection(): void {
let minX;
let maxX;
let minY;
let maxY;
let hasSelection = false;
this.visibleNodes.selectAll<SVGGElement, GraphNode>("g").each((node: GraphNode) => {
if (this.state.selection.isSelected(node)) {
hasSelection = true;
minX = minX ? Math.min(minX, node.x) : node.x;
maxX = maxX ? Math.max(maxX, node.x + node.getWidth()) : node.x + node.getWidth();
minY = minY ? Math.min(minY, node.y) : node.y;
maxY = maxY
? Math.max(maxY, node.y + node.getHeight(this.state.showTypes))
: node.y + node.getHeight(this.state.showTypes);
}
selectAllNodes() {
if (!d3.event.shiftKey) {
this.state.selection.clear();
});
if (hasSelection) {
this.viewGraphRegion(minX - C.NODE_INPUT_WIDTH, minY - 60,
maxX + C.NODE_INPUT_WIDTH, maxY + 60);
}
const allVisibleNodes = [...this.graph.nodes(n => n.visible)];
this.state.selection.select(allVisibleNodes, true);
this.updateGraphVisibility();
}
layoutAction(graph: GraphView) {
graph.updateGraphStateType(GraphStateType.NeedToFullRebuild);
graph.layoutGraph();
graph.updateGraphVisibility();
graph.viewWholeGraph();
graph.focusOnSvg();
// Actions (handlers of toolbox menu and hotkeys events)
private layoutAction(view: GraphView): void {
view.updateGraphStateType(GraphStateType.NeedToFullRebuild);
view.layoutGraph();
view.updateGraphVisibility();
view.viewWholeGraph();
view.focusOnSvg();
}
showAllAction(view: GraphView) {
for (const n of view.graph.nodes()) {
n.visible = !view.state.hideDead || n.isLive();
private showAllAction(view: GraphView): void {
for (const node of view.graph.nodes()) {
node.visible = !view.state.hideDead || node.isLive();
}
view.graph.forEachEdge((e: GraphEdge) => {
e.visible = e.source.visible || e.target.visible;
view.graph.forEachEdge((edge: GraphEdge) => {
edge.visible = edge.source.visible || edge.target.visible;
});
view.updateGraphVisibility();
view.viewWholeGraph();
view.focusOnSvg();
}
showControlAction(view: GraphView) {
for (const n of view.graph.nodes()) {
n.visible = n.cfg && (!view.state.hideDead || n.isLive());
private showControlAction(view: GraphView): void {
for (const node of view.graph.nodes()) {
node.visible = node.cfg && (!view.state.hideDead || node.isLive());
}
view.graph.forEachEdge((e: GraphEdge) => {
e.visible = e.type === "control" && e.source.visible && e.target.visible;
view.graph.forEachEdge((edge: GraphEdge) => {
edge.visible = edge.type === "control" && edge.source.visible && edge.target.visible;
});
view.showVisible();
}
toggleHideDead(view: GraphView) {
view.state.hideDead = !view.state.hideDead;
if (view.state.hideDead) {
view.hideDead();
} else {
view.showDead();
}
const element = document.getElementById('toggle-hide-dead');
element.classList.toggle('button-input-toggled', view.state.hideDead);
view.focusOnSvg();
}
hideDead() {
for (const n of this.graph.nodes()) {
if (!n.isLive()) {
n.visible = false;
this.state.selection.select([n], false);
}
}
this.updateGraphVisibility();
}
showDead() {
for (const n of this.graph.nodes()) {
if (!n.isLive()) {
n.visible = true;
}
}
this.updateGraphVisibility();
}
hideUnselectedAction(view: GraphView) {
for (const n of view.graph.nodes()) {
if (!view.state.selection.isSelected(n)) {
n.visible = false;
private hideUnselectedAction(view: GraphView): void {
for (const node of view.graph.nodes()) {
if (!view.state.selection.isSelected(node)) {
node.visible = false;
}
}
view.updateGraphVisibility();
view.focusOnSvg();
}
hideSelectedAction(view: GraphView) {
for (const n of view.graph.nodes()) {
if (view.state.selection.isSelected(n)) {
n.visible = false;
private hideSelectedAction(view: GraphView): void {
for (const node of view.graph.nodes()) {
if (view.state.selection.isSelected(node)) {
node.visible = false;
}
}
view.selectionHandler.clear();
view.nodesSelectionHandler.clear();
view.focusOnSvg();
}
zoomSelectionAction(view: GraphView) {
private zoomSelectionAction(view: GraphView): void {
view.viewSelection();
view.focusOnSvg();
}
toggleTypesAction(view: GraphView) {
view.toggleTypes();
private toggleHideDeadAction(view: GraphView): void {
view.state.hideDead = !view.state.hideDead;
if (view.state.hideDead) {
view.hideDead();
} else {
view.showDead();
}
const element = document.getElementById("toggle-hide-dead");
element.classList.toggle("button-input-toggled", view.state.hideDead);
view.focusOnSvg();
}
private toggleGraphCachingAction(): void {
const key = "cache-graphs";
const toggled = storageGetItem(key, true);
storageSetItem(key, !toggled);
const element = document.getElementById(key);
element.classList.toggle("button-input-toggled", !toggled);
private toggleTypesAction(view: GraphView): void {
view.state.showTypes = !view.state.showTypes;
const element = document.getElementById("toggle-types");
element.classList.toggle("button-input-toggled", view.state.showTypes);
view.updateGraphVisibility();
view.focusOnSvg();
}
searchInputAction(searchBar: HTMLInputElement, e: KeyboardEvent, onlyVisible: boolean) {
if (e.keyCode == 13) {
this.selectionHandler.clear();
const query = searchBar.value;
window.sessionStorage.setItem("lastSearch", query);
if (query.length == 0) return;
const reg = new RegExp(query);
const filterFunction = (n: GraphNode) => {
return (reg.exec(n.getDisplayLabel()) != null ||
(this.state.showTypes && reg.exec(n.getDisplayType())) ||
(reg.exec(n.getTitle())) ||
reg.exec(n.nodeLabel.opcode) != null);
};
const selection = [...this.graph.nodes(n => {
if ((e.ctrlKey || n.visible || !onlyVisible) && filterFunction(n)) {
if (e.ctrlKey || !onlyVisible) n.visible = true;
return true;
private toggleLayoutCachingAction(view: GraphView): void {
view.state.cacheLayout = !view.state.cacheLayout;
const element = document.getElementById("toggle-cache-layout");
element.classList.toggle("button-input-toggled", view.state.cacheLayout);
}
return false;
})];
this.selectionHandler.select(selection, true);
this.connectVisibleSelectedNodes();
this.updateGraphVisibility();
searchBar.blur();
this.viewSelection();
this.focusOnSvg();
private hideDead(): void {
for (const node of this.graph.nodes()) {
if (!node.isLive()) {
node.visible = false;
this.state.selection.select([node], false);
}
e.stopPropagation();
}
focusOnSvg() {
(document.getElementById("graph").childNodes[0] as HTMLElement).focus();
this.updateGraphVisibility();
}
svgKeyDown() {
const view = this;
const state = this.state;
const showSelectionFrontierNodes = (inEdges: boolean, filter: (e: GraphEdge, i: number) =>
boolean, doSelect: boolean) => {
const frontier = view.getNodeFrontier(state.selection, inEdges, filter);
if (frontier != undefined && frontier.size) {
if (doSelect) {
if (!d3.event.shiftKey) {
state.selection.clear();
private showDead(): void {
for (const node of this.graph.nodes()) {
if (!node.isLive()) {
node.visible = true;
}
state.selection.select([...frontier], true);
}
view.updateGraphVisibility();
this.updateGraphVisibility();
}
};
let eventHandled = true; // unless the below switch defaults
switch (d3.event.keyCode) {
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57:
// '1'-'9'
showSelectionFrontierNodes(true,
(edge: GraphEdge, index: number) => index == (d3.event.keyCode - 49),
!d3.event.ctrlKey);
break;
case 97:
case 98:
case 99:
case 100:
case 101:
case 102:
case 103:
case 104:
case 105:
// 'numpad 1'-'numpad 9'
showSelectionFrontierNodes(true,
(edge, index) => index == (d3.event.keyCode - 97),
!d3.event.ctrlKey);
break;
case 67:
// 'c'
showSelectionFrontierNodes(d3.event.altKey,
(edge, index) => edge.type == 'control',
true);
break;
case 69:
// 'e'
showSelectionFrontierNodes(d3.event.altKey,
(edge, index) => edge.type == 'effect',
true);
break;
case 79:
// 'o'
showSelectionFrontierNodes(false, undefined, false);
break;
case 73:
// 'i'
if (!d3.event.ctrlKey && !d3.event.shiftKey) {
showSelectionFrontierNodes(true, undefined, false);
} else {
eventHandled = false;
}
break;
case 65:
// 'a'
view.selectAllNodes();
break;
case 38:
// UP
case 40: {
// DOWN
showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true);
break;
}
case 82:
// 'r'
if (!d3.event.ctrlKey && !d3.event.shiftKey) {
this.layoutAction(this);
} else {
eventHandled = false;
}
break;
case 80:
// 'p'
view.selectOrigins();
break;
default:
eventHandled = false;
break;
case 83:
// 's'
if (!d3.event.ctrlKey && !d3.event.shiftKey) {
this.hideSelectedAction(this);
} else {
eventHandled = false;
}
break;
case 85:
// 'u'
if (!d3.event.ctrlKey && !d3.event.shiftKey) {
this.hideUnselectedAction(this);
} else {
eventHandled = false;
}
break;
}
if (eventHandled) {
d3.event.preventDefault();
}
// Hotkeys handlers
private selectAllNodes(): void {
if (!d3.event.shiftKey) {
this.state.selection.clear();
}
layoutGraph() {
const layoutMessage = this.graph.graphPhase.stateType == GraphStateType.Cached
? "Layout graph from cache"
: "Layout graph";
console.time(layoutMessage);
this.graphLayout.rebuild(this.state.showTypes);
const extent = this.graph.redetermineGraphBoundingBox(this.state.showTypes);
this.panZoom.translateExtent(extent);
this.minScale();
console.timeEnd(layoutMessage);
const allVisibleNodes = [...this.graph.nodes(node => node.visible)];
this.state.selection.select(allVisibleNodes, true);
this.updateGraphVisibility();
}
selectOrigins() {
private selectOrigins() {
const state = this.state;
// TODO (danylo boiko) Add array type
const origins = [];
let phase = this.phaseName;
const selection = new Set<any>();
for (const n of state.selection) {
const origin = n.nodeLabel.origin;
for (const node of state.selection) {
const origin = node.nodeLabel.origin;
if (origin) {
phase = origin.phase;
const node = this.graph.nodeMap[origin.nodeId];
if (phase === this.phaseName && node) {
if (node && phase === this.phaseName) {
origins.push(node);
} else {
selection.add(`${origin.nodeId}`);
......@@ -737,312 +726,8 @@ export class GraphView extends PhaseView {
if (selection.size > 0 && phase !== this.phaseName) {
this.showPhaseByName(phase, selection);
} else if (origins.length > 0) {
this.selectionHandler.clear();
this.selectionHandler.select(origins, true);
}
}
// call to propagate changes to graph
updateGraphVisibility() {
const view = this;
const graph = this.graph;
const state = this.state;
if (!graph) return;
const filteredEdges = [
...graph.filteredEdges(edge => this.graph.isRendered()
&& edge.source.visible && edge.target.visible)
];
const selEdges = view.visibleEdges.selectAll<SVGPathElement, GraphEdge>("path")
.data(filteredEdges, e => e.toString());
// remove old links
selEdges.exit().remove();
// add new paths
const newEdges = selEdges.enter()
.append('path');
newEdges.style('marker-end', 'url(#end-arrow)')
.attr("id", function (edge) { return "e," + edge.toString(); })
.on("click", function (edge) {
d3.event.stopPropagation();
if (!d3.event.shiftKey) {
view.selectionHandler.clear();
}
view.selectionHandler.select([edge.source, edge.target], true);
})
.attr("adjacentToHover", "false")
.classed('value', function (e) {
return e.type == 'value' || e.type == 'context';
}).classed('control', function (e) {
return e.type == 'control';
}).classed('effect', function (e) {
return e.type == 'effect';
}).classed('frame-state', function (e) {
return e.type == 'frame-state';
}).attr('stroke-dasharray', function (e) {
if (e.type == 'frame-state') return "10,10";
return (e.type == 'effect') ? "5,5" : "";
});
const newAndOldEdges = newEdges.merge(selEdges);
newAndOldEdges.classed('hidden', e => !e.isVisible());
// select existing nodes
const filteredNodes = [...graph.nodes(n => this.graph.isRendered() && n.visible)];
const allNodes = view.visibleNodes.selectAll<SVGGElement, GraphNode>("g");
const selNodes = allNodes.data(filteredNodes, n => n.toString());
// remove old nodes
selNodes.exit().remove();
// add new nodes
const newGs = selNodes.enter()
.append("g");
newGs.classed("turbonode", function (n) { return true; })
.classed("control", function (n) { return n.isControl(); })
.classed("live", function (n) { return n.isLive(); })
.classed("dead", function (n) { return !n.isLive(); })
.classed("javascript", function (n) { return n.isJavaScript(); })
.classed("input", function (n) { return n.isInput(); })
.classed("simplified", function (n) { return n.isSimplified(); })
.classed("machine", function (n) { return n.isMachine(); })
.on('mouseenter', function (node) {
const visibleEdges = view.visibleEdges.selectAll<SVGPathElement, GraphEdge>('path');
const adjInputEdges = visibleEdges.filter(e => e.target === node);
const adjOutputEdges = visibleEdges.filter(e => e.source === node);
adjInputEdges.attr('relToHover', "input");
adjOutputEdges.attr('relToHover', "output");
const adjInputNodes = adjInputEdges.data().map(e => e.source);
const visibleNodes = view.visibleNodes.selectAll<SVGGElement, GraphNode>("g");
visibleNodes.data<GraphNode>(adjInputNodes, n => n.toString())
.attr('relToHover', "input");
const adjOutputNodes = adjOutputEdges.data().map(e => e.target);
visibleNodes.data<GraphNode>(adjOutputNodes, n => n.toString())
.attr('relToHover', "output");
view.updateGraphVisibility();
})
.on('mouseleave', function (node) {
const visibleEdges = view.visibleEdges.selectAll<SVGPathElement, GraphEdge>('path');
const adjEdges = visibleEdges.filter(e => e.target === node || e.source === node);
adjEdges.attr('relToHover', "none");
const adjNodes = adjEdges.data().map(e => e.target).concat(adjEdges.data().map(e => e.source));
const visibleNodes = view.visibleNodes.selectAll<SVGPathElement, GraphNode>("g");
visibleNodes.data(adjNodes, n => n.toString()).attr('relToHover', "none");
view.updateGraphVisibility();
})
.on("click", d => {
if (!d3.event.shiftKey) view.selectionHandler.clear();
view.selectionHandler.select([d], undefined);
d3.event.stopPropagation();
})
.call(view.drag);
newGs.append("rect")
.attr("rx", 10)
.attr("ry", 10)
.attr('width', function (d) {
return d.getTotalNodeWidth();
})
.attr('height', function (d) {
return d.getNodeHeight(view.state.showTypes);
});
function appendInputAndOutputBubbles(g, d) {
for (let i = 0; i < d.inputs.length; ++i) {
const x = d.getInputX(i);
const y = -C.DEFAULT_NODE_BUBBLE_RADIUS;
g.append('circle')
.classed("filledBubbleStyle", function (c) {
return d.inputs[i].isVisible();
})
.classed("bubbleStyle", function (c) {
return !d.inputs[i].isVisible();
})
.attr("id", `ib,${d.inputs[i]}`)
.attr("r", C.DEFAULT_NODE_BUBBLE_RADIUS)
.attr("transform", function (d) {
return "translate(" + x + "," + y + ")";
})
.on("click", function (this: SVGCircleElement, d) {
const components = this.id.split(',');
const node = graph.nodeMap[components[3]];
const edge = node.inputs[components[2]];
const visible = !edge.isVisible();
node.setInputVisibility(components[2], visible);
d3.event.stopPropagation();
view.updateGraphVisibility();
});
}
if (d.outputs.length != 0) {
const x = d.getOutputX();
const y = d.getNodeHeight(view.state.showTypes) + C.DEFAULT_NODE_BUBBLE_RADIUS;
g.append('circle')
.classed("filledBubbleStyle", function (c) {
return d.areAnyOutputsVisible() == 2;
})
.classed("halFilledBubbleStyle", function (c) {
return d.areAnyOutputsVisible() == 1;
})
.classed("bubbleStyle", function (c) {
return d.areAnyOutputsVisible() == 0;
})
.attr("id", "ob," + d.id)
.attr("r", C.DEFAULT_NODE_BUBBLE_RADIUS)
.attr("transform", function (d) {
return "translate(" + x + "," + y + ")";
})
.on("click", function (d) {
d.setOutputVisibility(d.areAnyOutputsVisible() == 0);
d3.event.stopPropagation();
view.updateGraphVisibility();
});
}
}
newGs.each(function (d) {
appendInputAndOutputBubbles(d3.select(this), d);
});
newGs.each(function (d) {
d3.select(this).append("text")
.classed("label", true)
.attr("text-anchor", "right")
.attr("dx", 5)
.attr("dy", 5)
.append('tspan')
.text(function (l) {
return d.getDisplayLabel();
})
.append("title")
.text(function (l) {
return d.getTitle();
});
if (d.nodeLabel.type != undefined) {
d3.select(this).append("text")
.classed("label", true)
.classed("type", true)
.attr("text-anchor", "right")
.attr("dx", 5)
.attr("dy", d.labelBox.height + 5)
.append('tspan')
.text(function (l) {
return d.getDisplayType();
})
.append("title")
.text(function (l) {
return d.getType();
});
}
});
const newAndOldNodes = newGs.merge(selNodes);
newAndOldNodes.select<SVGTextElement>('.type').each(function (d) {
this.setAttribute('visibility', view.state.showTypes ? 'visible' : 'hidden');
});
newAndOldNodes
.classed("selected", function (n) {
if (state.selection.isSelected(n)) return true;
return false;
})
.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
.select('rect')
.attr('height', function (d) { return d.getNodeHeight(view.state.showTypes); });
view.visibleBubbles = d3.selectAll('circle');
view.updateInputAndOutputBubbles();
graph.maxGraphX = graph.maxGraphNodeX;
newAndOldEdges.attr("d", function (edge) {
return edge.generatePath(graph, view.state.showTypes);
});
}
private getSvgViewDimensions(): [number, number] {
return [this.container.clientWidth, this.container.clientHeight];
}
getSvgExtent(): [[number, number], [number, number]] {
return [[0, 0], [this.container.clientWidth, this.container.clientHeight]];
}
minScale() {
const [clientWith, clientHeight] = this.getSvgViewDimensions();
const minXScale = clientWith / (2 * this.graph.width);
const minYScale = clientHeight / (2 * this.graph.height);
const minScale = Math.min(minXScale, minYScale);
this.panZoom.scaleExtent([minScale, 40]);
return minScale;
}
onresize() {
const trans = d3.zoomTransform(this.svg.node());
const ctrans = this.panZoom.constrain()(trans, this.getSvgExtent(), this.panZoom.translateExtent());
this.panZoom.transform(this.svg, ctrans);
}
toggleTypes() {
const view = this;
view.state.showTypes = !view.state.showTypes;
const element = document.getElementById('toggle-types');
element.classList.toggle('button-input-toggled', view.state.showTypes);
view.updateGraphVisibility();
}
viewSelection() {
const view = this;
let minX;
let maxX;
let minY;
let maxY;
let hasSelection = false;
view.visibleNodes.selectAll<SVGGElement, GraphNode>("g").each(function (n) {
if (view.state.selection.isSelected(n)) {
hasSelection = true;
minX = minX ? Math.min(minX, n.x) : n.x;
maxX = maxX ? Math.max(maxX, n.x + n.getTotalNodeWidth()) :
n.x + n.getTotalNodeWidth();
minY = minY ? Math.min(minY, n.y) : n.y;
maxY = maxY ? Math.max(maxY, n.y + n.getNodeHeight(view.state.showTypes)) :
n.y + n.getNodeHeight(view.state.showTypes);
this.nodesSelectionHandler.clear();
this.nodesSelectionHandler.select(origins, true);
}
});
if (hasSelection) {
view.viewGraphRegion(minX - C.NODE_INPUT_WIDTH, minY - 60,
maxX + C.NODE_INPUT_WIDTH, maxY + 60);
}
}
viewGraphRegion(minX, minY, maxX, maxY) {
const [width, height] = this.getSvgViewDimensions();
const dx = maxX - minX;
const dy = maxY - minY;
const x = (minX + maxX) / 2;
const y = (minY + maxY) / 2;
const scale = Math.min(width / dx, height / dy) * 0.9;
this.svg
.transition().duration(120).call(this.panZoom.scaleTo, scale)
.transition().duration(120).call(this.panZoom.translateTo, x, y);
}
viewWholeGraph() {
this.panZoom.scaleTo(this.svg, 0);
this.panZoom.translateTo(this.svg,
this.graph.minGraphX + this.graph.width / 2,
this.graph.minGraphY + this.graph.height / 2);
}
private updateGraphStateType(stateType: GraphStateType): void {
this.graph.graphPhase.stateType = stateType;
}
private isCachingEnabled(): boolean {
return storageGetItem("cache-graphs", true);
}
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as C from "../common/constants";
import * as d3 from "d3";
import { storageGetItem, storageSetItem } from "../common/util";
import { PhaseView } from "./view";
import { SelectionBroker } from "../selection/selection-broker";
import { SelectionMap } from "../selection/selection";
import { ClearableHandler, NodeSelectionHandler } from "../selection/selection-handler";
import { GraphStateType } from "../phases/graph-phase/graph-phase";
import { Edge } from "../edge";
import { Node } from "../node";
import { TurboshaftGraph } from "../turboshaft-graph";
import { Graph } from "../graph";
export abstract class MovableView<GraphType extends Graph | TurboshaftGraph> extends PhaseView {
phaseName: string;
graph: GraphType;
showPhaseByName: (name: string, selection: Set<any>) => void;
broker: SelectionBroker;
toolbox: HTMLElement;
state: MovableViewState;
nodesSelectionHandler: NodeSelectionHandler & ClearableHandler;
divElement: d3.Selection<any, any, any, any>;
graphElement: d3.Selection<any, any, any, any>;
svg: d3.Selection<any, any, any, any>;
panZoom: d3.ZoomBehavior<SVGElement, any>;
public abstract updateGraphVisibility(): void;
public abstract svgKeyDown(): void;
constructor(idOrContainer: string | HTMLElement, broker: SelectionBroker,
showPhaseByName: (name: string) => void, toolbox: HTMLElement) {
super(idOrContainer);
this.broker = broker;
this.showPhaseByName = showPhaseByName;
this.toolbox = toolbox;
this.state = new MovableViewState();
this.divElement = d3.select(this.divNode);
// Listen for key events. Note that the focus handler seems
// to be important even if it does nothing.
this.svg = this.divElement.append("svg")
.attr("version", "2.0")
.attr("width", "100%")
.attr("height", "100%")
.on("focus", () => { })
.on("keydown", () => this.svgKeyDown());
this.svg.append("svg:defs")
.append("svg:marker")
.attr("id", "end-arrow")
.attr("viewBox", "0 -4 8 8")
.attr("refX", 2)
.attr("markerWidth", 2.5)
.attr("markerHeight", 2.5)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-4L8,0L0,4");
this.graphElement = this.svg.append("g");
this.panZoom = d3.zoom<SVGElement, any>()
.scaleExtent([0.2, 40])
.on("zoom", () => {
if (d3.event.shiftKey) return false;
this.graphElement.attr("transform", d3.event.transform);
return true;
})
.on("start", () => {
if (d3.event.shiftKey) return;
d3.select("body").style("cursor", "move");
})
.on("end", () => d3.select("body").style("cursor", "auto"));
this.svg.call(this.panZoom).on("dblclick.zoom", null);
}
public createViewElement(): HTMLDivElement {
const pane = document.createElement("div");
pane.setAttribute("id", C.GRAPH_PANE_ID);
return pane;
}
public detachSelection(): Map<string, any> {
return this.state.selection.detachSelection();
}
public onresize() {
const trans = d3.zoomTransform(this.svg.node());
const ctrans = this.panZoom.constrain()(trans, this.getSvgExtent(),
this.panZoom.translateExtent());
this.panZoom.transform(this.svg, ctrans);
}
public hide(): void {
if (this.state.cacheLayout) {
const matrix = this.graphElement.node().transform.baseVal.consolidate().matrix;
this.graph.graphPhase.transform = { scale: matrix.a, x: matrix.e, y: matrix.f };
} else {
this.graph.graphPhase.transform = null;
}
super.hide();
this.deleteContent();
}
protected focusOnSvg(): void {
const svg = document.getElementById(C.GRAPH_PANE_ID).childNodes[0] as HTMLElement;
svg.focus();
}
protected updateGraphStateType(stateType: GraphStateType): void {
this.graph.graphPhase.stateType = stateType;
}
protected viewGraphRegion(minX: number, minY: number,
maxX: number, maxY: number): void {
const [width, height] = this.getSvgViewDimensions();
const dx = maxX - minX;
const dy = maxY - minY;
const x = (minX + maxX) / 2;
const y = (minY + maxY) / 2;
const scale = Math.min(width / dx, height / dy) * 0.9;
this.svg
.transition().duration(120).call(this.panZoom.scaleTo, scale)
.transition().duration(120).call(this.panZoom.translateTo, x, y);
}
protected addImgInput(id: string, title: string, onClick): void {
const input = this.createImgInput(id, title, onClick);
this.toolbox.appendChild(input);
}
protected addToggleImgInput(id: string, title: string, initState: boolean, onClick): void {
const input = this.createImgToggleInput(id, title, initState, onClick);
this.toolbox.appendChild(input);
}
protected minScale(graphWidth: number, graphHeight: number): number {
const [clientWith, clientHeight] = this.getSvgViewDimensions();
const minXScale = clientWith / (2 * graphWidth);
const minYScale = clientHeight / (2 * graphHeight);
const minScale = Math.min(minXScale, minYScale);
this.panZoom.scaleExtent([minScale, 40]);
return minScale;
}
protected getNodeFrontier<NodeType extends Node<any>, EdgeType extends Edge<any>>(
nodes: Iterable<NodeType>, inEdges: boolean,
edgeFilter: (edge: EdgeType, idx: number) => boolean): Set<NodeType> {
const frontier = new Set<NodeType>();
let newState = true;
const edgeFrontier = this.getEdgeFrontier<EdgeType>(nodes, inEdges, edgeFilter);
// Control key toggles edges rather than just turning them on
if (d3.event.ctrlKey) {
for (const edge of edgeFrontier) {
if (edge.visible) newState = false;
}
}
for (const edge of edgeFrontier) {
edge.visible = newState;
if (newState) {
const node = inEdges ? edge.source : edge.target;
node.visible = true;
frontier.add(node);
}
}
this.updateGraphVisibility();
return newState ? frontier : undefined;
}
protected showSelectionFrontierNodes<EdgeType extends Edge<any>>(
inEdges: boolean, filter: (edge: EdgeType, idx: number) => boolean,
select: boolean): void {
const frontier = this.getNodeFrontier(this.state.selection, inEdges, filter);
if (frontier !== undefined && frontier.size) {
if (select) {
if (!d3.event.shiftKey) this.state.selection.clear();
this.state.selection.select([...frontier], true);
}
this.updateGraphVisibility();
}
}
protected getEdgeFrontier<EdgeType extends Edge<any>> (nodes: Iterable<Node<any>>,
inEdges: boolean, edgeFilter: (edge: EdgeType, idx: number) => boolean): Set<EdgeType> {
const frontier = new Set<EdgeType>();
for (const node of nodes) {
let edgeNumber = 0;
const edges = inEdges ? node.inputs : node.outputs;
for (const edge of edges) {
if (edgeFilter === undefined || edgeFilter(edge, edgeNumber)) {
frontier.add(edge);
}
++edgeNumber;
}
}
return frontier;
}
protected connectVisibleSelectedElements(): void {
for (const element of this.state.selection) {
element.inputs.forEach((edge: Edge<any>) => {
if (edge.source.visible && edge.target.visible) {
edge.visible = true;
}
});
element.outputs.forEach((edge: Edge<any>) => {
if (edge.source.visible && edge.target.visible) {
edge.visible = true;
}
});
}
}
protected showVisible() {
this.updateGraphVisibility();
this.viewWholeGraph();
this.focusOnSvg();
}
protected viewWholeGraph(): void {
this.panZoom.scaleTo(this.svg, 0);
this.panZoom.translateTo(this.svg,
this.graph.minGraphX + this.graph.width / 2,
this.graph.minGraphY + this.graph.height / 2);
}
private deleteContent(): void {
for (const item of this.toolbox.querySelectorAll(".graph-toolbox-item")) {
item.parentElement.removeChild(item);
}
if (!this.state.cacheLayout) {
this.updateGraphStateType(GraphStateType.NeedToFullRebuild);
}
this.graph.graphPhase.rendered = false;
this.updateGraphVisibility();
}
private getSvgViewDimensions(): [number, number] {
return [this.container.clientWidth, this.container.clientHeight];
}
private getSvgExtent(): [[number, number], [number, number]] {
return [[0, 0], [this.container.clientWidth, this.container.clientHeight]];
}
private createImgInput(id: string, title: string, onClick): HTMLElement {
const input = document.createElement("input");
input.setAttribute("id", id);
input.setAttribute("type", "image");
input.setAttribute("title", title);
input.setAttribute("src", `img/toolbox/${id}-icon.png`);
input.className = "button-input graph-toolbox-item";
input.addEventListener("click", onClick);
return input;
}
private createImgToggleInput(id: string, title: string, initState: boolean, onClick):
HTMLElement {
const input = this.createImgInput(id, title, onClick);
input.classList.toggle("button-input-toggled", initState);
return input;
}
}
export class MovableViewState {
public selection: SelectionMap;
public get hideDead(): boolean {
return storageGetItem("toggle-hide-dead", false);
}
public set hideDead(value: boolean) {
storageSetItem("toggle-hide-dead", value);
}
public get showTypes(): boolean {
return storageGetItem("toggle-types", false);
}
public set showTypes(value: boolean) {
storageSetItem("toggle-types", value);
}
public get showProperties(): boolean {
return storageGetItem("toggle-properties", false);
}
public set showProperties(value: boolean) {
storageSetItem("toggle-properties", value);
}
public get cacheLayout(): boolean {
return storageGetItem("toggle-cache-layout", true);
}
public set cacheLayout(value: boolean) {
storageSetItem("toggle-cache-layout", value);
}
}
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as d3 from "d3";
import * as C from "../common/constants";
import { partial } from "../common/util";
import { MovableView } from "./movable-view";
import { TurboshaftGraphPhase } from "../phases/turboshaft-graph-phase/turboshaft-graph-phase";
import { SelectionBroker } from "../selection/selection-broker";
import { SelectionMap } from "../selection/selection";
import { TurboshaftGraphBlock, TurboshaftGraphBlockType } from "../phases/turboshaft-graph-phase/turboshaft-graph-block";
import { TurboshaftGraphNode } from "../phases/turboshaft-graph-phase/turboshaft-graph-node";
import { TurboshaftGraphEdge } from "../phases/turboshaft-graph-phase/turboshaft-graph-edge";
import { TurboshaftGraph } from "../turboshaft-graph";
import { TurboshaftGraphLayout } from "../turboshaft-graph-layout";
import { GraphStateType } from "../phases/graph-phase/graph-phase";
import { OutputVisibilityType } from "../node";
import { BlockSelectionHandler, ClearableHandler } from "../selection/selection-handler";
export class TurboshaftGraphView extends MovableView<TurboshaftGraph> {
graphLayout: TurboshaftGraphLayout;
blocksSelectionHandler: BlockSelectionHandler & ClearableHandler;
visibleBlocks: d3.Selection<any, TurboshaftGraphBlock, any, any>;
visibleNodes: d3.Selection<any, TurboshaftGraphNode, any, any>;
visibleEdges: d3.Selection<any, TurboshaftGraphEdge<TurboshaftGraphBlock>, any, any>;
visibleBubbles: d3.Selection<any, any, any, any>;
blockDrag: d3.DragBehavior<any, TurboshaftGraphBlock, TurboshaftGraphBlock>;
constructor(idOrContainer: string | HTMLElement, broker: SelectionBroker,
showPhaseByName: (name: string) => void, toolbox: HTMLElement) {
super(idOrContainer, broker, showPhaseByName, toolbox);
this.state.selection = new SelectionMap((b: TurboshaftGraphBlock) => b.identifier());
this.visibleBlocks = this.graphElement.append("g");
this.visibleEdges = this.graphElement.append("g");
this.visibleNodes = this.graphElement.append("g");
this.blockDrag = d3.drag<any, TurboshaftGraphBlock, TurboshaftGraphBlock>()
.on("drag", (block: TurboshaftGraphBlock) => {
block.x += d3.event.dx;
block.y += d3.event.dy;
this.updateVisibleBlocks();
});
}
public initializeContent(data: TurboshaftGraphPhase, rememberedSelection: Map<string, any>):
void {
this.show();
this.addImgInput("layout", "layout graph",
partial(this.layoutAction, this));
this.addImgInput("show-all", "show all blocks",
partial(this.showAllBlocksAction, this));
this.addToggleImgInput("toggle-properties", "toggle propetries",
this.state.showProperties, partial(this.togglePropertiesAction, this));
this.addToggleImgInput("toggle-cache-layout", "toggle saving graph layout",
this.state.cacheLayout, partial(this.toggleLayoutCachingAction, this));
this.phaseName = data.name;
this.createGraph(data, rememberedSelection);
this.viewWholeGraph();
if (this.state.cacheLayout && data.transform) {
this.svg.call(this.panZoom.transform, d3.zoomIdentity
.translate(data.transform.x, data.transform.y)
.scale(data.transform.scale));
}
}
public updateGraphVisibility(): void {
if (!this.graph) return;
this.updateVisibleBlocks();
this.visibleNodes = d3.selectAll(".turboshaft-node");
this.visibleBubbles = d3.selectAll("circle");
this.updateInlineNodes();
this.updateInputAndOutputBubbles();
}
public svgKeyDown(): void {
}
public searchInputAction(searchInput: HTMLInputElement, e: Event, onlyVisible: boolean): void {
}
private createGraph(data: TurboshaftGraphPhase, selection) {
this.graph = new TurboshaftGraph(data);
this.graphLayout = new TurboshaftGraphLayout(this.graph);
if (!this.state.cacheLayout ||
this.graph.graphPhase.stateType == GraphStateType.NeedToFullRebuild) {
this.updateGraphStateType(GraphStateType.NeedToFullRebuild);
this.showAllBlocksAction(this);
} else {
this.showVisible();
}
this.layoutGraph();
this.updateGraphVisibility();
}
private layoutGraph(): void {
const layoutMessage = this.graph.graphPhase.stateType == GraphStateType.Cached
? "Layout graph from cache"
: "Layout graph";
console.time(layoutMessage);
this.graphLayout.rebuild(this.state.showProperties);
const extent = this.graph.redetermineGraphBoundingBox(this.state.showProperties);
this.panZoom.translateExtent(extent);
this.minScale(this.graph.width, this.graph.height);
console.timeEnd(layoutMessage);
}
private updateVisibleBlocks(): void {
const view = this;
// select existing blocks
const filteredBlocks = [
...this.graph.blocks(block => this.graph.isRendered() && block.visible)
];
const allBlocks = view.visibleBlocks
.selectAll<SVGGElement, TurboshaftGraphBlock>(".turboshaft-block");
const selBlocks = allBlocks.data(filteredBlocks, block => block.toString());
// remove old blocks
selBlocks.exit().remove();
// add new blocks
const newBlocks = selBlocks
.enter()
.append("g")
.classed("turboshaft-block", true)
.classed("block", b => b.type == TurboshaftGraphBlockType.Block)
.classed("merge", b => b.type == TurboshaftGraphBlockType.Merge)
.classed("loop", b => b.type == TurboshaftGraphBlockType.Loop)
.call(view.blockDrag);
newBlocks
.append("rect")
.attr("rx", 35)
.attr("ry", 35)
.attr("width", block => block.getWidth())
.attr("height", block => block.getHeight(view.state.showProperties));
newBlocks.each(function (block: TurboshaftGraphBlock) {
const svg = d3.select<SVGGElement, TurboshaftGraphBlock>(this);
svg
.append("text")
.attr("text-anchor", "middle")
.attr("x", block.getWidth() / 2)
.classed("block-label", true)
.append("tspan")
.text(block.displayLabel);
view.appendInlineNodes(svg, block);
view.appendInputAndOutputBubbles(svg, block);
});
newBlocks.merge(selBlocks)
.classed("selected", block => view.state.selection.isSelected(block))
.attr("transform", block => `translate(${block.x},${block.y})`)
.select("rect")
.attr("height", block => block.getHeight(view.state.showProperties));
}
private appendInlineNodes(svg: d3.Selection<SVGGElement, TurboshaftGraphBlock, any, any>,
block: TurboshaftGraphBlock): void {
const state = this.state;
const graph = this.graph;
const filteredNodes = [...block.nodes.filter(node => graph.isRendered() && node.visible)];
const allNodes = svg.selectAll<SVGGElement, TurboshaftGraphNode>(".turboshaft-inline-node");
const selNodes = allNodes.data(filteredNodes, node => node.toString());
// remove old nodes
selNodes.exit().remove();
// add new nodes
const newNodes = selNodes
.enter()
.append("g")
.classed("turboshaft-node", true)
.classed("inline-node", true);
let nodeY = block.labelBox.height;
const blockWidth = block.getWidth();
newNodes.each(function (node: TurboshaftGraphNode) {
const nodeSvg = d3.select(this);
nodeSvg
.attr("id", node.id)
.append("text")
.attr("dx", 25)
.classed("inline-node-label", true)
.attr("dy", nodeY)
.append("tspan")
.text(`${node.getInlineLabel()}[${node.getPropertiesTypeAbbreviation()}]`);
nodeY += node.labelBox.height;
if (node.properties) {
nodeSvg
.append("text")
.attr("dx", 25)
.classed("inline-node-properties", true)
.attr("dy", nodeY)
.append("tspan")
.text(node.getReadableProperties(blockWidth))
.append("title")
.text(node.properties);
nodeY += node.labelBox.height;
}
});
newNodes.merge(selNodes)
.classed("selected", node => state.selection.isSelected(node))
.select("rect")
.attr("height", node => node.getHeight(state.showProperties));
}
private updateInputAndOutputBubbles(): void {
const view = this;
const graph = this.graph;
this.visibleBubbles.classed("filledBubbleStyle", function () {
const components = this.id.split(",");
if (components[0] === "ib") {
return graph.blockMap[components[3]].inputs[components[2]].isVisible();
}
return graph.blockMap[components[1]].areAnyOutputsVisible()
== OutputVisibilityType.AllNodesVisible;
}).classed("halfFilledBubbleStyle", function () {
const components = this.id.split(",");
if (components[0] === "ib") return false;
return graph.blockMap[components[1]].areAnyOutputsVisible()
== OutputVisibilityType.SomeNodesVisible;
}).classed("bubbleStyle", function () {
const components = this.id.split(",");
if (components[0] === "ib") {
return !graph.blockMap[components[3]].inputs[components[2]].isVisible();
}
return graph.blockMap[components[1]].areAnyOutputsVisible()
== OutputVisibilityType.NoVisibleNodes;
});
this.visibleBubbles.each(function () {
const components = this.id.split(",");
if (components[0] === "ob") {
const from = graph.blockMap[components[1]];
const x = from.getOutputX();
const y = from.getHeight(view.state.showProperties) + C.DEFAULT_NODE_BUBBLE_RADIUS;
this.setAttribute("transform", `translate(${x},${y})`);
}
});
}
private appendInputAndOutputBubbles(
svg: d3.Selection<SVGGElement, TurboshaftGraphBlock, any, any>,
block: TurboshaftGraphBlock): void {
for (let i = 0; i < block.inputs.length; i++) {
const x = block.getInputX(i);
const y = -C.DEFAULT_NODE_BUBBLE_RADIUS;
svg.append("circle")
.classed("filledBubbleStyle", block.inputs[i].isVisible())
.classed("bubbleStyle", !block.inputs[i].isVisible())
.attr("id", `ib,${block.inputs[i].toString(i)}`)
.attr("r", C.DEFAULT_NODE_BUBBLE_RADIUS)
.attr("transform", `translate(${x},${y})`);
}
if (block.outputs.length > 0) {
const x = block.getOutputX();
const y = block.getHeight(this.state.showProperties) + C.DEFAULT_NODE_BUBBLE_RADIUS;
svg.append("circle")
.classed("filledBubbleStyle", block.areAnyOutputsVisible()
== OutputVisibilityType.AllNodesVisible)
.classed("halFilledBubbleStyle", block.areAnyOutputsVisible()
== OutputVisibilityType.SomeNodesVisible)
.classed("bubbleStyle", block.areAnyOutputsVisible()
== OutputVisibilityType.NoVisibleNodes)
.attr("id", `ob,${block.id}`)
.attr("r", C.DEFAULT_NODE_BUBBLE_RADIUS)
.attr("transform", `translate(${x},${y})`);
}
}
private updateInlineNodes(): void {
const state = this.state;
let totalHeight = 0;
let blockId = 0;
this.visibleNodes.each(function (node: TurboshaftGraphNode) {
if (blockId != node.block.id) {
blockId = node.block.id;
totalHeight = 0;
}
totalHeight += node.getHeight(state.showProperties);
const nodeSvg = d3.select(this);
const nodeY = state.showProperties && node.properties
? totalHeight - node.labelBox.height
: totalHeight;
nodeSvg.select(".inline-node-label").attr("dy", nodeY);
nodeSvg.select(".inline-node-properties").attr("visibility", state.showProperties
? "visible"
: "hidden");
});
}
// Actions (handlers of toolbox menu and hotkeys events)
private layoutAction(view: TurboshaftGraphView): void {
view.updateGraphStateType(GraphStateType.NeedToFullRebuild);
view.layoutGraph();
view.updateGraphVisibility();
view.viewWholeGraph();
view.focusOnSvg();
}
private showAllBlocksAction(view: TurboshaftGraphView): void {
for (const node of view.graph.blocks()) {
node.visible = true;
}
for (const edge of view.graph.blocksEdges()) {
edge.visible = true;
}
view.showVisible();
}
private togglePropertiesAction(view: TurboshaftGraphView): void {
view.state.showProperties = !view.state.showProperties;
const element = document.getElementById("toggle-properties");
element.classList.toggle("button-input-toggled", view.state.showProperties);
view.updateGraphVisibility();
view.focusOnSvg();
}
private toggleLayoutCachingAction(view: TurboshaftGraphView): void {
view.state.cacheLayout = !view.state.cacheLayout;
const element = document.getElementById("toggle-cache-layout");
element.classList.toggle("button-input-toggled", view.state.cacheLayout);
}
}
......@@ -39,7 +39,12 @@
"src/views/disassembly-view.ts",
"src/views/text-view.ts",
"src/views/info-view.ts",
"src/views/movable-view.ts",
"src/views/turboshaft-graph-view.ts",
"src/origin.ts",
"src/movable-container.ts",
"src/turboshaft-graph.ts",
"src/turboshaft-graph-layout.ts",
"src/position.ts",
"src/source.ts",
"src/node.ts",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment