diff --git a/tools/turbolizer/css/turboshaft.css b/tools/turbolizer/css/turboshaft.css index f5c6a707ec..3373d5a84b 100644 --- a/tools/turbolizer/css/turboshaft.css +++ b/tools/turbolizer/css/turboshaft.css @@ -1,3 +1,19 @@ +:root { + --input: #50de89; + --output: #ff5b64; + --select: #ffd800; +} + +path.input { + stroke: var(--input); + stroke-width: 16px; +} + +path.output { + stroke: var(--output); + stroke-width: 16px; +} + g.turboshaft-block rect { stroke-dasharray: 20; stroke-width: 7; @@ -8,18 +24,24 @@ g.turboshaft-block:hover rect { stroke-width: 10; } +g.turboshaft-block.selected rect { + stroke-dasharray: 0; + stroke-width: 15; + stroke: var(--select); +} + g.block rect { - fill: #ecf3fe; + fill: #eef4fd; stroke: #4285f4; } g.merge rect { - fill: #e9fcee; + fill: #ecfcf0; stroke: #2bde5a; } g.loop rect { - fill: #fdecea; + fill: #fdf0ee; stroke: #e94235; } @@ -39,6 +61,32 @@ g.loop rect { fill: #ea4335; } -.inline-node-properties tspan { - fill: #9227b0; +.inline-node-label tspan { + fill: #344344; +} + +.inline-node-label:hover tspan { + fill: #5a6c6c; +} + +.inline-node-label.selected tspan { + fill: var(--select); +} + +.inline-node-properties tspan { + fill: #ca48f6; +} + +g.turboshaft-node.input .inline-node-label tspan { + fill: var(--input); +} + +g.turboshaft-node.output .inline-node-label tspan { + fill: var(--output); +} + +#layout-type-select { + box-sizing: border-box; + height: 1.5em; + margin-left: 5px; } diff --git a/tools/turbolizer/src/common/constants.ts b/tools/turbolizer/src/common/constants.ts index b2e5cbba05..5a61b22721 100644 --- a/tools/turbolizer/src/common/constants.ts +++ b/tools/turbolizer/src/common/constants.ts @@ -5,6 +5,10 @@ export const MAX_RANK_SENTINEL = 0; export const BEZIER_CONSTANT = 0.3; export const GRAPH_MARGIN = 250; +export const TURBOSHAFT_NODE_X_INDENT = 25; +export const TURBOSHAFT_BLOCK_BORDER_RADIUS = 35; +export const TURBOSHAFT_BLOCK_ROW_SEPARATION = 200; +export const ARROW_HEAD_HEIGHT = 7; export const DEFAULT_NODE_BUBBLE_RADIUS = 12; export const NODE_INPUT_WIDTH = 50; export const MINIMUM_NODE_OUTPUT_APPROACH = 15; diff --git a/tools/turbolizer/src/common/util.ts b/tools/turbolizer/src/common/util.ts index 07278a3227..aee4e7b739 100644 --- a/tools/turbolizer/src/common/util.ts +++ b/tools/turbolizer/src/common/util.ts @@ -27,12 +27,13 @@ export function camelize(obj: any): any { return obj; } -export function sortUnique(arr: Array, f: (a: T, b: T) => number, equal: (a: T, b: T) => boolean): Array { +export function sortUnique(arr: Array, comparator: (a: T, b: T) => number, + equals: (a: T, b: T) => boolean): Array { if (arr.length == 0) return arr; - arr = arr.sort(f); + arr = arr.sort(comparator); const uniqueArr = [arr[0]]; for (let i = 1; i < arr.length; i++) { - if (!equal(arr[i - 1], arr[i])) { + if (!equals(arr[i - 1], arr[i])) { uniqueArr.push(arr[i]); } } diff --git a/tools/turbolizer/src/edge.ts b/tools/turbolizer/src/edge.ts index b6709db514..b65611c1da 100644 --- a/tools/turbolizer/src/edge.ts +++ b/tools/turbolizer/src/edge.ts @@ -2,25 +2,82 @@ // 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 { 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"; +import { Graph } from "./graph"; +import { TurboshaftGraph } from "./turboshaft-graph"; export abstract class Edge { target: NodeType; source: NodeType; + index: number; backEdgeNumber: number; visible: boolean; - constructor(target: NodeType, source: NodeType) { + constructor(target: NodeType, index: number, source: NodeType) { this.target = target; + this.index = index; this.source = source; this.backEdgeNumber = 0; this.visible = false; } + public getInputHorizontalPosition(graph: Graph | TurboshaftGraph, extendHeight: boolean): number { + if (graph.graphPhase.rendered && this.backEdgeNumber > 0) { + return graph.maxGraphNodeX + this.backEdgeNumber * C.MINIMUM_EDGE_SEPARATION; + } + const source = this.source; + const target = this.target; + const index = this.index; + const inputX = target.x + target.getInputX(index); + const inputApproach = target.getInputApproach(this.index); + const outputApproach = source.getOutputApproach(extendHeight); + if (inputApproach > outputApproach) { + return inputX; + } + const inputOffset = C.MINIMUM_EDGE_SEPARATION * (index + 1); + return target.x < source.x + ? target.x + target.getWidth() + inputOffset + : target.x - inputOffset; + } + + public generatePath(graph: Graph | TurboshaftGraph, extendHeight: boolean): string { + const target = this.target; + const source = this.source; + const inputX = target.x + target.getInputX(this.index); + const inputY = target.y - 2 * C.DEFAULT_NODE_BUBBLE_RADIUS - C.ARROW_HEAD_HEIGHT; + const outputX = source.x + source.getOutputX(); + const outputY = source.y + source.getHeight(extendHeight) + C.DEFAULT_NODE_BUBBLE_RADIUS; + let inputApproach = target.getInputApproach(this.index); + const outputApproach = source.getOutputApproach(extendHeight); + const horizontalPos = this.getInputHorizontalPosition(graph, extendHeight); + + let path: string; + + if (inputY < outputY) { + path = `M ${outputX} ${outputY}\nL ${outputX} ${outputApproach}\nL ${horizontalPos} ${outputApproach}`; + if (horizontalPos !== inputX) { + path += `L ${horizontalPos} ${inputApproach}`; + } else if (inputApproach < outputApproach) { + inputApproach = outputApproach; + } + path += `L ${inputX} ${inputApproach}\nL ${inputX} ${inputY}`; + } else { + const controlY = outputY + (inputY - outputY) * C.BEZIER_CONSTANT; + path = `M ${outputX} ${outputY}\nC ${outputX} ${controlY},\n${inputX} ${outputY},\n${inputX} ${inputY}`; + } + + return path; + } + public isVisible(): boolean { return this.visible && this.source.visible && this.target.visible; } + + public toString(): string { + return `${this.source.id},${this.index},${this.target.id}`; + } } diff --git a/tools/turbolizer/src/graph-layout.ts b/tools/turbolizer/src/graph-layout.ts index 3f68df79b3..2ad4b096b6 100644 --- a/tools/turbolizer/src/graph-layout.ts +++ b/tools/turbolizer/src/graph-layout.ts @@ -7,17 +7,18 @@ import { Graph } from "./graph"; import { GraphNode } from "./phases/graph-phase/graph-node"; import { GraphEdge } from "./phases/graph-phase/graph-edge"; import { GraphStateType } from "./phases/graph-phase/graph-phase"; +import { LayoutOccupation } from "./layout-occupation"; export class GraphLayout { graph: Graph; - graphOccupation: GraphOccupation; + layoutOccupation: LayoutOccupation; startTime: number; maxRank: number; visitOrderWithinRank: number; constructor(graph: Graph) { this.graph = graph; - this.graphOccupation = new GraphOccupation(graph); + this.layoutOccupation = new LayoutOccupation(graph); this.maxRank = 0; this.visitOrderWithinRank = 0; } @@ -194,7 +195,7 @@ export class GraphLayout { // compact and not overlapping live input lines. rankSets.reverse().forEach((rankSet: Array) => { for (const node of rankSet) { - this.graphOccupation.clearNodeOutputs(node, showTypes); + this.layoutOccupation.clearOutputs(node, showTypes); } this.traceOccupation("After clearing outputs"); @@ -203,7 +204,7 @@ export class GraphLayout { rankSet = rankSet.sort((a: GraphNode, b: GraphNode) => a.compare(b)); for (const node of rankSet) { if (node.visible) { - node.x = this.graphOccupation.occupyNode(node); + node.x = this.layoutOccupation.occupy(node); 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; @@ -215,12 +216,12 @@ export class GraphLayout { this.traceOccupation("Before clearing nodes"); - this.graphOccupation.clearOccupiedNodes(); + this.layoutOccupation.clearOccupied(); this.traceOccupation("After clearing nodes"); for (const node of rankSet) { - this.graphOccupation.occupyNodeInputs(node, showTypes); + this.layoutOccupation.occupyInputs(node, showTypes); } this.traceOccupation("After occupying inputs and determining bounding box"); @@ -247,219 +248,7 @@ export class GraphLayout { private traceOccupation(message: string): void { if (C.TRACE_LAYOUT) { console.log(message); - this.graphOccupation.print(); - } - } -} - -class GraphOccupation { - graph: Graph; - filledSlots: Array; - nodeOccupations: Array<[number, number]>; - minSlot: number; - maxSlot: number; - - constructor(graph: Graph) { - this.graph = graph; - this.filledSlots = new Array(); - this.nodeOccupations = new Array<[number, number]>(); - this.minSlot = 0; - this.maxSlot = 0; - } - - public clearNodeOutputs(source: GraphNode, showTypes: boolean): void { - for (const edge of source.outputs) { - if (!edge.isVisible()) continue; - for (const inputEdge of edge.target.inputs) { - if (inputEdge.source === source) { - const horizontalPos = edge.getInputHorizontalPosition(this.graph, showTypes); - this.clearPositionRangeWithMargin(horizontalPos, horizontalPos, C.NODE_INPUT_WIDTH / 2); - } - } - } - } - - public clearOccupiedNodes(): void { - for (const [firstSlot, endSlotExclusive] of this.nodeOccupations) { - this.clearSlotRange(firstSlot, endSlotExclusive); - } - this.nodeOccupations = new Array<[number, number]>(); - } - - public occupyNode(node: GraphNode): number { - const width = node.getWidth(); - const margin = C.MINIMUM_EDGE_SEPARATION; - const paddedWidth = width + 2 * margin; - const [direction, position] = this.getPlacementHint(node); - const x = position - paddedWidth + margin; - this.trace(`Node ${node.id} placement hint [${x}, ${(x + paddedWidth)})`); - const placement = this.findSpace(x, paddedWidth, direction); - const [firstSlot, slotWidth] = placement; - const endSlotExclusive = firstSlot + slotWidth - 1; - this.occupySlotRange(firstSlot, endSlotExclusive); - this.nodeOccupations.push([firstSlot, endSlotExclusive]); - if (direction < 0) { - return this.slotToLeftPosition(firstSlot + slotWidth) - width - margin; - } else if (direction > 0) { - return this.slotToLeftPosition(firstSlot) + margin; - } else { - return this.slotToLeftPosition(firstSlot + slotWidth / 2) - (width / 2); - } - } - - public occupyNodeInputs(node: GraphNode, showTypes: boolean): void { - for (let i = 0; i < node.inputs.length; ++i) { - if (node.inputs[i].isVisible()) { - const edge = node.inputs[i]; - if (!edge.isBackEdge()) { - const horizontalPos = edge.getInputHorizontalPosition(this.graph, showTypes); - this.trace(`Occupying input ${i} of ${node.id} at ${horizontalPos}`); - this.occupyPositionRangeWithMargin(horizontalPos, horizontalPos, C.NODE_INPUT_WIDTH / 2); - } - } - } - } - - public print(): void { - let output = ""; - for (let currentSlot = -40; currentSlot < 40; ++currentSlot) { - if (currentSlot != 0) { - output += " "; - } else { - output += "|"; - } - } - console.log(output); - output = ""; - for (let currentSlot2 = -40; currentSlot2 < 40; ++currentSlot2) { - if (this.filledSlots[this.slotToIndex(currentSlot2)]) { - output += "*"; - } else { - output += " "; - } - } - console.log(output); - } - - private getPlacementHint(node: GraphNode): [number, number] { - let position = 0; - let direction = -1; - let outputEdges = 0; - let inputEdges = 0; - for (const outputEdge of node.outputs) { - if (!outputEdge.isVisible()) continue; - const output = outputEdge.target; - for (let l = 0; l < output.inputs.length; ++l) { - if (output.rank > node.rank) { - const inputEdge = output.inputs[l]; - if (inputEdge.isVisible()) ++inputEdges; - if (output.inputs[l].source == node) { - position += output.x + output.getInputX(l) + C.NODE_INPUT_WIDTH / 2; - outputEdges++; - if (l >= (output.inputs.length / 2)) { - direction = 1; - } - } - } - } - } - if (outputEdges != 0) { - position /= outputEdges; - } - if (outputEdges > 1 || inputEdges == 1) { - direction = 0; - } - return [direction, position]; - } - - private occupyPositionRange(from: number, to: number): void { - this.occupySlotRange(this.positionToSlot(from), this.positionToSlot(to - 1)); - } - - private clearPositionRange(from: number, to: number): void { - this.clearSlotRange(this.positionToSlot(from), this.positionToSlot(to - 1)); - } - - private occupySlotRange(from: number, to: number): void { - this.trace(`Occupied [${this.slotToLeftPosition(from)} ${this.slotToLeftPosition(to + 1)})`); - this.setIndexRange(from, to, true); - } - - private clearSlotRange(from: number, to: number): void { - this.trace(`Cleared [${this.slotToLeftPosition(from)} ${this.slotToLeftPosition(to + 1)})`); - this.setIndexRange(from, to, false); - } - - private clearPositionRangeWithMargin(from: number, to: number, margin: number): void { - const fromMargin = from - Math.floor(margin); - const toMargin = to + Math.floor(margin); - this.clearPositionRange(fromMargin, toMargin); - } - - private occupyPositionRangeWithMargin(from: number, to: number, margin: number): void { - const fromMargin = from - Math.floor(margin); - const toMargin = to + Math.floor(margin); - this.occupyPositionRange(fromMargin, toMargin); - } - - private findSpace(pos: number, width: number, direction: number): [number, number] { - const widthSlots = Math.floor((width + C.NODE_INPUT_WIDTH - 1) / - C.NODE_INPUT_WIDTH); - - const currentSlot = this.positionToSlot(pos + width / 2); - let widthSlotsRemainingLeft = widthSlots; - let widthSlotsRemainingRight = widthSlots; - let slotsChecked = 0; - - while (true) { - const mod = slotsChecked++ % 2; - const currentScanSlot = currentSlot + (mod ? -1 : 1) * (slotsChecked >> 1); - if (!this.filledSlots[this.slotToIndex(currentScanSlot)]) { - if (mod) { - if (direction <= 0) --widthSlotsRemainingLeft; - } else if (direction >= 0) { - --widthSlotsRemainingRight; - } - if (widthSlotsRemainingLeft == 0 || widthSlotsRemainingRight == 0 || - (widthSlotsRemainingLeft + widthSlotsRemainingRight) == widthSlots && - (widthSlots == slotsChecked)) { - return mod ? [currentScanSlot, widthSlots] - : [currentScanSlot - widthSlots + 1, widthSlots]; - } - } else { - if (mod) { - widthSlotsRemainingLeft = widthSlots; - } else { - widthSlotsRemainingRight = widthSlots; - } - } - } - } - - private setIndexRange(from: number, to: number, value: boolean): void { - if (to < from) throw ("Illegal slot range"); - while (from <= to) { - this.maxSlot = Math.max(from, this.maxSlot); - this.minSlot = Math.min(from, this.minSlot); - this.filledSlots[this.slotToIndex(from++)] = value; - } - } - - private positionToSlot(position: number): number { - return Math.floor(position / C.NODE_INPUT_WIDTH); - } - - private slotToIndex(slot: number): number { - return slot >= 0 ? slot * 2 : slot * 2 + 1; - } - - private slotToLeftPosition(slot: number): number { - return slot * C.NODE_INPUT_WIDTH; - } - - private trace(message): void { - if (C.TRACE_LAYOUT) { - console.log(message); + this.layoutOccupation.print(); } } } diff --git a/tools/turbolizer/src/graph.ts b/tools/turbolizer/src/graph.ts index 79b785704d..a1e84d3213 100644 --- a/tools/turbolizer/src/graph.ts +++ b/tools/turbolizer/src/graph.ts @@ -10,11 +10,12 @@ import { MovableContainer } from "./movable-container"; export class Graph extends MovableContainer { nodeMap: Array; - maxBackEdgeNumber: number; + originNodesMap: Map>; constructor(graphPhase: GraphPhase) { super(graphPhase); this.nodeMap = graphPhase.nodeIdToNodeMap; + this.originNodesMap = graphPhase.originIdToNodesMap; } public *nodes(func = (n: GraphNode) => true) { diff --git a/tools/turbolizer/src/layout-occupation.ts b/tools/turbolizer/src/layout-occupation.ts new file mode 100644 index 0000000000..892c56f901 --- /dev/null +++ b/tools/turbolizer/src/layout-occupation.ts @@ -0,0 +1,221 @@ +// 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 { Graph } from "./graph"; +import { GraphNode } from "./phases/graph-phase/graph-node"; +import { TurboshaftGraph } from "./turboshaft-graph"; +import { TurboshaftGraphBlock } from "./phases/turboshaft-graph-phase/turboshaft-graph-block"; + +export class LayoutOccupation { + graph: Graph | TurboshaftGraph; + filledSlots: Array; + occupations: Array<[number, number]>; + minSlot: number; + maxSlot: number; + + constructor(graph: Graph | TurboshaftGraph) { + this.graph = graph; + this.filledSlots = new Array(); + this.occupations = new Array<[number, number]>(); + this.minSlot = 0; + this.maxSlot = 0; + } + + public clearOutputs(source: GraphNode | TurboshaftGraphBlock, showTypes: boolean): void { + for (const edge of source.outputs) { + if (!edge.isVisible()) continue; + for (const inputEdge of edge.target.inputs) { + if (inputEdge.source === source) { + const horizontalPos = edge.getInputHorizontalPosition(this.graph, showTypes); + this.clearPositionRangeWithMargin(horizontalPos, horizontalPos, C.NODE_INPUT_WIDTH / 2); + } + } + } + } + + public clearOccupied(): void { + for (const [firstSlot, endSlotExclusive] of this.occupations) { + this.clearSlotRange(firstSlot, endSlotExclusive); + } + this.occupations = new Array<[number, number]>(); + } + + public occupy(item: GraphNode | TurboshaftGraphBlock): number { + const width = item.getWidth(); + const margin = C.MINIMUM_EDGE_SEPARATION; + const paddedWidth = width + 2 * margin; + const [direction, position] = this.getPlacementHint(item); + const x = position - paddedWidth + margin; + this.trace(`${item.id} placement hint [${x}, ${(x + paddedWidth)})`); + const placement = this.findSpace(x, paddedWidth, direction); + const [firstSlot, slotWidth] = placement; + const endSlotExclusive = firstSlot + slotWidth - 1; + this.occupySlotRange(firstSlot, endSlotExclusive); + this.occupations.push([firstSlot, endSlotExclusive]); + if (direction < 0) { + return this.slotToLeftPosition(firstSlot + slotWidth) - width - margin; + } else if (direction > 0) { + return this.slotToLeftPosition(firstSlot) + margin; + } else { + return this.slotToLeftPosition(firstSlot + slotWidth / 2) - (width / 2); + } + } + + public occupyInputs(item: GraphNode | TurboshaftGraphBlock, showTypes: boolean): void { + for (let i = 0; i < item.inputs.length; ++i) { + if (item.inputs[i].isVisible()) { + const edge = item.inputs[i]; + if (!edge.isBackEdge()) { + const horizontalPos = edge.getInputHorizontalPosition(this.graph, showTypes); + this.trace(`Occupying input ${i} of ${item.id} at ${horizontalPos}`); + this.occupyPositionRangeWithMargin(horizontalPos, horizontalPos, C.NODE_INPUT_WIDTH / 2); + } + } + } + } + + public print(): void { + let output = ""; + for (let currentSlot = -40; currentSlot < 40; ++currentSlot) { + if (currentSlot != 0) { + output += " "; + } else { + output += "|"; + } + } + console.log(output); + output = ""; + for (let currentSlot2 = -40; currentSlot2 < 40; ++currentSlot2) { + if (this.filledSlots[this.slotToIndex(currentSlot2)]) { + output += "*"; + } else { + output += " "; + } + } + console.log(output); + } + + private getPlacementHint(item: GraphNode | TurboshaftGraphBlock): [number, number] { + let position = 0; + let direction = -1; + let outputEdges = 0; + let inputEdges = 0; + for (const outputEdge of item.outputs) { + if (!outputEdge.isVisible()) continue; + const output = outputEdge.target; + for (let l = 0; l < output.inputs.length; ++l) { + if (output.rank > item.rank) { + const inputEdge = output.inputs[l]; + if (inputEdge.isVisible()) ++inputEdges; + if (output.inputs[l].source == item) { + position += output.x + output.getInputX(l) + C.NODE_INPUT_WIDTH / 2; + outputEdges++; + if (l >= (output.inputs.length / 2)) { + direction = 1; + } + } + } + } + } + if (outputEdges != 0) { + position /= outputEdges; + } + if (outputEdges > 1 || inputEdges == 1) { + direction = 0; + } + return [direction, position]; + } + + private occupyPositionRange(from: number, to: number): void { + this.occupySlotRange(this.positionToSlot(from), this.positionToSlot(to - 1)); + } + + private clearPositionRange(from: number, to: number): void { + this.clearSlotRange(this.positionToSlot(from), this.positionToSlot(to - 1)); + } + + private occupySlotRange(from: number, to: number): void { + this.trace(`Occupied [${this.slotToLeftPosition(from)} ${this.slotToLeftPosition(to + 1)})`); + this.setIndexRange(from, to, true); + } + + private clearSlotRange(from: number, to: number): void { + this.trace(`Cleared [${this.slotToLeftPosition(from)} ${this.slotToLeftPosition(to + 1)})`); + this.setIndexRange(from, to, false); + } + + private clearPositionRangeWithMargin(from: number, to: number, margin: number): void { + const fromMargin = from - Math.floor(margin); + const toMargin = to + Math.floor(margin); + this.clearPositionRange(fromMargin, toMargin); + } + + private occupyPositionRangeWithMargin(from: number, to: number, margin: number): void { + const fromMargin = from - Math.floor(margin); + const toMargin = to + Math.floor(margin); + this.occupyPositionRange(fromMargin, toMargin); + } + + private findSpace(pos: number, width: number, direction: number): [number, number] { + const widthSlots = Math.floor((width + C.NODE_INPUT_WIDTH - 1) / + C.NODE_INPUT_WIDTH); + + const currentSlot = this.positionToSlot(pos + width / 2); + let widthSlotsRemainingLeft = widthSlots; + let widthSlotsRemainingRight = widthSlots; + let slotsChecked = 0; + + while (true) { + const mod = slotsChecked++ % 2; + const currentScanSlot = currentSlot + (mod ? -1 : 1) * (slotsChecked >> 1); + if (!this.filledSlots[this.slotToIndex(currentScanSlot)]) { + if (mod) { + if (direction <= 0) --widthSlotsRemainingLeft; + } else if (direction >= 0) { + --widthSlotsRemainingRight; + } + if (widthSlotsRemainingLeft == 0 || widthSlotsRemainingRight == 0 || + (widthSlotsRemainingLeft + widthSlotsRemainingRight) == widthSlots && + (widthSlots == slotsChecked)) { + return mod ? [currentScanSlot, widthSlots] + : [currentScanSlot - widthSlots + 1, widthSlots]; + } + } else { + if (mod) { + widthSlotsRemainingLeft = widthSlots; + } else { + widthSlotsRemainingRight = widthSlots; + } + } + } + } + + private setIndexRange(from: number, to: number, value: boolean): void { + if (to < from) throw ("Illegal slot range"); + while (from <= to) { + this.maxSlot = Math.max(from, this.maxSlot); + this.minSlot = Math.min(from, this.minSlot); + this.filledSlots[this.slotToIndex(from++)] = value; + } + } + + private positionToSlot(position: number): number { + return Math.floor(position / C.NODE_INPUT_WIDTH); + } + + private slotToIndex(slot: number): number { + return slot >= 0 ? slot * 2 : slot * 2 + 1; + } + + private slotToLeftPosition(slot: number): number { + return slot * C.NODE_INPUT_WIDTH; + } + + private trace(message): void { + if (C.TRACE_LAYOUT) { + console.log(message); + } + } +} diff --git a/tools/turbolizer/src/movable-container.ts b/tools/turbolizer/src/movable-container.ts index c33f733c3e..92da716f27 100644 --- a/tools/turbolizer/src/movable-container.ts +++ b/tools/turbolizer/src/movable-container.ts @@ -7,6 +7,7 @@ import { TurboshaftGraphPhase } from "./phases/turboshaft-graph-phase/turboshaft export abstract class MovableContainer { graphPhase: GraphPhaseType; + maxBackEdgeNumber: number; minGraphX: number; maxGraphX: number; maxGraphNodeX: number; diff --git a/tools/turbolizer/src/node.ts b/tools/turbolizer/src/node.ts index 9ae413a62c..338e327198 100644 --- a/tools/turbolizer/src/node.ts +++ b/tools/turbolizer/src/node.ts @@ -16,6 +16,9 @@ export abstract class Node; outputs: Array; visible: boolean; + outputApproach: number; + visitOrderWithinRank: number; + rank: number; x: number; y: number; labelBox: { width: number, height: number }; @@ -29,9 +32,12 @@ export abstract class Node(); this.outputs = new Array(); this.visible = false; + this.outputApproach = C.MINIMUM_NODE_OUTPUT_APPROACH; + this.visitOrderWithinRank = 0; + this.rank = C.MAX_RANK_SENTINEL; this.x = 0; this.y = 0; - this.labelBox = measureText(this.displayLabel); + if (displayLabel) this.labelBox = measureText(this.displayLabel); } public areAnyOutputsVisible(): OutputVisibilityType { @@ -81,6 +87,25 @@ export abstract class Node): number { + if (this.visitOrderWithinRank < other.visitOrderWithinRank) { + return -1; + } else if (this.visitOrderWithinRank == other.visitOrderWithinRank) { + return 0; + } + return 1; + } + public identifier(): string { return `${this.id}`; } diff --git a/tools/turbolizer/src/phases/graph-phase/graph-edge.ts b/tools/turbolizer/src/phases/graph-phase/graph-edge.ts index 2b132a89a7..b75eb4bd02 100644 --- a/tools/turbolizer/src/phases/graph-phase/graph-edge.ts +++ b/tools/turbolizer/src/phases/graph-phase/graph-edge.ts @@ -2,75 +2,18 @@ // 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 { Graph } from "../../graph"; import { Edge } from "../../edge"; import { GraphNode } from "./graph-node"; export class GraphEdge extends Edge { - index: number; type: string; constructor(target: GraphNode, index: number, source: GraphNode, type: string) { - super(target, source); - this.index = index; + super(target, index, source); this.type = type; } - public getInputHorizontalPosition(graph: Graph, showTypes: boolean): number { - if (graph.graphPhase.rendered && this.backEdgeNumber > 0) { - return graph.maxGraphNodeX + this.backEdgeNumber * C.MINIMUM_EDGE_SEPARATION; - } - const source = this.source; - const target = this.target; - const index = this.index; - const inputX = target.x + target.getInputX(index); - const inputApproach = target.getInputApproach(this.index); - const outputApproach = source.getOutputApproach(showTypes); - if (inputApproach > outputApproach) { - return inputX; - } - const inputOffset = C.MINIMUM_EDGE_SEPARATION * (index + 1); - return target.x < source.x - ? target.x + target.getWidth() + inputOffset - : target.x - inputOffset; - } - - public generatePath(graph: Graph, showTypes: boolean): string { - const target = this.target; - const source = this.source; - const inputX = target.x + target.getInputX(this.index); - 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.getHeight(showTypes) + C.DEFAULT_NODE_BUBBLE_RADIUS; - let inputApproach = target.getInputApproach(this.index); - const outputApproach = source.getOutputApproach(showTypes); - const horizontalPos = this.getInputHorizontalPosition(graph, showTypes); - - let path: string; - - if (inputY < outputY) { - path = `M ${outputX} ${outputY}\nL ${outputX} ${outputApproach}\nL ${horizontalPos} ${outputApproach}`; - if (horizontalPos !== inputX) { - path += `L ${horizontalPos} ${inputApproach}`; - } else if (inputApproach < outputApproach) { - inputApproach = outputApproach; - } - path += `L ${inputX} ${inputApproach}\nL ${inputX} ${inputY}`; - } else { - const controlY = outputY + (inputY - outputY) * C.BEZIER_CONSTANT; - path = `M ${outputX} ${outputY}\nC ${outputX} ${controlY},\n${inputX} ${outputY},\n${inputX} ${inputY}`; - } - - return path; - } - public isBackEdge(): boolean { return this.target.hasBackEdges() && (this.target.rank < this.source.rank); } - - public toString(): string { - return `${this.source.id},${this.index},${this.target.id}`; - } } diff --git a/tools/turbolizer/src/phases/graph-phase/graph-node.ts b/tools/turbolizer/src/phases/graph-phase/graph-node.ts index 4704d876b6..9aaf3e08a2 100644 --- a/tools/turbolizer/src/phases/graph-phase/graph-node.ts +++ b/tools/turbolizer/src/phases/graph-phase/graph-node.ts @@ -10,18 +10,13 @@ import { GraphEdge } from "./graph-edge"; export class GraphNode extends Node { nodeLabel: NodeLabel; - rank: number; - outputApproach: number; cfg: boolean; width: number; normalHeight: number; - visitOrderWithinRank: number; constructor(nodeLabel: NodeLabel) { super(nodeLabel.id, nodeLabel.getDisplayLabel()); this.nodeLabel = nodeLabel; - this.rank = C.MAX_RANK_SENTINEL; - this.outputApproach = C.MINIMUM_NODE_OUTPUT_APPROACH; // Every control node is a CFG node. this.cfg = nodeLabel.control; const typeBox = measureText(this.getDisplayType()); @@ -29,7 +24,6 @@ export class GraphNode extends Node { 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 { @@ -109,29 +103,10 @@ export class GraphNode extends Node { return deepestRank; } - public getInputApproach(index: number): number { - return this.y - C.MINIMUM_NODE_INPUT_APPROACH - - (index % 4) * C.MINIMUM_EDGE_SEPARATION - C.DEFAULT_NODE_BUBBLE_RADIUS; - } - - public getOutputApproach(showTypes: boolean): number { - return this.y + this.outputApproach + this.getHeight(showTypes) + - + C.DEFAULT_NODE_BUBBLE_RADIUS; - } - public hasBackEdges(): boolean { return (this.nodeLabel.opcode === "Loop") || ((this.nodeLabel.opcode === "Phi" || this.nodeLabel.opcode === "EffectPhi" || this.nodeLabel.opcode === "InductionVariablePhi") && this.inputs[this.inputs.length - 1].source.nodeLabel.opcode === "Loop"); } - - public compare(other: GraphNode): number { - if (this.visitOrderWithinRank < other.visitOrderWithinRank) { - return -1; - } else if (this.visitOrderWithinRank == other.visitOrderWithinRank) { - return 0; - } - return 1; - } } diff --git a/tools/turbolizer/src/phases/graph-phase/graph-phase.ts b/tools/turbolizer/src/phases/graph-phase/graph-phase.ts index 133db2884b..d3a8fd9897 100644 --- a/tools/turbolizer/src/phases/graph-phase/graph-phase.ts +++ b/tools/turbolizer/src/phases/graph-phase/graph-phase.ts @@ -15,6 +15,7 @@ export class GraphPhase extends Phase { stateType: GraphStateType; nodeLabelMap: Array; nodeIdToNodeMap: Array; + originIdToNodesMap: Map>; rendered: boolean; transform: { x: number, y: number, scale: number }; @@ -26,6 +27,7 @@ export class GraphPhase extends Phase { this.stateType = GraphStateType.NeedToFullRebuild; this.nodeLabelMap = nodeLabelMap ?? new Array(); this.nodeIdToNodeMap = nodeIdToNodeMap ?? new Array(); + this.originIdToNodesMap = new Map>(); this.rendered = false; } @@ -42,10 +44,10 @@ export class GraphPhase extends Phase { const jsonOrigin = node.origin; if (jsonOrigin) { if (jsonOrigin.nodeId) { - origin = new NodeOrigin(jsonOrigin.nodeId, jsonOrigin.reducer, jsonOrigin.phase); + origin = new NodeOrigin(jsonOrigin.nodeId, jsonOrigin.phase, jsonOrigin.reducer); } else { - origin = new BytecodeOrigin(jsonOrigin.bytecodePosition, jsonOrigin.reducer, - jsonOrigin.phase); + origin = new BytecodeOrigin(jsonOrigin.bytecodePosition, jsonOrigin.phase, + jsonOrigin.reducer); } } @@ -68,7 +70,14 @@ export class GraphPhase extends Phase { } const newNode = new GraphNode(label); this.data.nodes.push(newNode); - nodeIdToNodeMap[newNode.id] = newNode; + nodeIdToNodeMap[newNode.identifier()] = newNode; + if (origin) { + const identifier = origin.identifier(); + if (!this.originIdToNodesMap.has(identifier)) { + this.originIdToNodesMap.set(identifier, new Array()); + } + this.originIdToNodesMap.get(identifier).push(newNode); + } } return nodeIdToNodeMap; } diff --git a/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-block.ts b/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-block.ts index cb680db8a9..ea5a96fb95 100644 --- a/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-block.ts +++ b/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-block.ts @@ -2,6 +2,7 @@ // 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 { TurboshaftGraphNode } from "./turboshaft-graph-node"; import { Node } from "../../node"; import { TurboshaftGraphEdge } from "./turboshaft-graph-edge"; @@ -11,6 +12,9 @@ export class TurboshaftGraphBlock extends Node; nodes: Array; + showProperties: boolean; + width: number; + height: number; constructor(id: number, type: TurboshaftGraphBlockType, deferred: boolean, predecessors: Array) { @@ -23,14 +27,29 @@ export class TurboshaftGraphBlock extends Node((accumulator: number, node: TurboshaftGraphNode) => { - return accumulator + node.getHeight(showProperties); - }, this.labelBox.height); + if (this.showProperties != showProperties) { + this.height = this.nodes.reduce((accumulator: number, node: TurboshaftGraphNode) => { + return accumulator + node.getHeight(showProperties); + }, this.labelBox.height); + this.showProperties = showProperties; + } + return this.height; } public getWidth(): number { - const maxWidth = Math.max(...this.nodes.map((node: TurboshaftGraphNode) => node.getWidth())); - return Math.max(maxWidth, this.labelBox.width) + 50; + if (!this.width) { + const maxNodesWidth = Math.max(...this.nodes.map((node: TurboshaftGraphNode) => + node.getWidth())); + this.width = Math.max(maxNodesWidth, this.labelBox.width) + C.TURBOSHAFT_NODE_X_INDENT * 2; + } + return this.width; + } + + public hasBackEdges(): boolean { + return (this.type == TurboshaftGraphBlockType.Loop) || + (this.type == TurboshaftGraphBlockType.Merge && + this.inputs.length > 0 && + this.inputs[this.inputs.length - 1].source.type == TurboshaftGraphBlockType.Loop); } public toString(): string { diff --git a/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-edge.ts b/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-edge.ts index 1466437383..0f1b78c3d5 100644 --- a/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-edge.ts +++ b/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-edge.ts @@ -9,13 +9,15 @@ import { TurboshaftGraphBlock } from "./turboshaft-graph-block"; export class TurboshaftGraphEdge extends Edge { - constructor(target: Type, source: Type) { - super(target, source); + constructor(target: Type, index: number, source: Type) { + super(target, index, 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}`; + public isBackEdge(): boolean { + if (this.target instanceof TurboshaftGraphBlock) { + return this.target.hasBackEdges() && (this.target.rank < this.source.rank); + } + return this.target.rank < this.source.rank; } } diff --git a/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-node.ts b/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-node.ts index 129aeb2b7e..2fd4099b92 100644 --- a/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-node.ts +++ b/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-node.ts @@ -13,29 +13,45 @@ export class TurboshaftGraphNode extends Node 0) { + title += `\nInputs: ${this.inputs.map(i => i.source.id).join(", ")}`; + } + if (this.outputs.length > 0) { + title += `\nOutputs: ${this.outputs.map(i => i.target.id).join(", ")}`; + } + const opPropertiesStr = this.properties.length > 0 ? this.properties : "No op properties"; + return title + `\n${opPropertiesStr}`; } public getInlineLabel(): string { @@ -44,29 +60,11 @@ export class TurboshaftGraphNode extends Node propertiesWidth) return this.properties; - const widthOfOneSymbol = Math.floor(propertiesWidth / this.properties.length); + if (blockWidth > this.propertiesBox.width) return this.properties; + const widthOfOneSymbol = Math.floor(this.propertiesBox.width / 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 { diff --git a/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-phase.ts b/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-phase.ts index b4f6ec69e4..7726f00865 100644 --- a/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-phase.ts +++ b/tools/turbolizer/src/phases/turboshaft-graph-phase/turboshaft-graph-phase.ts @@ -8,7 +8,7 @@ import { TurboshaftGraphEdge } from "./turboshaft-graph-edge"; import { TurboshaftGraphBlock } from "./turboshaft-graph-block"; export class TurboshaftGraphPhase extends Phase { - highestBlockId: number; + highestBlockId: number; // TODO (danylo boiko) Delete this field data: TurboshaftGraphData; stateType: GraphStateType; layoutType: TurboshaftLayoutType; @@ -24,7 +24,6 @@ export class TurboshaftGraphPhase extends Phase { this.highestBlockId = highestBlockId; this.data = data ?? new TurboshaftGraphData(); this.stateType = GraphStateType.NeedToFullRebuild; - this.layoutType = TurboshaftLayoutType.Inline; this.nodeIdToNodeMap = nodeIdToNodeMap ?? new Array(); this.blockIdToBlockMap = blockIdToBlockMap ?? new Array(); this.rendered = false; @@ -42,12 +41,12 @@ export class TurboshaftGraphPhase extends Phase { const block = new TurboshaftGraphBlock(blockJson.id, blockJson.type, blockJson.deferred, blockJson.predecessors); this.data.blocks.push(block); - this.blockIdToBlockMap[block.id] = block; + this.blockIdToBlockMap[block.identifier()] = block; } for (const block of this.blockIdToBlockMap) { - for (const predecessor of block.predecessors) { + for (const [idx, predecessor] of block.predecessors.entries()) { const source = this.blockIdToBlockMap[predecessor]; - const edge = new TurboshaftGraphEdge(block, source); + const edge = new TurboshaftGraphEdge(block, idx, source); block.inputs.push(edge); source.outputs.push(edge); } @@ -61,7 +60,7 @@ export class TurboshaftGraphPhase extends Phase { block, nodeJson.op_properties_type, nodeJson.properties); block.nodes.push(node); this.data.nodes.push(node); - this.nodeIdToNodeMap[node.id] = node; + this.nodeIdToNodeMap[node.identifier()] = node; } } @@ -69,11 +68,14 @@ export class TurboshaftGraphPhase extends Phase { for (const edgeJson of edgesJson) { const target = this.nodeIdToNodeMap[edgeJson.target]; const source = this.nodeIdToNodeMap[edgeJson.source]; - const edge = new TurboshaftGraphEdge(target, source); + const edge = new TurboshaftGraphEdge(target, -1, source); this.data.edges.push(edge); target.inputs.push(edge); source.outputs.push(edge); } + for (const node of this.data.nodes) { + node.initDisplayLabel(); + } } } diff --git a/tools/turbolizer/src/selection/selection-broker.ts b/tools/turbolizer/src/selection/selection-broker.ts index 4067b102b2..4ebb45c09b 100644 --- a/tools/turbolizer/src/selection/selection-broker.ts +++ b/tools/turbolizer/src/selection/selection-broker.ts @@ -34,11 +34,21 @@ export class SelectionBroker { this.nodeHandlers.push(handler); } + public deleteNodeHandler(handler: NodeSelectionHandler & ClearableHandler): void { + this.allHandlers = this.allHandlers.filter(h => h != handler); + this.nodeHandlers = this.nodeHandlers.filter(h => h != handler); + } + public addBlockHandler(handler: BlockSelectionHandler & ClearableHandler): void { this.allHandlers.push(handler); this.blockHandlers.push(handler); } + public deleteBlockHandler(handler: BlockSelectionHandler & ClearableHandler): void { + this.allHandlers = this.allHandlers.filter(h => h != handler); + this.blockHandlers = this.blockHandlers.filter(h => h != handler); + } + public addInstructionHandler(handler: InstructionSelectionHandler & ClearableHandler): void { this.allHandlers.push(handler); this.instructionHandlers.push(handler); diff --git a/tools/turbolizer/src/source-resolver.ts b/tools/turbolizer/src/source-resolver.ts index bc7c33af14..66a6f1951d 100644 --- a/tools/turbolizer/src/source-resolver.ts +++ b/tools/turbolizer/src/source-resolver.ts @@ -46,6 +46,7 @@ export class SourceResolver { this.phases = new Array(); // Maps phase names to phaseIds. this.phaseNames = new Map(); + this.instructionsPhase = new InstructionsPhase(""); // The disassembly phase is stored separately. this.disassemblyPhase = undefined; // Maps line numbers to source positions @@ -155,22 +156,19 @@ export class SourceResolver { break; case PhaseType.Instructions: const castedInstructions = genericPhase as InstructionsPhase; - let instructionsPhase: InstructionsPhase = null; - if (this.instructionsPhase) { - instructionsPhase = this.instructionsPhase; - instructionsPhase.name += `, ${castedInstructions.name}`; + if (this.instructionsPhase.name === "") { + this.instructionsPhase.name = castedInstructions.name; } else { - instructionsPhase = new InstructionsPhase(castedInstructions.name); + this.instructionsPhase.name += `, ${castedInstructions.name}`; } - instructionsPhase.parseNodeIdToInstructionRangeFromJSON(castedInstructions + this.instructionsPhase.parseNodeIdToInstructionRangeFromJSON(castedInstructions ?.nodeIdToInstructionRange); - instructionsPhase.parseBlockIdToInstructionRangeFromJSON(castedInstructions + this.instructionsPhase.parseBlockIdToInstructionRangeFromJSON(castedInstructions ?.blockIdToInstructionRange); - instructionsPhase.parseInstructionOffsetToPCOffsetFromJSON(castedInstructions + this.instructionsPhase.parseInstructionOffsetToPCOffsetFromJSON(castedInstructions ?.instructionOffsetToPCOffset); - instructionsPhase.parseCodeOffsetsInfoFromJSON(castedInstructions + this.instructionsPhase.parseCodeOffsetsInfoFromJSON(castedInstructions ?.codeOffsetsInfo); - this.instructionsPhase = instructionsPhase; break; case PhaseType.Graph: const castedGraph = genericPhase as GraphPhase; diff --git a/tools/turbolizer/src/turboshaft-graph-layout.ts b/tools/turbolizer/src/turboshaft-graph-layout.ts index 80eca90123..844c333384 100644 --- a/tools/turbolizer/src/turboshaft-graph-layout.ts +++ b/tools/turbolizer/src/turboshaft-graph-layout.ts @@ -1,40 +1,251 @@ +// 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 { TurboshaftGraph } from "./turboshaft-graph"; import { GraphStateType } from "./phases/phase"; -import { TurboshaftLayoutType } from "./phases/turboshaft-graph-phase/turboshaft-graph-phase"; +import { + TurboshaftGraphBlock, + TurboshaftGraphBlockType +} from "./phases/turboshaft-graph-phase/turboshaft-graph-block"; +import { LayoutOccupation } from "./layout-occupation"; export class TurboshaftGraphLayout { graph: TurboshaftGraph; + layoutOccupation: LayoutOccupation; + startTime: number; + maxRank: number; + visitOrderWithinRank: number; constructor(graph: TurboshaftGraph) { this.graph = graph; + this.layoutOccupation = new LayoutOccupation(graph); + this.maxRank = 0; + this.visitOrderWithinRank = 0; } 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; + switch (this.graph.graphPhase.stateType) { + case GraphStateType.NeedToFullRebuild: + this.fullRebuild(showProperties); + break; + case GraphStateType.Cached: + this.cachedRebuild(); + break; + default: + throw "Unsupported graph state type"; } 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; + private fullRebuild(showProperties: boolean): void { + this.startTime = performance.now(); + this.maxRank = 0; + this.visitOrderWithinRank = 0; + + const blocks = this.initBlocks(); + this.initWorkList(blocks); + + let visited = new Array(); + blocks.forEach((block: TurboshaftGraphBlock) => this.dfsFindRankLate(visited, block)); + visited = new Array(); + blocks.forEach((block: TurboshaftGraphBlock) => this.dfsRankOrder(visited, block)); + + const rankSets = this.getRankSets(showProperties); + this.placeBlocks(rankSets, showProperties); + this.calculateBackEdgeNumbers(); + this.graph.graphPhase.stateType = GraphStateType.Cached; + } + + private cachedRebuild(): void { + this.calculateBackEdgeNumbers(); + } + + private initBlocks(): Array { + // First determine the set of blocks that have no inputs. Those are the + // basis for top-down DFS to determine rank and block placement. + const blocksHasNoInputs = new Array(); + for (const block of this.graph.blocks()) { + blocksHasNoInputs[block.id] = true; + } + for (const edge of this.graph.blocksEdges()) { + blocksHasNoInputs[edge.target.id] = false; + } + + // Finialize the list of blocks. + const blocks = new Array(); + const visited = new Array(); + for (const block of this.graph.blocks()) { + if (blocksHasNoInputs[block.id]) { + blocks.push(block); + } + visited[block.id] = false; + block.rank = 0; + block.visitOrderWithinRank = 0; + block.outputApproach = C.MINIMUM_NODE_OUTPUT_APPROACH; + } + this.trace("layoutGraph init"); + return blocks; + } + + private initWorkList(blocks: Array): void { + const workList: Array = blocks.slice(); + while (workList.length != 0) { + const block: TurboshaftGraphBlock = workList.pop(); + let changed = false; + if (block.rank == C.MAX_RANK_SENTINEL) { + block.rank = 1; + changed = true; + } + let begin = 0; + let end = block.inputs.length; + if (block.type == TurboshaftGraphBlockType.Merge && block.inputs.length > 0) { + begin = block.inputs.length - 1; + } else if (block.hasBackEdges()) { + end = 1; + } + for (let l = begin; l < end; ++l) { + const input = block.inputs[l].source; + if (input.visible && input.rank >= block.rank) { + block.rank = input.rank + 1; + changed = true; + } + } + if (changed) { + const hasBackEdges = block.hasBackEdges(); + for (let l = block.outputs.length - 1; l >= 0; --l) { + if (hasBackEdges && (l != 0)) { + workList.unshift(block.outputs[l].target); + } else { + workList.push(block.outputs[l].target); + } + } + } + this.maxRank = Math.max(block.rank, this.maxRank); + this.trace("layoutGraph work list"); + } + } + + private dfsFindRankLate(visited: Array, block: TurboshaftGraphBlock): void { + if (visited[block.id]) return; + visited[block.id] = true; + const originalRank = block.rank; + let newRank = block.rank; + let isFirstInput = true; + for (const outputEdge of block.outputs) { + const output = outputEdge.target; + this.dfsFindRankLate(visited, output); + const outputRank = output.rank; + if (output.visible && (isFirstInput || outputRank <= newRank) && + (outputRank > originalRank)) { + newRank = outputRank - 1; + } + isFirstInput = false; + } + if (block.hasBackEdges()) { + block.rank = newRank; + } + } + + private dfsRankOrder(visited: Array, block: TurboshaftGraphBlock): void { + if (visited[block.id]) return; + visited[block.id] = true; + for (const outputEdge of block.outputs) { + if (outputEdge.isVisible()) { + const output = outputEdge.target; + this.dfsRankOrder(visited, output); + } + } + if (block.visitOrderWithinRank == 0) { + block.visitOrderWithinRank = ++this.visitOrderWithinRank; + } + } + + private getRankSets(showProperties: boolean): Array> { + const rankMaxBlockHeight = new Array(); + for (const block of this.graph.blocks()) { + rankMaxBlockHeight[block.rank] = Math.max(rankMaxBlockHeight[block.rank] ?? 0, + block.getHeight(showProperties)); + } + + const rankSets = new Array>(); + for (const block of this.graph.blocks()) { + block.y = rankMaxBlockHeight.slice(1, block.rank).reduce((accumulator, current) => { + return accumulator + current; + }, block.rank * (C.TURBOSHAFT_BLOCK_ROW_SEPARATION + 2 * C.DEFAULT_NODE_BUBBLE_RADIUS)); + if (block.visible) { + if (!rankSets[block.rank]) { + rankSets[block.rank] = new Array(block); + } else { + rankSets[block.rank].push(block); + } + } + } + return rankSets; + } + + private placeBlocks(rankSets: Array>, showProperties: boolean): void { + // Iterate backwards from highest to lowest rank, placing blocks so that they + // spread out from the "center" as much as possible while still being + // compact and not overlapping live input lines. + rankSets.reverse().forEach((rankSet: Array) => { + for (const block of rankSet) { + this.layoutOccupation.clearOutputs(block, showProperties); + } + + this.traceOccupation("After clearing outputs"); + + let placedCount = 0; + rankSet = rankSet.sort((a: TurboshaftGraphBlock, b: TurboshaftGraphBlock) => a.compare(b)); + for (const block of rankSet) { + if (block.visible) { + block.x = this.layoutOccupation.occupy(block); + const blockWidth = block.getWidth(); + this.trace(`Block ${block.id} is placed between [${block.x}, ${block.x + blockWidth})`); + const staggeredFlooredI = Math.floor(placedCount++ % 3); + const delta = C.MINIMUM_EDGE_SEPARATION * staggeredFlooredI; + block.outputApproach += delta; + } else { + block.x = 0; + } + } + + this.traceOccupation("Before clearing blocks"); + + this.layoutOccupation.clearOccupied(); + + this.traceOccupation("After clearing blocks"); + + for (const block of rankSet) { + this.layoutOccupation.occupyInputs(block, showProperties); + } + + this.traceOccupation("After occupying inputs and determining bounding box"); + }); + } + + private calculateBackEdgeNumbers(): void { + this.graph.maxBackEdgeNumber = 0; + for (const edge of this.graph.blocksEdges()) { + if (edge.isBackEdge()) { + edge.backEdgeNumber = ++this.graph.maxBackEdgeNumber; + } else { + edge.backEdgeNumber = 0; } } } + + private trace(message: string): void { + if (C.TRACE_LAYOUT) { + console.log(`${message} ${performance.now() - this.startTime}`); + } + } + + private traceOccupation(message: string): void { + if (C.TRACE_LAYOUT) { + console.log(message); + this.layoutOccupation.print(); + } + } } diff --git a/tools/turbolizer/src/turboshaft-graph.ts b/tools/turbolizer/src/turboshaft-graph.ts index 86ef140602..0a5d7d397c 100644 --- a/tools/turbolizer/src/turboshaft-graph.ts +++ b/tools/turbolizer/src/turboshaft-graph.ts @@ -37,7 +37,17 @@ export class TurboshaftGraph extends MovableContainer { for (const block of this.blockMap) { if (!block) continue; for (const edge of block.inputs) { - if (!edge || func(edge)) continue; + if (!edge || !func(edge)) continue; + yield edge; + } + } + } + + public *nodesEdges(func = (e: TurboshaftGraphEdge) => true) { + for (const block of this.nodeMap) { + if (!block) continue; + for (const edge of block.inputs) { + if (!edge || !func(edge)) continue; yield edge; } } @@ -61,7 +71,7 @@ export class TurboshaftGraph extends MovableContainer { + C.NODE_INPUT_WIDTH); } - this.maxGraphX = this.maxGraphNodeX + 3 * C.MINIMUM_EDGE_SEPARATION; + this.maxGraphX = this.maxGraphNodeX + this.maxBackEdgeNumber * C.MINIMUM_EDGE_SEPARATION; this.width = this.maxGraphX - this.minGraphX; this.height = this.maxGraphY - this.minGraphY; diff --git a/tools/turbolizer/src/views/code-view.ts b/tools/turbolizer/src/views/code-view.ts index a3d0638982..595b52e7e4 100644 --- a/tools/turbolizer/src/views/code-view.ts +++ b/tools/turbolizer/src/views/code-view.ts @@ -3,6 +3,12 @@ // found in the LICENSE file. import { Source } from "../source"; +import { GenericPosition, SourceResolver } from "../source-resolver"; +import { SelectionBroker } from "../selection/selection-broker"; +import { View } from "./view"; +import { SelectionMap } from "../selection/selection"; +import { ViewElements } from "../common/view-elements"; +import { SelectionHandler } from "../selection/selection-handler"; interface PR { prettyPrint(_: unknown, el: HTMLElement): void; @@ -12,13 +18,6 @@ declare global { const PR: PR; } -import { GenericPosition, SourceResolver } from "../source-resolver"; -import { SelectionBroker } from "../selection/selection-broker"; -import { View } from "./view"; -import { SelectionMap } from "../selection/selection"; -import { ViewElements } from "../common/view-elements"; -import { SelectionHandler } from "../selection/selection-handler"; - export enum CodeMode { MAIN_SOURCE = "main function", INLINED_SOURCE = "inlined function" @@ -81,7 +80,7 @@ export class CodeView extends View { view.updateSelection(); }, }; - view.selection = new SelectionMap((sp: GenericPosition) => sp.toString()); + view.selection = new SelectionMap((gp: GenericPosition) => gp.toString()); broker.addSourcePositionHandler(selectionHandler); this.selectionHandler = selectionHandler; this.initializeCode(); @@ -280,5 +279,4 @@ export class CodeView extends View { view.addHtmlElementToSourcePosition(sourcePosition, lineElement); } } - } diff --git a/tools/turbolizer/src/views/graph-view.ts b/tools/turbolizer/src/views/graph-view.ts index eacf1f78d0..a2a01c8065 100644 --- a/tools/turbolizer/src/views/graph-view.ts +++ b/tools/turbolizer/src/views/graph-view.ts @@ -13,7 +13,7 @@ import { GraphEdge } from "../phases/graph-phase/graph-edge"; import { GraphLayout } from "../graph-layout"; import { GraphData, GraphPhase, GraphStateType } from "../phases/graph-phase/graph-phase"; import { BytecodePosition } from "../position"; -import { BytecodeOrigin } from "../origin"; +import { BytecodeOrigin, NodeOrigin } from "../origin"; import { MovableView } from "./movable-view"; import { ClearableHandler, NodeSelectionHandler } from "../selection/selection-handler"; import { GenericPosition } from "../source-resolver"; @@ -69,10 +69,8 @@ export class GraphView extends MovableView { 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); + const adaptedSelection = this.createGraph(data, rememberedSelection); this.broker.addNodeHandler(this.nodesSelectionHandler); const selectedNodes = adaptedSelection?.size > 0 @@ -162,8 +160,8 @@ export class GraphView extends MovableView { 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("g"); + const adjInputNodes = adjInputEdges.data().map(edge => edge.source); visibleNodes.data(adjInputNodes, node => node.toString()) .attr("relToHover", "input"); const adjOutputNodes = adjOutputEdges.data().map(edge => edge.target); @@ -238,7 +236,7 @@ export class GraphView extends MovableView { view.updateInputAndOutputBubbles(); graph.maxGraphX = graph.maxGraphNodeX; - newAndOldEdges.attr("d", node => node.generatePath(graph, view.state.showTypes)); + newAndOldEdges.attr("d", edge => edge.generatePath(graph, view.state.showTypes)); } public svgKeyDown(): void { @@ -365,11 +363,6 @@ export class GraphView extends MovableView { private initializeNodesSelectionHandler(): NodeSelectionHandler & ClearableHandler { const view = this; return { - clear: function () { - view.state.selection.clear(); - view.broker.broadcastClear(this); - view.updateGraphVisibility(); - }, select: function (selectedNodes: Array, selected: boolean) { const locations = new Array(); for (const node of selectedNodes) { @@ -384,22 +377,27 @@ export class GraphView extends MovableView { view.broker.broadcastSourcePositionSelect(this, locations, selected); view.updateGraphVisibility(); }, + clear: function () { + view.state.selection.clear(); + view.broker.broadcastClear(this); + 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); - }); - } + for (const item of view.state.selection.selectedKeys()) { + const node = view.graph.nodeMap[item]; + if (!node) continue; + 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); + }); } view.updateGraphVisibility(); }, @@ -410,7 +408,7 @@ export class GraphView extends MovableView { }; } - private createGraph(data: GraphPhase, selection): void { + private createGraph(data: GraphPhase, rememberedSelection: Map): Set { this.graph = new Graph(data); this.graphLayout = new GraphLayout(this.graph); @@ -422,8 +420,10 @@ export class GraphView extends MovableView { this.showVisible(); } - if (selection !== undefined) { - for (const item of selection) { + const adaptedSelection = this.adaptSelectionToCurrentPhase(data.data, rememberedSelection); + + if (adaptedSelection !== undefined) { + for (const item of adaptedSelection) { if (this.graph.nodeMap[item]) this.graph.nodeMap[item].visible = true; } } @@ -432,6 +432,7 @@ export class GraphView extends MovableView { this.layoutGraph(); this.updateGraphVisibility(); + return adaptedSelection; } private layoutGraph(): void { @@ -530,31 +531,28 @@ export class GraphView extends MovableView { private adaptSelectionToCurrentPhase(data: GraphData, selection: Map): Set { - // TODO (danylo boiko) Speed up adapting - const updatedGraphSelection = new Set(); - if (!data || !(selection instanceof Map)) return updatedGraphSelection; - // Adding survived nodes (with the same id) - for (const node of data.nodes) { - const stringKey = this.state.selection.stringKey(node); - if (selection.has(stringKey)) { - updatedGraphSelection.add(stringKey); + const updatedSelection = new Set(); + if (!data || !(selection instanceof Map)) return updatedSelection; + for (const [key, node] of selection.entries()) { + // Adding survived nodes (with the same id) + const survivedNode = this.graph.nodeMap[key]; + if (survivedNode) { + updatedSelection.add(this.state.selection.stringKey(survivedNode)); } - } - // Adding children of nodes - for (const node of data.nodes) { + // Adding children of nodes + const childNodes = this.graph.originNodesMap.get(key); + if (childNodes?.length > 0) { + for (const childNode of childNodes) { + updatedSelection.add(this.state.selection.stringKey(childNode)); + } + } + // Adding ancestors of nodes const originStringKey = this.state.selection.originStringKey(node); - if (originStringKey && selection.has(originStringKey)) { - updatedGraphSelection.add(this.state.selection.stringKey(node)); + if (originStringKey) { + updatedSelection.add(originStringKey); } } - // Adding ancestors of nodes - selection.forEach(selectedNode => { - const originStringKey = this.state.selection.originStringKey(selectedNode); - if (originStringKey) { - updatedGraphSelection.add(originStringKey); - } - }); - return updatedGraphSelection; + return updatedSelection; } private attachSelection(selection: Set): Array { @@ -703,27 +701,26 @@ export class GraphView extends MovableView { this.updateGraphVisibility(); } - private selectOrigins() { - const state = this.state; - // TODO (danylo boiko) Add array type - const origins = []; + private selectOrigins(): void { + const origins = new Array(); let phase = this.phaseName; - const selection = new Set(); - for (const node of state.selection) { + const selection = new Set(); + for (const node of this.state.selection) { const origin = node.nodeLabel.origin; - if (origin) { + if (origin && origin instanceof NodeOrigin) { phase = origin.phase; - const node = this.graph.nodeMap[origin.nodeId]; - if (node && phase === this.phaseName) { + const node = this.graph.nodeMap[origin.identifier()]; + if (phase === this.phaseName && node) { origins.push(node); } else { - selection.add(`${origin.nodeId}`); + selection.add(origin.identifier()); } } } // Only go through phase reselection if we actually need // to display another phase. if (selection.size > 0 && phase !== this.phaseName) { + this.hide(); this.showPhaseByName(phase, selection); } else if (origins.length > 0) { this.nodesSelectionHandler.clear(); diff --git a/tools/turbolizer/src/views/movable-view.ts b/tools/turbolizer/src/views/movable-view.ts index d2faac0359..865767c1c1 100644 --- a/tools/turbolizer/src/views/movable-view.ts +++ b/tools/turbolizer/src/views/movable-view.ts @@ -14,6 +14,7 @@ import { Edge } from "../edge"; import { Node } from "../node"; import { TurboshaftGraph } from "../turboshaft-graph"; import { Graph } from "../graph"; +import { TurboshaftLayoutType } from "../phases/turboshaft-graph-phase/turboshaft-graph-phase"; export abstract class MovableView extends PhaseView { phaseName: string; @@ -102,6 +103,7 @@ export abstract class MovableView ext } else { this.graph.graphPhase.transform = null; } + this.broker.deleteNodeHandler(this.nodesSelectionHandler); super.hide(); this.deleteContent(); } @@ -270,6 +272,7 @@ export abstract class MovableView ext export class MovableViewState { public selection: SelectionMap; + public blocksSelection: SelectionMap; public get hideDead(): boolean { return storageGetItem("toggle-hide-dead", false); @@ -302,4 +305,12 @@ export class MovableViewState { public set cacheLayout(value: boolean) { storageSetItem("toggle-cache-layout", value); } + + public get turboshaftLayoutType() { + return storageGetItem("turboshaft-layout-type", TurboshaftLayoutType.Inline); + } + + public set turboshaftLayoutType(layoutType: TurboshaftLayoutType) { + storageSetItem("turboshaft-layout-type", layoutType); + } } diff --git a/tools/turbolizer/src/views/turboshaft-graph-view.ts b/tools/turbolizer/src/views/turboshaft-graph-view.ts index fd7eae7d20..dec2248e04 100644 --- a/tools/turbolizer/src/views/turboshaft-graph-view.ts +++ b/tools/turbolizer/src/views/turboshaft-graph-view.ts @@ -2,21 +2,31 @@ // 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 * as d3 from "d3"; 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"; +import { + BlockSelectionHandler, + ClearableHandler, + NodeSelectionHandler +} from "../selection/selection-handler"; +import { + TurboshaftGraphPhase, + TurboshaftLayoutType +} from "../phases/turboshaft-graph-phase/turboshaft-graph-phase"; +import { + TurboshaftGraphBlock, + TurboshaftGraphBlockType +} from "../phases/turboshaft-graph-phase/turboshaft-graph-block"; export class TurboshaftGraphView extends MovableView { graphLayout: TurboshaftGraphLayout; @@ -31,17 +41,26 @@ export class TurboshaftGraphView extends MovableView { showPhaseByName: (name: string) => void, toolbox: HTMLElement) { super(idOrContainer, broker, showPhaseByName, toolbox); - this.state.selection = new SelectionMap((b: TurboshaftGraphBlock) => b.identifier()); + this.state.selection = new SelectionMap(node => node.identifier()); + this.state.blocksSelection = new SelectionMap(blockId => String(blockId)); + + this.nodesSelectionHandler = this.initializeNodesSelectionHandler(); + this.blocksSelectionHandler = this.initializeBlocksSelectionHandler(); + + this.svg.on("click", () => { + this.nodesSelectionHandler.clear(); + this.blocksSelectionHandler.clear(); + }); - this.visibleBlocks = this.graphElement.append("g"); this.visibleEdges = this.graphElement.append("g"); + this.visibleBlocks = this.graphElement.append("g"); this.visibleNodes = this.graphElement.append("g"); this.blockDrag = d3.drag() .on("drag", (block: TurboshaftGraphBlock) => { block.x += d3.event.dx; block.y += d3.event.dy; - this.updateVisibleBlocks(); + this.updateBlockLocation(block); }); } @@ -52,13 +71,16 @@ export class TurboshaftGraphView extends MovableView { partial(this.layoutAction, this)); this.addImgInput("show-all", "show all blocks", partial(this.showAllBlocksAction, this)); - this.addToggleImgInput("toggle-properties", "toggle propetries", + this.addToggleImgInput("toggle-properties", "toggle properties", this.state.showProperties, partial(this.togglePropertiesAction, this)); this.addToggleImgInput("toggle-cache-layout", "toggle saving graph layout", this.state.cacheLayout, partial(this.toggleLayoutCachingAction, this)); + this.addLayoutTypeSelect(); this.phaseName = data.name; this.createGraph(data, rememberedSelection); + this.broker.addNodeHandler(this.nodesSelectionHandler); + this.broker.addBlockHandler(this.blocksSelectionHandler); this.viewWholeGraph(); if (this.state.cacheLayout && data.transform) { @@ -70,8 +92,8 @@ export class TurboshaftGraphView extends MovableView { public updateGraphVisibility(): void { if (!this.graph) return; - this.updateVisibleBlocks(); - this.visibleNodes = d3.selectAll(".turboshaft-node"); + this.updateVisibleBlocksAndEdges(); + this.visibleNodes = this.visibleBlocks.selectAll(".turboshaft-node"); this.visibleBubbles = d3.selectAll("circle"); this.updateInlineNodes(); this.updateInputAndOutputBubbles(); @@ -83,6 +105,78 @@ export class TurboshaftGraphView extends MovableView { public searchInputAction(searchInput: HTMLInputElement, e: Event, onlyVisible: boolean): void { } + public hide() { + this.broker.deleteBlockHandler(this.blocksSelectionHandler); + super.hide(); + } + + private initializeNodesSelectionHandler(): NodeSelectionHandler & ClearableHandler { + const view = this; + return { + select: function (selectedNodes: Array, selected: boolean) { + view.state.selection.select(selectedNodes, selected); + view.updateGraphVisibility(); + }, + clear: function () { + view.state.selection.clear(); + view.broker.broadcastClear(this); + view.updateGraphVisibility(); + }, + brokeredNodeSelect: function (nodeIds: Set, selected: boolean) { + const selection = view.graph.nodes(node => nodeIds.has(node.identifier())); + view.state.selection.select(selection, selected); + view.updateGraphVisibility(); + }, + brokeredClear: function () { + view.state.selection.clear(); + view.updateGraphVisibility(); + } + }; + } + + private initializeBlocksSelectionHandler(): BlockSelectionHandler & ClearableHandler { + const view = this; + return { + select: function (selectedBlocks: Array, selected: boolean) { + view.state.blocksSelection.select(selectedBlocks, selected); + view.broker.broadcastBlockSelect(this, selectedBlocks, selected); + view.updateGraphVisibility(); + }, + clear: function () { + view.state.blocksSelection.clear(); + view.broker.broadcastClear(this); + view.updateGraphVisibility(); + }, + brokeredBlockSelect: function (blockIds: Array, selected: boolean) { + view.state.blocksSelection.select(blockIds, selected); + view.updateGraphVisibility(); + }, + brokeredClear: function () { + view.state.blocksSelection.clear(); + view.updateGraphVisibility(); + }, + }; + } + + private addLayoutTypeSelect(): void { + const view = this; + const select = document.createElement("select") as HTMLSelectElement; + select.id = "layout-type-select"; + select.className = "graph-toolbox-item"; + const keys = Object.keys(TurboshaftLayoutType).filter(t => isNaN(Number(t))); + for (const key of keys) { + const option = document.createElement("option"); + option.text = key; + select.add(option); + } + select.selectedIndex = this.state.turboshaftLayoutType; + select.onchange = function (this: HTMLSelectElement) { + view.state.turboshaftLayoutType = this.selectedIndex as TurboshaftLayoutType; + view.layoutAction(view); + }; + this.toolbox.appendChild(select); + } + private createGraph(data: TurboshaftGraphPhase, selection) { this.graph = new TurboshaftGraph(data); this.graphLayout = new TurboshaftGraphLayout(this.graph); @@ -101,10 +195,11 @@ export class TurboshaftGraphView extends MovableView { private layoutGraph(): void { const layoutMessage = this.graph.graphPhase.stateType == GraphStateType.Cached - ? "Layout graph from cache" - : "Layout graph"; + ? "Layout turboshaft graph from cache" + : "Layout turboshaft graph"; console.time(layoutMessage); + this.graph.graphPhase.layoutType = this.state.turboshaftLayoutType; this.graphLayout.rebuild(this.state.showProperties); const extent = this.graph.redetermineGraphBoundingBox(this.state.showProperties); this.panZoom.translateExtent(extent); @@ -112,8 +207,56 @@ export class TurboshaftGraphView extends MovableView { console.timeEnd(layoutMessage); } - private updateVisibleBlocks(): void { + private updateBlockLocation(block: TurboshaftGraphBlock): void { + this.visibleBlocks + .selectAll(".turboshaft-block") + .filter(b => b == block) + .attr("transform", block => `translate(${block.x},${block.y})`); + + this.visibleEdges + .selectAll>("path") + .filter(edge => edge.target === block || edge.source === block) + .attr("d", edge => edge.generatePath(this.graph, this.state.showProperties)); + } + + private updateVisibleBlocksAndEdges(): void { const view = this; + + // select existing edges + const filteredEdges = [ + ...this.graph.blocksEdges(edge => this.graph.isRendered() + && edge.source.visible && edge.target.visible) + ]; + + const selEdges = view.visibleEdges + .selectAll>("path") + .data(filteredEdges, edge => edge.toString()); + + // remove old edges + selEdges.exit().remove(); + + // add new edges + 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.blocksSelectionHandler.clear(); + } + view.blocksSelectionHandler.select( + [edge.source.identifier(), edge.target.identifier()], + true + ); + }) + .attr("adjacentToHover", "false"); + + const newAndOldEdges = newEdges.merge(selEdges); + + newAndOldEdges.classed("hidden", edge => !edge.isVisible()); + // select existing blocks const filteredBlocks = [ ...this.graph.blocks(block => this.graph.isRendered() && block.visible) @@ -133,12 +276,34 @@ export class TurboshaftGraphView extends MovableView { .classed("block", b => b.type == TurboshaftGraphBlockType.Block) .classed("merge", b => b.type == TurboshaftGraphBlockType.Merge) .classed("loop", b => b.type == TurboshaftGraphBlockType.Loop) + .on("mouseenter", (block: TurboshaftGraphBlock) => { + const visibleEdges = view.visibleEdges + .selectAll>("path"); + const adjInputEdges = visibleEdges.filter(edge => edge.target === block); + const adjOutputEdges = visibleEdges.filter(edge => edge.source === block); + adjInputEdges.classed("input", true); + adjOutputEdges.classed("output", true); + view.updateGraphVisibility(); + }) + .on("mouseleave", (block: TurboshaftGraphBlock) => { + const visibleEdges = view.visibleEdges + .selectAll>("path"); + const adjEdges = visibleEdges + .filter(edge => edge.target === block || edge.source === block); + adjEdges.classed("input output", false); + view.updateGraphVisibility(); + }) + .on("click", (block: TurboshaftGraphBlock) => { + if (!d3.event.shiftKey) view.blocksSelectionHandler.clear(); + view.blocksSelectionHandler.select([block.identifier()], undefined); + d3.event.stopPropagation(); + }) .call(view.blockDrag); newBlocks .append("rect") - .attr("rx", 35) - .attr("ry", 35) + .attr("rx", C.TURBOSHAFT_BLOCK_BORDER_RADIUS) + .attr("ry", C.TURBOSHAFT_BLOCK_BORDER_RADIUS) .attr("width", block => block.getWidth()) .attr("height", block => block.getHeight(view.state.showProperties)); @@ -156,10 +321,12 @@ export class TurboshaftGraphView extends MovableView { }); newBlocks.merge(selBlocks) - .classed("selected", block => view.state.selection.isSelected(block)) + .classed("selected", block => view.state.blocksSelection.isSelected(block.identifier())) .attr("transform", block => `translate(${block.x},${block.y})`) .select("rect") .attr("height", block => block.getHeight(view.state.showProperties)); + + newAndOldEdges.attr("d", edge => edge.generatePath(this.graph, view.state.showProperties)); } private appendInlineNodes(svg: d3.Selection, @@ -177,33 +344,57 @@ export class TurboshaftGraphView extends MovableView { const newNodes = selNodes .enter() .append("g") - .classed("turboshaft-node", true) - .classed("inline-node", true); + .classed("turboshaft-node inline-node", true); let nodeY = block.labelBox.height; const blockWidth = block.getWidth(); + const view = this; newNodes.each(function (node: TurboshaftGraphNode) { const nodeSvg = d3.select(this); nodeSvg .attr("id", node.id) .append("text") - .attr("dx", 25) + .attr("dx", C.TURBOSHAFT_NODE_X_INDENT) .classed("inline-node-label", true) .attr("dy", nodeY) .append("tspan") - .text(`${node.getInlineLabel()}[${node.getPropertiesTypeAbbreviation()}]`); + .text(node.displayLabel) + .append("title") + .text(node.getTitle()); + nodeSvg + .on("mouseenter", (node: TurboshaftGraphNode) => { + view.visibleNodes.data( + node.inputs.map(edge => edge.source), source => source.toString()) + .classed("input", true); + view.visibleNodes.data( + node.outputs.map(edge => edge.target), target => target.toString()) + .classed("output", true); + view.updateGraphVisibility(); + }) + .on("mouseleave", (node: TurboshaftGraphNode) => { + const inOutNodes = node.inputs.map(edge => edge.source) + .concat(node.outputs.map(edge => edge.target)); + view.visibleNodes.data(inOutNodes, inOut => inOut.toString()) + .classed("input output", false); + view.updateGraphVisibility(); + }) + .on("click", (node: TurboshaftGraphNode) => { + if (!d3.event.shiftKey) view.nodesSelectionHandler.clear(); + view.nodesSelectionHandler.select([node], undefined); + d3.event.stopPropagation(); + }); nodeY += node.labelBox.height; if (node.properties) { nodeSvg .append("text") - .attr("dx", 25) + .attr("dx", C.TURBOSHAFT_NODE_X_INDENT) .classed("inline-node-properties", true) .attr("dy", nodeY) .append("tspan") .text(node.getReadableProperties(blockWidth)) .append("title") .text(node.properties); - nodeY += node.labelBox.height; + nodeY += node.propertiesBox.height; } }); @@ -256,7 +447,7 @@ export class TurboshaftGraphView extends MovableView { svg.append("circle") .classed("filledBubbleStyle", block.inputs[i].isVisible()) .classed("bubbleStyle", !block.inputs[i].isVisible()) - .attr("id", `ib,${block.inputs[i].toString(i)}`) + .attr("id", `ib,${block.inputs[i].toString()}`) .attr("r", C.DEFAULT_NODE_BUBBLE_RADIUS) .attr("transform", `translate(${x},${y})`); } @@ -290,10 +481,13 @@ export class TurboshaftGraphView extends MovableView { 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"); + nodeSvg + .select(".inline-node-label") + .classed("selected", node => state.selection.isSelected(node)) + .attr("dy", nodeY); + nodeSvg + .select(".inline-node-properties") + .attr("visibility", state.showProperties ? "visible" : "hidden"); }); } diff --git a/tools/turbolizer/tsconfig.json b/tools/turbolizer/tsconfig.json index a01a55fd70..5bd4334062 100644 --- a/tools/turbolizer/tsconfig.json +++ b/tools/turbolizer/tsconfig.json @@ -41,6 +41,7 @@ "src/views/info-view.ts", "src/views/movable-view.ts", "src/views/turboshaft-graph-view.ts", + "src/layout-occupation.ts", "src/origin.ts", "src/movable-container.ts", "src/turboshaft-graph.ts",