mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 07:04:04 -05:00
Pack trees in control flow graphs and add a setting for a wider or narrower layout (#7853)
Stacked on #7850 This PR implements this aspect of the cutter layout algorithm, using exact subtree shapes instead of the full bounding box  Example 1:   Example 2:  
This commit is contained in:
@@ -533,6 +533,7 @@ export function getCfgViewWith(
|
||||
editorid,
|
||||
treeid,
|
||||
isircfg,
|
||||
narrowtreelayout: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import IntervalTree from '@flatten-js/interval-tree';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import {AnnotatedCfgDescriptor, AnnotatedNodeDescriptor, EdgeColor} from '../types/compilation/cfg.interfaces.js';
|
||||
import {zip} from './utils.js';
|
||||
|
||||
// Much of the algorithm is inspired from
|
||||
// https://cutter.re/docs/api/widgets/classGraphGridLayout.html
|
||||
@@ -75,9 +77,17 @@ type Edge = {
|
||||
path: EdgeSegment[];
|
||||
};
|
||||
|
||||
type RowBound = {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
type BoundingBox = {
|
||||
rows: number;
|
||||
cols: number;
|
||||
// full bounding box
|
||||
width: number;
|
||||
height: number;
|
||||
// more exact tree shape
|
||||
rows: RowBound[];
|
||||
};
|
||||
|
||||
type Block = {
|
||||
@@ -148,6 +158,34 @@ type SegmentInfo = {
|
||||
|
||||
const EDGE_SPACING = 10;
|
||||
|
||||
function calculateTreePacking(left: BoundingBox, right: BoundingBox, narrowLayout: boolean) {
|
||||
if (!narrowLayout) {
|
||||
return 0;
|
||||
}
|
||||
const offsets: number[] = [];
|
||||
for (const [leftRow, rightRow] of zip(left.rows, right.rows)) {
|
||||
const leftBound = leftRow.end;
|
||||
const rightBound = rightRow.start;
|
||||
let offset = 0;
|
||||
offset -= left.width - leftBound;
|
||||
offset -= rightBound;
|
||||
offsets.push(offset);
|
||||
}
|
||||
return offsets.length === 0 ? 0 : offsets.reduce((a, b) => Math.min(a, b));
|
||||
}
|
||||
|
||||
function combineRowBounds(left: RowBound[], right: RowBound[]) {
|
||||
for (const [leftBound, rightBound] of zip(left, right)) {
|
||||
leftBound.start = Math.min(leftBound.start, rightBound.start);
|
||||
leftBound.end = Math.max(leftBound.end, rightBound.end);
|
||||
}
|
||||
if (left.length < right.length) {
|
||||
return [...left, ...right.slice(left.length).map(bound => cloneDeep(bound))];
|
||||
} else {
|
||||
return left;
|
||||
}
|
||||
}
|
||||
|
||||
export class GraphLayoutCore {
|
||||
// We use an adjacency list here
|
||||
blocks: Block[] = [];
|
||||
@@ -162,6 +200,7 @@ export class GraphLayoutCore {
|
||||
constructor(
|
||||
cfg: AnnotatedCfgDescriptor,
|
||||
readonly centerParents: boolean,
|
||||
readonly narrowLayout: boolean,
|
||||
) {
|
||||
this.populate_graph(cfg);
|
||||
|
||||
@@ -183,7 +222,7 @@ export class GraphLayoutCore {
|
||||
treeParent: null,
|
||||
row: 0,
|
||||
col: 0,
|
||||
boundingBox: {rows: 0, cols: 0},
|
||||
boundingBox: {width: 0, height: 0, rows: []},
|
||||
coordinates: {x: 0, y: 0},
|
||||
incidentEdgeCount: 0,
|
||||
};
|
||||
@@ -317,6 +356,10 @@ export class GraphLayoutCore {
|
||||
const block = this.blocks[root];
|
||||
block.row += rowShift;
|
||||
block.col += columnShift;
|
||||
for (const rowBound of block.boundingBox.rows) {
|
||||
rowBound.start += columnShift;
|
||||
rowBound.end += columnShift;
|
||||
}
|
||||
for (const j of block.treeEdges) {
|
||||
this.adjustSubtree(j, rowShift, columnShift);
|
||||
}
|
||||
@@ -330,8 +373,9 @@ export class GraphLayoutCore {
|
||||
block.row = 0;
|
||||
block.col = 0;
|
||||
block.boundingBox = {
|
||||
rows: 1,
|
||||
cols: 2,
|
||||
width: 2,
|
||||
height: 1,
|
||||
rows: [{start: 0, end: 2}],
|
||||
};
|
||||
} else if (block.treeEdges.length === 1) {
|
||||
const childIndex = block.treeEdges[0];
|
||||
@@ -339,35 +383,43 @@ export class GraphLayoutCore {
|
||||
block.row = 0;
|
||||
block.col = child.col;
|
||||
block.boundingBox = {
|
||||
rows: 1 + child.boundingBox.rows,
|
||||
cols: child.boundingBox.cols,
|
||||
width: child.boundingBox.width,
|
||||
height: child.boundingBox.height + 1,
|
||||
rows: [
|
||||
{start: child.col, end: child.col + 2},
|
||||
...child.boundingBox.rows.map(bound => cloneDeep(bound)),
|
||||
],
|
||||
};
|
||||
this.adjustSubtree(childIndex, 1, 0);
|
||||
} else {
|
||||
// If the node has more than two children we'll just center between the two direct children
|
||||
const boundingBox = {
|
||||
rows: 0,
|
||||
cols: 0,
|
||||
const boundingBox: BoundingBox = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
rows: [],
|
||||
};
|
||||
// Compute bounding box of all the subtrees and adjust
|
||||
// Place subtrees and update bounding box
|
||||
for (const i of block.treeEdges) {
|
||||
const child = this.blocks[i];
|
||||
this.adjustSubtree(i, 1, boundingBox.cols);
|
||||
boundingBox.rows += child.boundingBox.rows;
|
||||
boundingBox.cols += child.boundingBox.cols;
|
||||
const offset = calculateTreePacking(boundingBox, child.boundingBox, this.narrowLayout);
|
||||
this.adjustSubtree(i, 1, boundingBox.width + offset);
|
||||
boundingBox.width += child.boundingBox.width + offset;
|
||||
boundingBox.height = Math.max(boundingBox.height, child.boundingBox.height);
|
||||
boundingBox.rows = combineRowBounds(boundingBox.rows, child.boundingBox.rows);
|
||||
}
|
||||
// Position parent
|
||||
boundingBox.rows++;
|
||||
boundingBox.height++;
|
||||
block.boundingBox = boundingBox;
|
||||
block.row = 0;
|
||||
if (this.centerParents) {
|
||||
// center of bounding box
|
||||
block.col = Math.floor(Math.max(boundingBox.cols - 2, 0) / 2);
|
||||
block.col = Math.floor(Math.max(boundingBox.width - 2, 0) / 2);
|
||||
} else {
|
||||
// center between immediate children
|
||||
const [left, right] = [this.blocks[block.treeEdges[0]], this.blocks[block.treeEdges[1]]];
|
||||
block.col = Math.floor((left.col + right.col) / 2);
|
||||
}
|
||||
block.boundingBox.rows.unshift({start: block.col, end: block.col + 2});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,7 +434,7 @@ export class GraphLayoutCore {
|
||||
let offset = 0;
|
||||
for (const [i, tree] of trees) {
|
||||
this.adjustSubtree(i, 0, offset);
|
||||
offset += tree.boundingBox.cols;
|
||||
offset += tree.boundingBox.width;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface CfgState {
|
||||
selectedFunction: string | null;
|
||||
isircfg?: boolean;
|
||||
centerparents?: boolean;
|
||||
narrowtreelayout?: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -141,6 +141,8 @@ export class Cfg extends Pane<CfgState> {
|
||||
editorid: state.editorid,
|
||||
treeid: state.treeid,
|
||||
selectedFunction: (state as any).selectedFn,
|
||||
centerparents: state.centerparents,
|
||||
narrowtreelayout: state.narrowtreelayout,
|
||||
};
|
||||
}
|
||||
super(hub, container, state);
|
||||
@@ -531,7 +533,11 @@ export class Cfg extends Pane<CfgState> {
|
||||
const fn = this.results[name];
|
||||
this.bbMap = {};
|
||||
await this.createBasicBlocks(fn);
|
||||
this.layout = new GraphLayoutCore(fn as AnnotatedCfgDescriptor, !!this.state.centerparents);
|
||||
this.layout = new GraphLayoutCore(
|
||||
fn as AnnotatedCfgDescriptor,
|
||||
!!this.state.centerparents,
|
||||
!!this.state.narrowtreelayout,
|
||||
);
|
||||
this.applyLayout();
|
||||
this.drawEdges();
|
||||
this.infoElement.innerHTML = `Layout time: ${Math.round(this.layout.layoutTime)}ms<br/>Basic blocks: ${
|
||||
@@ -708,6 +714,7 @@ export class Cfg extends Pane<CfgState> {
|
||||
selectedFunction: this.state.selectedFunction,
|
||||
isircfg: this.state.isircfg,
|
||||
centerparents: this.toggles.get().centerparents,
|
||||
narrowtreelayout: this.toggles.get().narrowtreelayout,
|
||||
};
|
||||
this.paneRenaming.addState(state);
|
||||
return state;
|
||||
|
||||
@@ -154,3 +154,10 @@ export function getNumericToolTip(value: string, digitSeparator?: string): strin
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// zip two arrays up until min(a.length, b.length)
|
||||
export function* zip<T>(a: T[], b: T[]) {
|
||||
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
||||
yield [a[i], b[i]] as [T, T];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@
|
||||
span.hideable Layout Options
|
||||
.dropdown-menu.options
|
||||
.button-checkbox
|
||||
button.dropdown-item.btn.btn-sm.btn-light.center-parents(type="button" title="Center Parents" data-bind="centerparents" aria-pressed="false" aria-label="Center Parents")
|
||||
button.dropdown-item.btn.btn-sm.btn-light(type="button" title="Center Parents" data-bind="centerparents" aria-pressed="false" aria-label="Center Parents")
|
||||
span Center Parents
|
||||
input.d-none(type="checkbox" checked=false)
|
||||
.button-checkbox
|
||||
button.dropdown-item.btn.btn-sm.btn-light(type="button" title="Narrow Tree Layout" data-bind="narrowtreelayout" aria-pressed="false" aria-label="Narrow Tree Layout")
|
||||
span Narrow Tree Layout
|
||||
input.d-none(type="checkbox" checked=false)
|
||||
.btn-group.btn-group-sm(role="group" aria-label="CFG Export")
|
||||
button.btn.btn-sm.btn-light.dropdown-toggle(type="button" title="LLVM Opt Pass Options" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Set output options")
|
||||
span.fas.fa-arrow-down
|
||||
|
||||
Reference in New Issue
Block a user