diff options
-rw-r--r-- | static/panes/cfg-view.interfaces.ts | 20 | ||||
-rw-r--r-- | static/panes/cfg-view.ts | 351 | ||||
-rw-r--r-- | static/panes/pane.ts | 5 |
3 files changed, 175 insertions, 201 deletions
diff --git a/static/panes/cfg-view.interfaces.ts b/static/panes/cfg-view.interfaces.ts index 9f37b2411..5683354f2 100644 --- a/static/panes/cfg-view.interfaces.ts +++ b/static/panes/cfg-view.interfaces.ts @@ -22,15 +22,17 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. -export interface CfgState { - id: number; - editorid?: number; - treeid?: number; +import {PaneState} from './pane.interfaces'; +import * as vis from 'vis-network'; + +export interface CfgOptions { + physics?: boolean; + navigation?: boolean; +} + +export interface CfgState extends PaneState { selectedFn?: string; - pos: any; // vis.Network.Position + pos: vis.Position; scale: number; - options?: { - physics?: boolean; - navigation?: boolean; - }; + options?: CfgOptions; } diff --git a/static/panes/cfg-view.ts b/static/panes/cfg-view.ts index 12cb89ae3..a54c447d7 100644 --- a/static/panes/cfg-view.ts +++ b/static/panes/cfg-view.ts @@ -24,17 +24,17 @@ import * as vis from 'vis-network'; import _ from 'underscore'; -import {Toggles} from '../widgets/toggles'; import {ga} from '../analytics'; +import {Toggles} from '../widgets/toggles'; import TomSelect from 'tom-select'; import {Container} from 'golden-layout'; -import {CfgState} from './cfg-view.interfaces'; -import {PaneRenaming} from '../widgets/pane-renaming'; +import {CfgOptions, CfgState} from './cfg-view.interfaces'; import {Hub} from '../hub'; +import {Pane} from './pane'; interface NodeInfo { edges: string[]; - dagEdges: number[]; + dagEdges: string[]; index: string; id: number; level: number; @@ -42,10 +42,7 @@ interface NodeInfo { inCount: number; } -export class Cfg { - container: Container; - eventHub: any; - domRoot: JQuery; +export class Cfg extends Pane<CfgState> { defaultCfgOutput: object; llvmCfgPlaceholder: object; binaryModeSupport: object; @@ -53,31 +50,21 @@ export class Cfg { savedScale: any; needsMove: boolean; currentFunc: string; - functions: {[key: string]: any}; - networkOpts: any; - cfgVisualiser: any; - compilerId: number; - _compilerName = ''; - _editorid?: number; - _treeid?: number; + functions: Record<string, vis.Data>; + networkOpts: vis.Options; + cfgVisualiser: vis.Network; _binaryFilter: boolean; functionPicker: TomSelect; - supportsCfg = false; toggles: Toggles; toggleNavigationButton: JQuery; toggleNavigationTitle: string; togglePhysicsButton: JQuery; togglePhysicsTitle: string; - topBar: JQuery; - paneName: string; - paneRenaming: PaneRenaming; + options: Required<CfgOptions>; constructor(hub: Hub, container: Container, state: CfgState) { - this.container = container; - this.eventHub = hub.createEventHub(); - this.domRoot = container.getElement(); - this.domRoot.html($('#cfg').html()); - this.defaultCfgOutput = {nodes: [{id: 0, shape: 'box', label: 'No Output'}], edges: []}; + super(hub, container, state); + this.llvmCfgPlaceholder = { nodes: [ { @@ -98,9 +85,7 @@ export class Cfg { ], edges: [], }; - // Note that this might be outdated if no functions were present when creating the link, but that's handled - // by selectize - state.options = state.options || {}; + this.savedPos = state.pos; this.savedScale = state.scale; this.needsMove = this.savedPos && this.savedScale; @@ -108,90 +93,43 @@ export class Cfg { this.currentFunc = state.selectedFn || ''; this.functions = {}; - this.networkOpts = { - autoResize: true, - locale: 'en', - edges: { - arrows: {to: {enabled: true}}, - smooth: { - enabled: true, - type: 'dynamic', - roundness: 1, - }, - physics: true, - }, - nodes: { - font: {face: 'Consolas, "Liberation Mono", Courier, monospace', align: 'left'}, - }, - layout: { - hierarchical: { - enabled: true, - direction: 'UD', - nodeSpacing: 100, - levelSeparation: 150, - }, - }, - physics: { - enabled: !!state.options.physics, - hierarchicalRepulsion: { - nodeDistance: 160, - }, - }, - interaction: { - navigationButtons: !!state.options.navigation, - keyboard: { - enabled: true, - speed: {x: 10, y: 10, zoom: 0.03}, - bindToWindow: false, - }, - }, - }; - - this.cfgVisualiser = new vis.Network( - this.domRoot.find('.graph-placeholder')[0], - this.defaultCfgOutput, - this.networkOpts - ); - - this.initButtons(state); - - this.compilerId = state.id; - this._editorid = state.editorid; - this._treeid = state.treeid; this._binaryFilter = false; const pickerEl = this.domRoot.find('.function-picker')[0] as HTMLInputElement; - this.functionPicker = new TomSelect( - pickerEl, - { - sortField: 'name', - valueField: 'name', - labelField: 'name', - searchField: ['name'], - dropdownParent: 'body', - plugins: ['input_autogrow'], - onChange: (val: string) => { + this.functionPicker = new TomSelect(pickerEl, { + sortField: 'name', + valueField: 'name', + labelField: 'name', + searchField: ['name'], + dropdownParent: 'body', + plugins: ['input_autogrow'], + onChange: (e: any) => { + // TomSelect says it's an Event, but we receive strings + const val = e as string; + if (val in this.functions) { const selectedFn = this.functions[val]; - if (selectedFn) { - this.currentFunc = val; - this.showCfgResults({ - nodes: selectedFn.nodes, - edges: selectedFn.edges, - }); + this.currentFunc = val; + this.showCfgResults({ + nodes: selectedFn.nodes, + edges: selectedFn.edges, + }); + if (selectedFn.nodes && selectedFn.nodes.length > 0) { this.cfgVisualiser.selectNodes([selectedFn.nodes[0].id]); - this.resize(); - this.saveState(); } - }, - } as any - // The current version of tom-select has a very restrictive type definition - // that forces to pass the whole options object. This is a workaround to make it type check - ); - - this.paneRenaming = new PaneRenaming(this, state); + this.resize(); + this.updateState(); + } + }, + }); - this.initCallbacks(); this.updateButtons(); + } + + override getInitialHTML(): string { + return $('#cfg').html(); + } + + override registerOpeningAnalyticsEvent(): void { ga.proxy('send', { hitType: 'event', eventCategory: 'OpenViewPane', @@ -199,24 +137,27 @@ export class Cfg { }); } - onCompileResult(compilerId: number, compiler: any, result: any) { - if (this.compilerId === compilerId) { + override onCompileResult(compilerId: number, compiler: any, result: any) { + if (this.compilerInfo.compilerId === compilerId) { let functionNames: string[] = []; - if (this.supportsCfg && !$.isEmptyObject(result.cfg)) { + if (compiler.supportsCfg && !$.isEmptyObject(result.cfg)) { this.functions = result.cfg; functionNames = Object.keys(this.functions); if (functionNames.indexOf(this.currentFunc) === -1) { this.currentFunc = functionNames[0]; } + const selectedFn = this.functions[this.currentFunc]; this.showCfgResults({ - nodes: this.functions[this.currentFunc].nodes, - edges: this.functions[this.currentFunc].edges, + nodes: selectedFn.nodes, + edges: selectedFn.edges, }); - this.cfgVisualiser.selectNodes([this.functions[this.currentFunc].nodes[0].id]); + if (selectedFn.nodes && selectedFn.nodes.length > 0) { + this.cfgVisualiser.selectNodes([selectedFn.nodes[0].id]); + } } else { // We don't reset the current function here as we would lose the saved one if this happened at the beginning // (Hint: It *does* happen) - if (result.compilationOptions.indexOf('-emit-llvm') === -1) { + if (!result.compilationOptions?.includes('-emit-llvm')) { this.showCfgResults(this._binaryFilter ? this.binaryModeSupport : this.defaultCfgOutput); } else { this.showCfgResults(this._binaryFilter ? this.binaryModeSupport : this.llvmCfgPlaceholder); @@ -236,25 +177,80 @@ export class Cfg { functionNames.length ? this.currentFunc : 'The input does not contain any function', true ); - this.saveState(); + this.updateState(); } } - onCompiler(compilerId: number, compiler: any) { - if (compilerId === this.compilerId) { - this._compilerName = compiler ? compiler.name : ''; - this.supportsCfg = compiler.supportsCfg; + + override registerDynamicElements(state: CfgState) { + this.defaultCfgOutput = {nodes: [{id: 0, shape: 'box', label: 'No Output'}], edges: []}; + // Note that this might be outdated if no functions were present when creating the link, but that's handled + // by selectize + this.options = { + navigation: state.options?.navigation ?? false, + physics: state.options?.physics ?? false, + }; + + this.networkOpts = { + autoResize: true, + locale: 'en', + edges: { + arrows: {to: {enabled: true}}, + smooth: { + enabled: true, + type: 'dynamic', + roundness: 1, + }, + physics: true, + }, + nodes: { + font: {face: 'Consolas, "Liberation Mono", Courier, monospace', align: 'left'}, + }, + layout: { + hierarchical: { + enabled: true, + direction: 'UD', + nodeSpacing: 100, + levelSeparation: 150, + }, + }, + physics: { + enabled: this.options.physics, + hierarchicalRepulsion: { + nodeDistance: 160, + }, + }, + interaction: { + navigationButtons: this.options.navigation, + keyboard: { + enabled: true, + speed: {x: 10, y: 10, zoom: 0.03}, + bindToWindow: false, + }, + }, + }; + + this.cfgVisualiser = new vis.Network( + this.domRoot.find('.graph-placeholder')[0], + this.defaultCfgOutput, + this.networkOpts + ); + } + + override onCompiler(compilerId: number, compiler: any) { + if (compilerId === this.compilerInfo.compilerId) { + this.compilerInfo.compilerName = compiler ? compiler.name : ''; this.updateTitle(); } } onFiltersChange(compilerId: number, filters: any) { - if (this.compilerId === compilerId) { + if (this.compilerInfo.compilerId === compilerId) { this._binaryFilter = filters.binary; } } - initButtons(state: any) { - this.toggles = new Toggles(this.domRoot.find('.options'), state.options); + override registerButtons(state: CfgState) { + this.toggles = new Toggles(this.domRoot.find('.options'), this.options); this.toggleNavigationButton = this.domRoot.find('.toggle-navigation'); this.toggleNavigationTitle = this.toggleNavigationButton.prop('title') as string; @@ -265,23 +261,15 @@ export class Cfg { this.topBar = this.domRoot.find('.top-bar'); } - initCallbacks() { - this.cfgVisualiser.on('dragEnd', this.saveState.bind(this)); - this.cfgVisualiser.on('zoom', this.saveState.bind(this)); + override registerCallbacks() { + this.cfgVisualiser.on('dragEnd', this.updateState.bind(this)); + this.cfgVisualiser.on('zoom', this.updateState.bind(this)); - this.paneRenaming.on('renamePane', this.saveState.bind(this)); - - this.eventHub.on('compilerClose', this.onCompilerClose, this); - this.eventHub.on('compileResult', this.onCompileResult, this); - this.eventHub.on('compiler', this.onCompiler, this); this.eventHub.on('filtersChange', this.onFiltersChange, this); - this.container.on('destroy', this.close, this); - this.container.on('resize', this.resize, this); - this.container.on('shown', this.resize, this); - this.eventHub.emit('cfgViewOpened', this.compilerId); - this.eventHub.emit('requestFilters', this.compilerId); - this.eventHub.emit('requestCompiler', this.compilerId); + this.eventHub.emit('cfgViewOpened', this.compilerInfo.compilerId); + this.eventHub.emit('requestFilters', this.compilerInfo.compilerId); + this.eventHub.emit('requestCompiler', this.compilerInfo.compilerId); this.togglePhysicsButton.on('click', () => { this.networkOpts.physics.enabled = this.togglePhysicsButton.hasClass('active'); @@ -301,7 +289,7 @@ export class Cfg { }); this.toggles.on('change', () => { this.updateButtons(); - this.saveState(); + this.updateState(); }); } @@ -313,35 +301,17 @@ export class Cfg { formatButtonTitle(this.toggleNavigationButton, this.toggleNavigationTitle); } - resize() { - if (this.cfgVisualiser.canvas) { - const height = (this.domRoot.height() as number) - (this.topBar.outerHeight(true) ?? 0); - this.cfgVisualiser.setSize('100%', height.toString()); - this.cfgVisualiser.redraw(); - } + override resize() { + const height = (this.domRoot.height() as number) - (this.topBar.outerHeight(true) ?? 0); + this.cfgVisualiser.setSize('100%', height.toString()); + this.cfgVisualiser.redraw(); } - getDefaultPaneName() { + override getDefaultPaneName() { return 'Graph Viewer'; } - getPaneTag() { - if (this._editorid) { - return `${this._compilerName} (Editor #${this._editorid}, Compiler #${this.compilerId})`; - } else { - return `${this._compilerName} (Tree #${this._treeid}, Compiler #${this.compilerId})`; - } - } - - getPaneName() { - return this.paneName ? this.paneName : this.getDefaultPaneName() + ' ' + this.getPaneTag(); - } - - updateTitle() { - this.container.setTitle(_.escape(this.getPaneName())); - } - - assignLevels(data: any) { + assignLevels(data: vis.Data) { const nodes: NodeInfo[] = []; const idToIdx: string[] = []; for (const i in data.nodes) { @@ -357,17 +327,18 @@ export class Cfg { inCount: 0, }); } - const isEdgeValid = (edge: any) => edge.from in idToIdx && edge.to in idToIdx; - data.edges.forEach((edge: any) => { - if (isEdgeValid(edge)) { + const isEdgeValid = (edge: vis.Edge) => edge.from && edge.to && edge.from in idToIdx && edge.to in idToIdx; + for (const edge of data.edges as vis.Edge[]) { + if (edge.from && edge.to && isEdgeValid(edge)) { nodes[idToIdx[edge.from]].edges.push(idToIdx[edge.to]); } - }); + } - const dfs = (node: any) => { + const dfs = (node: NodeInfo) => { // choose which edges will be back-edges node.state = 1; - node.edges.forEach((targetIndex: number) => { + + node.edges.forEach(targetIndex => { const target = nodes[targetIndex]; if (target.state !== 1) { if (target.state === 0) { @@ -379,8 +350,8 @@ export class Cfg { }); node.state = 2; }; - const markLevels = (node: any) => { - node.dagEdges.forEach((targetIndex: number) => { + const markLevels = (node: NodeInfo) => { + node.dagEdges.forEach(targetIndex => { const target = nodes[targetIndex]; target.level = Math.max(target.level, node.level + 1); if (--target.inCount === 0) { @@ -388,18 +359,21 @@ export class Cfg { } }); }; - nodes.forEach((node: any) => { + nodes.forEach(node => { if (node.state === 0) { dfs(node); node.level = 1; markLevels(node); } }); - nodes.forEach((node: any) => { - data.nodes[node.index]['level'] = node.level; - }); - data.edges.forEach((edge: any) => { - if (isEdgeValid(edge)) { + if (data.nodes) { + for (const node of nodes) { + data.nodes[node.index]['level'] = node.level; + } + } + + for (const edge of data.edges as vis.Edge[]) { + if (edge.from && edge.to && isEdgeValid(edge)) { const nodeA = nodes[idToIdx[edge.from]]; const nodeB = nodes[idToIdx[edge.to]]; if (nodeA.level >= nodeB.level) { @@ -412,13 +386,14 @@ export class Cfg { } else { edge.physics = false; } - }); + } } - showCfgResults(data: any) { + showCfgResults(data: vis.Data) { this.assignLevels(data); this.cfgVisualiser.setData(data); - /* FIXME: This does not work. It's here because I suspected that not having content in the constructor was + /* FIXME: This does not work. + * It's here because I suspected that not having content in the constructor was * breaking the move, but it does not seem like it */ if (this.needsMove) { @@ -431,47 +406,39 @@ export class Cfg { } } - onCompilerClose(compilerId: number) { - if (this.compilerId === compilerId) { + override onCompilerClose(compilerId: number) { + if (this.compilerInfo.compilerId === compilerId) { // We can't immediately close as an outer loop somewhere in GoldenLayout is iterating over // the hierarchy. We can't modify while it's being iterated over. this.close(); - _.defer((self: Cfg) => { - self.container.close(); + _.defer(() => { + this.container.close(); }, this); } } - close() { + override close() { this.eventHub.unsubscribe(); - this.eventHub.emit('cfgViewClosed', this.compilerId); + this.eventHub.emit('cfgViewClosed', this.compilerInfo.compilerId); this.cfgVisualiser.destroy(); } - saveState() { - this.container.setState(this.currentState()); - } - getEffectiveOptions() { return this.toggles.get(); } - currentState(): CfgState { - const state = { - id: this.compilerId, - editorid: this._editorid, - treeid: this._treeid, + override getCurrentState(): CfgState { + return { + ...super.getCurrentState(), selectedFn: this.currentFunc, pos: this.cfgVisualiser.getViewPosition(), scale: this.cfgVisualiser.getScale(), options: this.getEffectiveOptions(), }; - this.paneRenaming.addState(state); - return state; } adaptStructure(names: string[]) { - return _.map(names, name => { + return names.map(name => { return {name}; }); } diff --git a/static/panes/pane.ts b/static/panes/pane.ts index 794252adf..b0fa9c0d2 100644 --- a/static/panes/pane.ts +++ b/static/panes/pane.ts @@ -84,6 +84,8 @@ export abstract class Pane<S> { this.paneRenaming = new PaneRenaming(this, state); + this.registerDynamicElements(state); + this.registerButtons(state); this.registerStandardCallbacks(); this.registerCallbacks(); @@ -116,6 +118,9 @@ export abstract class Pane<S> { */ abstract registerOpeningAnalyticsEvent(): void; + /** Optional overridable code for initializing necessary elements before rest of registers **/ + registerDynamicElements(state: S): void {} + /** Optionally overridable code for initializing pane buttons */ registerButtons(state: S): void {} |