blob: bd29f6d103462820ee67ecdb88c5c076299c1467 [file] [log] [blame] [raw]
// Copyright (c) 2017, Najjar Chedy
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import * as vis from 'vis-network';
import _ from 'underscore';
import {ga} from '../analytics';
import {Toggles} from '../widgets/toggles';
import TomSelect from 'tom-select';
import {Container} from 'golden-layout';
import {CfgOptions, CfgState} from './cfg-view.interfaces';
import {Hub} from '../hub';
import {Pane} from './pane';
interface NodeInfo {
edges: string[];
dagEdges: string[];
index: string;
id: number;
level: number;
state: number;
inCount: number;
}
export class Cfg extends Pane<CfgState> {
defaultCfgOutput: object;
llvmCfgPlaceholder: object;
binaryModeSupport: object;
savedPos: any;
savedScale: any;
needsMove: boolean;
currentFunc: string;
functions: Record<string, vis.Data>;
networkOpts: vis.Options;
cfgVisualiser: vis.Network;
_binaryFilter: boolean;
functionPicker: TomSelect;
toggles: Toggles;
toggleNavigationButton: JQuery;
toggleNavigationTitle: string;
togglePhysicsButton: JQuery;
togglePhysicsTitle: string;
options: Required<CfgOptions>;
constructor(hub: Hub, container: Container, state: CfgState) {
super(hub, container, state);
this.llvmCfgPlaceholder = {
nodes: [
{
id: 0,
shape: 'box',
label: '-emit-llvm currently not supported',
},
],
edges: [],
};
this.binaryModeSupport = {
nodes: [
{
id: 0,
shape: 'box',
label: 'Cfg mode cannot be used when the binary filter is set',
},
],
edges: [],
};
this.savedPos = state.pos;
this.savedScale = state.scale;
this.needsMove = this.savedPos && this.savedScale;
this.currentFunc = state.selectedFn || '';
this.functions = {};
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: (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];
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.updateState();
}
},
});
this.updateButtons();
}
override getInitialHTML(): string {
return $('#cfg').html();
}
override registerOpeningAnalyticsEvent(): void {
ga.proxy('send', {
hitType: 'event',
eventCategory: 'OpenViewPane',
eventAction: 'Cfg',
});
}
override onCompileResult(compilerId: number, compiler: any, result: any) {
if (this.compilerInfo.compilerId === compilerId) {
let functionNames: string[] = [];
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: selectedFn.nodes,
edges: selectedFn.edges,
});
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?.includes('-emit-llvm')) {
this.showCfgResults(this._binaryFilter ? this.binaryModeSupport : this.defaultCfgOutput);
} else {
this.showCfgResults(this._binaryFilter ? this.binaryModeSupport : this.llvmCfgPlaceholder);
}
}
this.functionPicker.clearOptions();
this.functionPicker.addOption(
functionNames.length
? this.adaptStructure(functionNames)
: {name: 'The input does not contain functions'}
);
this.functionPicker.refreshOptions(false);
this.functionPicker.clear();
this.functionPicker.addItem(
functionNames.length ? this.currentFunc : 'The input does not contain any function',
true
);
this.updateState();
}
}
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.compilerInfo.compilerId === compilerId) {
this._binaryFilter = filters.binary;
}
}
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;
this.togglePhysicsButton = this.domRoot.find('.toggle-physics');
this.togglePhysicsTitle = this.togglePhysicsButton.prop('title');
this.topBar = this.domRoot.find('.top-bar');
}
override registerCallbacks() {
this.cfgVisualiser.on('dragEnd', this.updateState.bind(this));
this.cfgVisualiser.on('zoom', this.updateState.bind(this));
this.eventHub.on('filtersChange', this.onFiltersChange, this);
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');
// change only physics.enabled option to preserve current node locations
this.cfgVisualiser.setOptions({
physics: {enabled: this.networkOpts.physics.enabled},
});
});
this.toggleNavigationButton.on('click', () => {
this.networkOpts.interaction.navigationButtons = this.toggleNavigationButton.hasClass('active');
this.cfgVisualiser.setOptions({
interaction: {
navigationButtons: this.networkOpts.interaction.navigationButtons,
},
});
});
this.toggles.on('change', () => {
this.updateButtons();
this.updateState();
});
}
updateButtons() {
const formatButtonTitle = (button: JQuery, title: string) => {
button.prop('title', '[' + (button.hasClass('active') ? 'ON' : 'OFF') + '] ' + title);
};
formatButtonTitle(this.togglePhysicsButton, this.togglePhysicsTitle);
formatButtonTitle(this.toggleNavigationButton, this.toggleNavigationTitle);
}
override resize() {
const height = (this.domRoot.height() as number) - (this.topBar.outerHeight(true) ?? 0);
if ((this.cfgVisualiser as any).canvas !== undefined) {
this.cfgVisualiser.setSize('100%', height.toString());
this.cfgVisualiser.redraw();
}
}
override getDefaultPaneName() {
return 'Graph Viewer';
}
assignLevels(data: vis.Data) {
const nodes: NodeInfo[] = [];
const idToIdx: string[] = [];
for (const i in data.nodes) {
const node = data.nodes[i];
idToIdx[node.id] = i;
nodes.push({
edges: [],
dagEdges: [],
index: i,
id: node.id,
level: 0,
state: 0,
inCount: 0,
});
}
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: NodeInfo) => {
// choose which edges will be back-edges
node.state = 1;
node.edges.forEach(targetIndex => {
const target = nodes[targetIndex];
if (target.state !== 1) {
if (target.state === 0) {
dfs(target);
}
node.dagEdges.push(targetIndex);
target.inCount += 1;
}
});
node.state = 2;
};
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) {
markLevels(target);
}
});
};
nodes.forEach(node => {
if (node.state === 0) {
dfs(node);
node.level = 1;
markLevels(node);
}
});
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) {
edge.physics = false;
} else {
edge.physics = true;
const diff = nodeB.level - nodeA.level;
edge.length = diff * (200 - 5 * Math.min(5, diff));
}
} else {
edge.physics = false;
}
}
}
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
* breaking the move, but it does not seem like it
*/
if (this.needsMove) {
this.cfgVisualiser.moveTo({
position: this.savedPos,
animation: false,
scale: this.savedScale,
});
this.needsMove = false;
}
}
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(() => {
this.container.close();
}, this);
}
}
override close() {
this.eventHub.unsubscribe();
this.eventHub.emit('cfgViewClosed', this.compilerInfo.compilerId);
this.cfgVisualiser.destroy();
}
getEffectiveOptions() {
return this.toggles.get();
}
override getCurrentState(): CfgState {
return {
...super.getCurrentState(),
selectedFn: this.currentFunc,
pos: this.cfgVisualiser.getViewPosition(),
scale: this.cfgVisualiser.getScale(),
options: this.getEffectiveOptions(),
};
}
adaptStructure(names: string[]) {
return names.map(name => {
return {name};
});
}
}