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

[turbolizer] Graph layout caching

- "Remember graph layout" button
- Graph layout caching (almost 10x speed up)
- Camera position and zoom saving
- Refactored graph.ts, graph-layout.ts and graphmultiview.ts

Bug: v8:7327
Change-Id: I6a9db1ddbbaf506bff0b9d1c1e015f245c7c3974
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3714248
Commit-Queue: Danylo Boiko <danielboyko02@gmail.com>
Reviewed-by: 's avatarNico Hartmann <nicohartmann@chromium.org>
Cr-Commit-Position: refs/heads/main@{#81309}
parent c3f60e8c
This diff was suppressed by a .gitattributes entry.
......@@ -12,6 +12,7 @@ export const MINIMUM_EDGE_SEPARATION = 20;
export const MINIMUM_NODE_INPUT_APPROACH = 15 + 2 * DEFAULT_NODE_BUBBLE_RADIUS;
export const DEFAULT_NODE_ROW_SEPARATION = 150;
export const TRACE_LAYOUT = false;
export const MULTIVIEW_ID = "multiview";
export const SOURCE_PANE_ID = "left";
export const SOURCE_COLLAPSE_ID = "source-shrink";
export const SOURCE_EXPAND_ID = "source-expand";
......
......@@ -78,3 +78,13 @@ export function createElement(tag: string, cls: string, content?: string): HTMLE
if (content !== undefined) el.innerText = content;
return el;
}
export function storageGetItem(key: string, defaultValue?: any, parse: boolean = true): any {
let value = window.sessionStorage.getItem(key);
if (parse) value = JSON.parse(value);
return value === null ? defaultValue : value;
}
export function storageSetItem(key: string, value: any): void {
window.sessionStorage.setItem(key, value);
}
This diff is collapsed.
......@@ -3,11 +3,12 @@
// found in the LICENSE file.
import * as C from "./common/constants";
import { GraphPhase } from "./phases/graph-phase";
import { GraphPhase, GraphStateType } from "./phases/graph-phase";
import { GraphEdge } from "./phases/graph-phase/graph-edge";
import { GraphNode } from "./phases/graph-phase/graph-node";
export class Graph {
graphPhase: GraphPhase;
nodeMap: Array<GraphNode>;
minGraphX: number;
maxGraphX: number;
......@@ -19,57 +20,41 @@ export class Graph {
height: number;
constructor(graphPhase: GraphPhase) {
this.nodeMap = [];
this.graphPhase = graphPhase;
this.nodeMap = graphPhase.nodeIdToNodeMap;
this.minGraphX = 0;
this.maxGraphX = 1;
this.minGraphY = 0;
this.maxGraphY = 1;
this.width = 1;
this.height = 1;
graphPhase.data.nodes.forEach((jsonNode: GraphNode) => {
this.nodeMap[jsonNode.id] = new GraphNode(jsonNode.nodeLabel);
});
graphPhase.data.edges.forEach((e: any) => {
const t = this.nodeMap[e.target.id];
const s = this.nodeMap[e.source.id];
const newEdge = new GraphEdge(t, e.index, s, e.type);
t.inputs.push(newEdge);
s.outputs.push(newEdge);
if (e.type == 'control') {
// Every source of a control edge is a CFG node.
s.cfg = true;
}
});
}
*nodes(p = (n: GraphNode) => true) {
public *nodes(func = (n: GraphNode) => true) {
for (const node of this.nodeMap) {
if (!node || !p(node)) continue;
if (!node || !func(node)) continue;
yield node;
}
}
*filteredEdges(p: (e: GraphEdge) => boolean) {
public *filteredEdges(func: (e: GraphEdge) => boolean) {
for (const node of this.nodes()) {
for (const edge of node.inputs) {
if (p(edge)) yield edge;
if (func(edge)) yield edge;
}
}
}
forEachEdge(p: (e: GraphEdge) => void) {
public forEachEdge(func: (e: GraphEdge) => void) {
for (const node of this.nodeMap) {
if (!node) continue;
for (const edge of node.inputs) {
p(edge);
func(edge);
}
}
}
redetermineGraphBoundingBox(showTypes: boolean): [[number, number], [number, number]] {
public redetermineGraphBoundingBox(showTypes: boolean): [[number, number], [number, number]] {
this.minGraphX = 0;
this.maxGraphNodeX = 1;
this.maxGraphX = undefined; // see below
......@@ -77,36 +62,43 @@ export class Graph {
this.maxGraphY = 1;
for (const node of this.nodes()) {
if (!node.visible) {
continue;
}
if (!node.visible) continue;
if (node.x < this.minGraphX) {
this.minGraphX = node.x;
}
if ((node.x + node.getTotalNodeWidth()) > this.maxGraphNodeX) {
this.maxGraphNodeX = node.x + node.getTotalNodeWidth();
}
if ((node.y - 50) < this.minGraphY) {
this.minGraphY = node.y - 50;
}
if ((node.y + node.getNodeHeight(showTypes) + 50) > this.maxGraphY) {
this.maxGraphY = node.y + node.getNodeHeight(showTypes) + 50;
}
this.minGraphX = Math.min(this.minGraphX, node.x);
this.maxGraphNodeX = Math.max(this.maxGraphNodeX,
node.x + node.getTotalNodeWidth());
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.maxGraphX = this.maxGraphNodeX +
this.maxBackEdgeNumber * 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;
const extent: [[number, number], [number, number]] = [
return [
[this.minGraphX - this.width / 2, this.minGraphY - this.height / 2],
[this.maxGraphX + this.width / 2, this.maxGraphY + this.height / 2]
];
}
return extent;
public makeEdgesVisible(): void {
if (this.graphPhase.stateType == GraphStateType.NeedToFullRebuild) {
this.forEachEdge(edge =>
edge.visible = edge.source.visible && edge.target.visible
);
} else {
this.forEachEdge(edge =>
edge.visible = edge.visible || (this.isRendered() &&
edge.type === "control" && edge.source.visible && edge.target.visible)
);
}
}
public isRendered(): boolean {
return this.graphPhase.rendered;
}
}
......@@ -2,16 +2,17 @@
// 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 { GraphView } from "./views/graph-view";
import { ScheduleView } from "./views/schedule-view";
import { SequenceView } from "./views/sequence-view";
import { SourceResolver } from "./source-resolver";
import { GenericPhase, SourceResolver } from "./source-resolver";
import { SelectionBroker } from "./selection/selection-broker";
import { PhaseView, View } from "./views/view";
import { GraphPhase } from "./phases/graph-phase";
import { GraphNode } from "./phases/graph-phase/graph-node";
const multiviewID = "multiview";
import { storageGetItem, storageSetItem } from "./common/util";
import { PhaseType } from "./phases/phase";
const toolboxHTML = `
<div class="graph-toolbox">
......@@ -32,20 +33,6 @@ export class GraphMultiView extends View {
selectMenu: HTMLSelectElement;
currentPhaseView: PhaseView;
createViewElement() {
const pane = document.createElement("div");
pane.setAttribute("id", multiviewID);
pane.setAttribute("tabindex", "1");
pane.className = "viewpane";
return pane;
}
hide() {
this.container.className = "";
this.hideCurrentPhase();
super.hide();
}
constructor(id, selectionBroker, sourceResolver) {
super(id);
const view = this;
......@@ -57,9 +44,8 @@ export class GraphMultiView extends View {
view.divNode.appendChild(toolbox);
const searchInput = toolbox.querySelector("#search-input") as HTMLInputElement;
const onlyVisibleCheckbox = toolbox.querySelector("#search-only-visible") as HTMLInputElement;
searchInput.addEventListener("keyup", e => {
if (!view.currentPhaseView) return;
view.currentPhaseView.searchInputAction(searchInput, e, onlyVisibleCheckbox.checked);
searchInput.addEventListener("keyup", (e: KeyboardEvent) => {
view.currentPhaseView?.searchInputAction(searchInput, e, onlyVisibleCheckbox.checked);
});
view.divNode.addEventListener("keyup", (e: KeyboardEvent) => {
if (e.keyCode == 191) { // keyCode == '/'
......@@ -70,7 +56,7 @@ export class GraphMultiView extends View {
view.displayPreviousGraphPhase();
}
});
searchInput.setAttribute("value", window.sessionStorage.getItem("lastSearch") || "");
searchInput.setAttribute("value", storageGetItem("lastSearch", "", false));
this.graph = new GraphView(this.divNode, selectionBroker, view.displayPhaseByName.bind(this),
toolbox.querySelector(".graph-toolbox"));
this.schedule = new ScheduleView(this.divNode, selectionBroker);
......@@ -78,65 +64,65 @@ export class GraphMultiView extends View {
this.selectMenu = toolbox.querySelector("#phase-select") as HTMLSelectElement;
}
initializeSelect() {
const view = this;
view.selectMenu.innerHTML = "";
view.sourceResolver.forEachPhase(phase => {
const optionElement = document.createElement("option");
let maxNodeId = "";
if (phase instanceof GraphPhase && phase.highestNodeId != 0) {
maxNodeId = ` ${phase.highestNodeId}`;
}
optionElement.text = `${phase.name}${maxNodeId}`;
view.selectMenu.add(optionElement);
});
this.selectMenu.onchange = function (this: HTMLSelectElement) {
const phaseIndex = this.selectedIndex;
window.sessionStorage.setItem("lastSelectedPhase", phaseIndex.toString());
view.displayPhase(view.sourceResolver.getPhase(phaseIndex));
};
public createViewElement(): HTMLDivElement {
const pane = document.createElement("div");
pane.setAttribute("id", C.MULTIVIEW_ID);
pane.setAttribute("tabindex", "1");
pane.className = "viewpane";
return pane;
}
show() {
public hide(): void {
this.container.className = "";
this.hideCurrentPhase();
super.hide();
}
public show(): void {
// Insert before is used so that the display is inserted before the
// resizer for the RangeView.
this.container.insertBefore(this.divNode, this.container.firstChild);
this.initializeSelect();
const lastPhaseIndex = +window.sessionStorage.getItem("lastSelectedPhase");
const lastPhaseIndex = storageGetItem("lastSelectedPhase");
const initialPhaseIndex = this.sourceResolver.repairPhaseId(lastPhaseIndex);
this.selectMenu.selectedIndex = initialPhaseIndex;
this.displayPhase(this.sourceResolver.getPhase(initialPhaseIndex));
}
displayPhase(phase, selection?: Map<string, GraphNode>) {
if (phase.type == "graph") {
public onresize(): void {
this.currentPhaseView?.onresize();
}
private displayPhase(phase: GenericPhase, selection?: Map<string, GraphNode>): void {
if (phase.type == PhaseType.Graph) {
this.displayPhaseView(this.graph, phase, selection);
} else if (phase.type == "schedule") {
} else if (phase.type == PhaseType.Schedule) {
this.displayPhaseView(this.schedule, phase, selection);
} else if (phase.type == "sequence") {
} else if (phase.type == PhaseType.Sequence) {
this.displayPhaseView(this.sequence, phase, selection);
}
}
displayPhaseView(view: PhaseView, data, selection?: Map<string, GraphNode>) {
private displayPhaseView(view: PhaseView, data: GenericPhase, selection?: Map<string, GraphNode>):
void {
const rememberedSelection = selection ? selection : this.hideCurrentPhase();
view.initializeContent(data, rememberedSelection);
this.currentPhaseView = view;
}
displayPhaseByName(phaseName, selection?: Map<string, GraphNode>) {
private displayPhaseByName(phaseName: string, selection?: Map<string, GraphNode>): void {
const phaseId = this.sourceResolver.getPhaseIdByName(phaseName);
this.selectMenu.selectedIndex = phaseId;
this.displayPhase(this.sourceResolver.getPhase(phaseId), selection);
}
displayNextGraphPhase() {
private displayNextGraphPhase(): void {
let nextPhaseIndex = this.selectMenu.selectedIndex + 1;
while (nextPhaseIndex < this.sourceResolver.phases.length) {
const nextPhase = this.sourceResolver.getPhase(nextPhaseIndex);
if (nextPhase.type == "graph") {
if (nextPhase.type == PhaseType.Graph) {
this.selectMenu.selectedIndex = nextPhaseIndex;
window.sessionStorage.setItem("lastSelectedPhase", nextPhaseIndex.toString());
storageSetItem("lastSelectedPhase", nextPhaseIndex);
this.displayPhase(nextPhase);
break;
}
......@@ -144,13 +130,13 @@ export class GraphMultiView extends View {
}
}
displayPreviousGraphPhase() {
private displayPreviousGraphPhase(): void {
let previousPhaseIndex = this.selectMenu.selectedIndex - 1;
while (previousPhaseIndex >= 0) {
const previousPhase = this.sourceResolver.getPhase(previousPhaseIndex);
if (previousPhase.type == "graph") {
if (previousPhase.type === PhaseType.Graph) {
this.selectMenu.selectedIndex = previousPhaseIndex;
window.sessionStorage.setItem("lastSelectedPhase", previousPhaseIndex.toString());
storageSetItem("lastSelectedPhase", previousPhaseIndex);
this.displayPhase(previousPhase);
break;
}
......@@ -158,7 +144,26 @@ export class GraphMultiView extends View {
}
}
hideCurrentPhase() {
private initializeSelect(): void {
const view = this;
view.selectMenu.innerHTML = "";
view.sourceResolver.forEachPhase((phase: GenericPhase) => {
const optionElement = document.createElement("option");
let maxNodeId = "";
if (phase instanceof GraphPhase && phase.highestNodeId != 0) {
maxNodeId = ` ${phase.highestNodeId}`;
}
optionElement.text = `${phase.name}${maxNodeId}`;
view.selectMenu.add(optionElement);
});
this.selectMenu.onchange = function (this: HTMLSelectElement) {
const phaseIndex = this.selectedIndex;
storageSetItem("lastSelectedPhase", phaseIndex);
view.displayPhase(view.sourceResolver.getPhase(phaseIndex));
};
}
private hideCurrentPhase(): Map<string, GraphNode> {
let rememberedSelection = null;
if (this.currentPhaseView != null) {
rememberedSelection = this.currentPhaseView.detachSelection();
......@@ -167,12 +172,4 @@ export class GraphMultiView extends View {
}
return rememberedSelection;
}
onresize() {
if (this.currentPhaseView) this.currentPhaseView.onresize();
}
detachSelection() {
return null;
}
}
......@@ -2,21 +2,27 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { BytecodeOrigin, NodeOrigin } from "./origin";
import { BytecodePosition, SourcePosition } from "./position";
export class NodeLabel {
id: number;
label: string;
title: string;
live: boolean;
properties: string;
sourcePosition: any;
origin: any;
sourcePosition: SourcePosition | BytecodePosition;
origin: NodeOrigin | BytecodeOrigin;
opcode: string;
control: boolean;
opinfo: string;
type: string;
inplaceUpdatePhase: string;
constructor(id: number, label: string, title: string, live: boolean, properties: string, sourcePosition: any, origin: any, opcode: string, control: boolean, opinfo: string, type: string) {
constructor(id: number, label: string, title: string, live: boolean,
properties: string, sourcePosition: SourcePosition | BytecodePosition,
origin: NodeOrigin | BytecodeOrigin, opcode: string, control: boolean,
opinfo: string, type: string) {
this.id = id;
this.label = label;
this.title = title;
......@@ -31,17 +37,17 @@ export class NodeLabel {
this.inplaceUpdatePhase = null;
}
equals(that?: NodeLabel) {
public equals(that?: NodeLabel): boolean {
if (!that) return false;
if (this.id != that.id) return false;
if (this.label != that.label) return false;
if (this.title != that.title) return false;
if (this.live != that.live) return false;
if (this.properties != that.properties) return false;
if (this.opcode != that.opcode) return false;
if (this.control != that.control) return false;
if (this.opinfo != that.opinfo) return false;
return this.type == that.type;
if (this.id !== that.id) return false;
if (this.label !== that.label) return false;
if (this.title !== that.title) return false;
if (this.live !== that.live) return false;
if (this.properties !== that.properties) return false;
if (this.opcode !== that.opcode) return false;
if (this.control !== that.control) return false;
if (this.opinfo !== that.opinfo) return false;
return this.type === that.type;
}
public getTitle(): string {
......
......@@ -12,16 +12,21 @@ import { GraphEdge } from "./graph-phase/graph-edge";
export class GraphPhase extends Phase {
highestNodeId: number;
data: GraphData;
stateType: GraphStateType;
nodeLabelMap: Array<NodeLabel>;
nodeIdToNodeMap: Array<GraphNode>;
rendered: boolean;
transform: { x: number, y: number, scale: number };
constructor(name: string, highestNodeId: number, data?: GraphData,
nodeLabelMap?: Array<NodeLabel>, nodeIdToNodeMap?: Array<GraphNode>) {
super(name, PhaseType.Graph);
this.highestNodeId = highestNodeId;
this.data = data ?? new GraphData();
this.stateType = GraphStateType.NeedToFullRebuild;
this.nodeLabelMap = nodeLabelMap ?? new Array<NodeLabel>();
this.nodeIdToNodeMap = nodeIdToNodeMap ?? new Array<GraphNode>();
this.rendered = false;
}
public parseDataFromJSON(dataJson, nodeLabelMap: Array<NodeLabel>): void {
......@@ -92,3 +97,8 @@ export class GraphData {
this.edges = edges ?? new Array<GraphEdge>();
}
}
export enum GraphStateType {
NeedToFullRebuild,
Cached
}
......@@ -18,7 +18,7 @@ export class GraphEdge extends Edge<GraphNode> {
}
public getInputHorizontalPosition(graph: Graph, showTypes: boolean): number {
if (this.backEdgeNumber > 0) {
if (graph.graphPhase.rendered && this.backEdgeNumber > 0) {
return graph.maxGraphNodeX + this.backEdgeNumber * C.MINIMUM_EDGE_SEPARATION;
}
const source = this.source;
......
......@@ -58,7 +58,7 @@ export class GraphNode extends Node<GraphEdge> {
opcode.startsWith("Reference") ||
opcode.startsWith("Any") ||
opcode.endsWith("ToNumber") ||
(opcode == "AnyToBoolean") ||
(opcode === "AnyToBoolean") ||
(opcode.startsWith("Load") && opcode.length > 4) ||
(opcode.startsWith("Store") && opcode.length > 5);
}
......@@ -132,4 +132,13 @@ export class GraphNode extends Node<GraphEdge> {
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;
}
}
......@@ -13,7 +13,6 @@ export class InstructionsPhase extends Phase {
// Maps instruction offsets to PC offset.
instructionOffsetToPCOffset?: Array<[number, number]>;
codeOffsetsInfo?: CodeOffsetsInfo;
// Maps instruction numbers to PC offsets.
instructionToPCOffset: Array<TurbolizerInstructionStartInfo>;
// Maps PC offsets to instructions.
......
......@@ -43,8 +43,8 @@ export function sourcePositionValid(l) {
&& typeof l.inliningId !== undefined) || typeof l.bytecodePosition != undefined;
}
type GenericPosition = SourcePosition | BytecodePosition;
type GenericPhase = GraphPhase | TurboshaftGraphPhase | DisassemblyPhase
export type GenericPosition = SourcePosition | BytecodePosition;
export type GenericPhase = GraphPhase | TurboshaftGraphPhase | DisassemblyPhase
| InstructionsPhase | SchedulePhase | SequencePhase;
export class SourceResolver {
......
This diff is collapsed.
......@@ -316,7 +316,7 @@ class Helper {
// There are two fixed live ranges for each register, one for normal, another for deferred.
// These are combined into a single row.
const fixedRegisterMap = new Map<string, {ranges: [Range, Range], registerIndex: number}>();
for (const [registerIndex, range] of rangeMap) {
for (const [registerIndex, range] of Object.entries(rangeMap)) {
const registerName = this.fixedRegisterName(range);
if (fixedRegisterMap.has(registerName)) {
const entry = fixedRegisterMap.get(registerName);
......@@ -572,7 +572,7 @@ class RangeViewConstructor {
private addVirtualRanges(row: number) {
const source = this.view.sequenceView.sequence.registerAllocation;
for (const [registerIndex, range] of source.liveRanges) {
for (const [registerIndex, range] of Object.entries(source.liveRanges)) {
const registerName = Helper.virtualRegisterName(registerIndex);
const registerEl = this.elementForVirtualRegister(registerName);
this.addRowToGroup(row, this.elementForRow(row, registerIndex,
......@@ -768,7 +768,7 @@ class PhaseChangeHandler {
this.view.gridAccessor.addGrid(newGrid);
const source = this.view.sequenceView.sequence.registerAllocation;
let row = 0;
for (const [registerIndex, range] of source.liveRanges) {
for (const [registerIndex, range] of Object.entries(source.liveRanges)) {
this.addnewIntervalsInRange(currentGrid, newGrid, row, registerIndex,
new RangePair([range, undefined]));
++row;
......
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