From edddddbc3f20c8f391e4a26e550fcc7969ffeef4 Mon Sep 17 00:00:00 2001 From: Jeremy Rifkin <51220084+jeremy-rifkin@users.noreply.github.com> Date: Sat, 16 Aug 2025 09:51:18 -0500 Subject: [PATCH] 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 ![image](https://github.com/user-attachments/assets/a2d90337-538d-466f-b42d-0c56f6d4e05f) Example 1: ![image](https://github.com/user-attachments/assets/c3f321c9-58b6-4529-b35a-5f9f13e995c0) ![image](https://github.com/user-attachments/assets/e7918fe1-f145-4e39-a416-32c49a8c3100) Example 2: ![image](https://github.com/user-attachments/assets/b8737738-8b35-40e1-ae82-cfa940827d39) ![image](https://github.com/user-attachments/assets/89a9634a-f10d-48e1-ae45-0c87c76c806c) --- static/components.ts | 1 + static/graph-layout-core.ts | 86 +++++++++++++++++++++++------ static/panes/cfg-view.interfaces.ts | 1 + static/panes/cfg-view.ts | 9 ++- static/utils.ts | 7 +++ views/templates/panes/cfg.pug | 6 +- 6 files changed, 91 insertions(+), 19 deletions(-) diff --git a/static/components.ts b/static/components.ts index 7c2cc2152..2c3bf6236 100644 --- a/static/components.ts +++ b/static/components.ts @@ -533,6 +533,7 @@ export function getCfgViewWith( editorid, treeid, isircfg, + narrowtreelayout: true, }, }; } diff --git a/static/graph-layout-core.ts b/static/graph-layout-core.ts index cea67a439..aed4894a3 100644 --- a/static/graph-layout-core.ts +++ b/static/graph-layout-core.ts @@ -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; } } diff --git a/static/panes/cfg-view.interfaces.ts b/static/panes/cfg-view.interfaces.ts index 12ee09cde..2a052ab87 100644 --- a/static/panes/cfg-view.interfaces.ts +++ b/static/panes/cfg-view.interfaces.ts @@ -26,6 +26,7 @@ export interface CfgState { selectedFunction: string | null; isircfg?: boolean; centerparents?: boolean; + narrowtreelayout?: boolean; } /* diff --git a/static/panes/cfg-view.ts b/static/panes/cfg-view.ts index d6c22a9f3..066419251 100644 --- a/static/panes/cfg-view.ts +++ b/static/panes/cfg-view.ts @@ -141,6 +141,8 @@ export class Cfg extends Pane { 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 { 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
Basic blocks: ${ @@ -708,6 +714,7 @@ export class Cfg extends Pane { selectedFunction: this.state.selectedFunction, isircfg: this.state.isircfg, centerparents: this.toggles.get().centerparents, + narrowtreelayout: this.toggles.get().narrowtreelayout, }; this.paneRenaming.addState(state); return state; diff --git a/static/utils.ts b/static/utils.ts index fcbc58955..fda5d8b6f 100644 --- a/static/utils.ts +++ b/static/utils.ts @@ -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(a: T[], b: T[]) { + for (let i = 0; i < Math.min(a.length, b.length); i++) { + yield [a[i], b[i]] as [T, T]; + } +} diff --git a/views/templates/panes/cfg.pug b/views/templates/panes/cfg.pug index 9bae3ff83..3f7b17600 100644 --- a/views/templates/panes/cfg.pug +++ b/views/templates/panes/cfg.pug @@ -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