mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
This PR refactors some common utilities out of lib/ and into shared/ and eliminates some use of underscore.js, as well as general type improvements done along the way.
438 lines
18 KiB
TypeScript
438 lines
18 KiB
TypeScript
// Copyright (c) 2022, Compiler Explorer Authors
|
|
// 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 $ from 'jquery';
|
|
import _ from 'underscore';
|
|
import * as monaco from 'monaco-editor';
|
|
import {Container} from 'golden-layout';
|
|
import TomSelect from 'tom-select';
|
|
import scrollIntoView from 'scroll-into-view-if-needed';
|
|
|
|
import {MonacoPane} from './pane.js';
|
|
import {LLVMOptPipelineViewState} from './llvm-opt-pipeline.interfaces.js';
|
|
import {MonacoPaneState} from './pane.interfaces.js';
|
|
|
|
import {ga} from '../analytics.js';
|
|
import {extendConfig} from '../monaco-config.js';
|
|
import {Hub} from '../hub.js';
|
|
import * as utils from '../utils.js';
|
|
import {Toggles} from '../widgets/toggles.js';
|
|
|
|
import {
|
|
LLVMOptPipelineBackendOptions,
|
|
LLVMOptPipelineOutput,
|
|
LLVMOptPipelineResults,
|
|
} from '../../types/compilation/llvm-opt-pipeline-output.interfaces.js';
|
|
import {unwrap} from '../assert.js';
|
|
import {CompilationResult} from '../compilation/compilation.interfaces.js';
|
|
import {CompilerInfo} from '../compiler.interfaces.js';
|
|
import {escapeHTML} from '../../shared/common-utils.js';
|
|
|
|
const MIN_SIDEBAR_WIDTH = 100;
|
|
|
|
export class LLVMOptPipeline extends MonacoPane<monaco.editor.IStandaloneDiffEditor, LLVMOptPipelineViewState> {
|
|
results: LLVMOptPipelineResults = {};
|
|
passesColumn: JQuery;
|
|
passesList: JQuery;
|
|
passesColumnResizer: JQuery;
|
|
body: JQuery;
|
|
clickCallback: (e: JQuery.ClickEvent) => void;
|
|
keydownCallback: (e: JQuery.KeyDownEvent) => void;
|
|
isPassListSelected = false;
|
|
functionSelector: TomSelect;
|
|
originalModel: any;
|
|
modifiedModel: any;
|
|
options: Toggles;
|
|
filters: Toggles;
|
|
state: LLVMOptPipelineViewState;
|
|
lastOptions: LLVMOptPipelineBackendOptions = {
|
|
filterDebugInfo: true,
|
|
filterIRMetadata: false,
|
|
fullModule: false,
|
|
noDiscardValueNames: true,
|
|
demangle: true,
|
|
libraryFunctions: true,
|
|
};
|
|
resizeStartX: number;
|
|
resizeStartWidth: number;
|
|
resizeDragMoveBind: (e: MouseEvent) => void;
|
|
resizeDragEndBind: (e: MouseEvent) => void;
|
|
firstResults = true;
|
|
|
|
constructor(hub: Hub, container: Container, state: LLVMOptPipelineViewState & MonacoPaneState) {
|
|
super(hub, container, state);
|
|
this.passesColumn = this.domRoot.find('.passes-column');
|
|
this.passesList = this.domRoot.find('.passes-list');
|
|
this.body = this.domRoot.find('.llvm-opt-pipeline-body');
|
|
if (state.sidebarWidth === 0) {
|
|
_.defer(() => {
|
|
state.sidebarWidth = parseInt(
|
|
unwrap(document.defaultView).getComputedStyle(this.passesColumn.get()[0]).width,
|
|
10,
|
|
);
|
|
state.sidebarWidth = Math.max(state.sidebarWidth, MIN_SIDEBAR_WIDTH);
|
|
this.resize();
|
|
this.updateState();
|
|
});
|
|
} else {
|
|
state.sidebarWidth = Math.max(state.sidebarWidth, MIN_SIDEBAR_WIDTH);
|
|
this.passesColumn.get()[0].style.width = state.sidebarWidth + 'px';
|
|
}
|
|
this.state = state;
|
|
const selector = this.domRoot.get()[0].getElementsByClassName('function-selector')[0];
|
|
if (!(selector instanceof HTMLSelectElement)) {
|
|
throw new Error('.function-selector is not an HTMLSelectElement');
|
|
}
|
|
this.functionSelector = new TomSelect(selector, {
|
|
valueField: 'value',
|
|
labelField: 'title',
|
|
searchField: ['title'],
|
|
dropdownParent: 'body',
|
|
plugins: ['input_autogrow'],
|
|
sortField: 'title',
|
|
onChange: e => this.selectFunction(e as any as string),
|
|
});
|
|
this.clickCallback = this.onClickCallback.bind(this);
|
|
this.keydownCallback = this.onKeydownCallback.bind(this);
|
|
$(document).on('click', this.clickCallback);
|
|
$(document).on('keydown', this.keydownCallback);
|
|
this.eventHub.emit('llvmOptPipelineViewOpened', this.compilerInfo.compilerId);
|
|
this.eventHub.emit('requestSettings');
|
|
this.emitOptions(true);
|
|
}
|
|
|
|
override getInitialHTML(): string {
|
|
return $('#llvm-opt-pipeline').html();
|
|
}
|
|
|
|
override createEditor(editorRoot: HTMLElement): monaco.editor.IStandaloneDiffEditor {
|
|
const editor = monaco.editor.createDiffEditor(
|
|
editorRoot,
|
|
extendConfig({
|
|
language: 'llvm-ir',
|
|
readOnly: true,
|
|
glyphMargin: true,
|
|
lineNumbersMinChars: 3,
|
|
}),
|
|
);
|
|
this.originalModel = monaco.editor.createModel('', 'llvm-ir');
|
|
this.modifiedModel = monaco.editor.createModel('', 'llvm-ir');
|
|
editor.setModel({original: this.originalModel, modified: this.modifiedModel});
|
|
return editor;
|
|
}
|
|
|
|
override getPrintName() {
|
|
return '<Unimplemented>';
|
|
}
|
|
|
|
override sendPrintData() {
|
|
// nop
|
|
}
|
|
|
|
override registerOpeningAnalyticsEvent(): void {
|
|
ga.proxy('send', {
|
|
hitType: 'event',
|
|
eventCategory: 'OpenViewPane',
|
|
eventAction: 'LLVMOptPipelineView',
|
|
});
|
|
}
|
|
|
|
override getDefaultPaneName(): string {
|
|
return 'LLVM Opt Pipeline Viewer';
|
|
}
|
|
|
|
override registerButtons(state: LLVMOptPipelineViewState) {
|
|
super.registerButtons(state);
|
|
this.options = new Toggles(this.domRoot.find('.options'), state as unknown as Record<string, boolean>);
|
|
this.options.on('change', this.onOptionsChange.bind(this));
|
|
this.filters = new Toggles(this.domRoot.find('.filters'), state as unknown as Record<string, boolean>);
|
|
this.filters.on('change', this.onOptionsChange.bind(this));
|
|
|
|
this.passesColumnResizer = this.domRoot.find('.passes-column-resizer');
|
|
this.passesColumnResizer.get()[0].addEventListener('mousedown', this.initResizeDrag.bind(this), false);
|
|
}
|
|
|
|
initResizeDrag(e: MouseEvent) {
|
|
// taken from SO
|
|
this.resizeStartX = e.clientX;
|
|
this.resizeStartWidth = parseInt(
|
|
unwrap(document.defaultView).getComputedStyle(this.passesColumn.get()[0]).width,
|
|
10,
|
|
);
|
|
this.resizeDragMoveBind = this.resizeDragMove.bind(this);
|
|
this.resizeDragEndBind = this.resizeDragEnd.bind(this);
|
|
document.documentElement.addEventListener('mousemove', this.resizeDragMoveBind, false);
|
|
document.documentElement.addEventListener('mouseup', this.resizeDragEndBind, false);
|
|
}
|
|
|
|
resizeDragMove(e: MouseEvent) {
|
|
let width = this.resizeStartWidth + e.clientX - this.resizeStartX;
|
|
if (width < MIN_SIDEBAR_WIDTH) {
|
|
width = MIN_SIDEBAR_WIDTH;
|
|
}
|
|
this.passesColumn.get()[0].style.width = width + 'px';
|
|
this.state.sidebarWidth = width;
|
|
this.resize();
|
|
}
|
|
|
|
resizeDragEnd(e: MouseEvent) {
|
|
document.documentElement.removeEventListener('mousemove', this.resizeDragMoveBind, false);
|
|
document.documentElement.removeEventListener('mouseup', this.resizeDragEndBind, false);
|
|
}
|
|
|
|
emitOptions(force = false) {
|
|
const options = this.options.get();
|
|
const filters = this.filters.get();
|
|
// TODO: Make use of filter-inconsequential-passes on the back end? Maybe provide a specific function arg to
|
|
// the backend? Would be a data transfer optimization.
|
|
const newOptions: LLVMOptPipelineBackendOptions = {
|
|
//'filter-inconsequential-passes': options['filter-inconsequential-passes'],
|
|
filterDebugInfo: filters['filter-debug-info'],
|
|
filterIRMetadata: filters['filter-instruction-metadata'],
|
|
fullModule: options['dump-full-module'],
|
|
noDiscardValueNames: options['-fno-discard-value-names'],
|
|
demangle: options['demangle-symbols'],
|
|
libraryFunctions: options['library-functions'],
|
|
};
|
|
let changed = false;
|
|
for (const k in newOptions) {
|
|
if (newOptions[k] !== this.lastOptions[k]) {
|
|
changed = true;
|
|
}
|
|
}
|
|
this.lastOptions = newOptions;
|
|
if (changed || force) {
|
|
this.eventHub.emit('llvmOptPipelineViewOptionsUpdated', this.compilerInfo.compilerId, newOptions, true);
|
|
}
|
|
}
|
|
|
|
onOptionsChange() {
|
|
// Redo pass sidebar
|
|
this.selectFunction(this.state.selectedFunction);
|
|
// Inform compiler of the options
|
|
this.emitOptions();
|
|
}
|
|
|
|
override onCompileResult(compilerId: number, compiler: CompilerInfo, result: CompilationResult): void {
|
|
if (this.compilerInfo.compilerId !== compilerId) return;
|
|
if (result.hasLLVMOptPipelineOutput) {
|
|
const output: LLVMOptPipelineOutput = unwrap(result.llvmOptPipelineOutput);
|
|
if (output.error) {
|
|
this.editor
|
|
.getModel()
|
|
?.original.setValue(
|
|
`<An error occurred while generating the optimization pipeline output: ${output.error}>`,
|
|
);
|
|
this.editor.getModel()?.modified.setValue('');
|
|
}
|
|
this.updateResults(output.results);
|
|
} else if (compiler.supportsLLVMOptPipelineView) {
|
|
this.updateResults({});
|
|
this.editor.getModel()?.original.setValue('<Error>');
|
|
this.editor.getModel()?.modified.setValue('');
|
|
}
|
|
}
|
|
|
|
override onCompiler(
|
|
compilerId: number,
|
|
compiler: CompilerInfo | null,
|
|
options: string,
|
|
editorId: number,
|
|
treeId: number,
|
|
): void {
|
|
if (this.compilerInfo.compilerId !== compilerId) return;
|
|
this.compilerInfo.compilerName = compiler ? compiler.name : '';
|
|
this.compilerInfo.editorId = editorId;
|
|
this.compilerInfo.treeId = treeId;
|
|
this.updateTitle();
|
|
if (compiler && !compiler.supportsLLVMOptPipelineView) {
|
|
//this.editor.setValue('<LLVM IR output is not supported for this compiler>');
|
|
}
|
|
}
|
|
|
|
updateResults(results: LLVMOptPipelineResults): void {
|
|
this.results = results;
|
|
//const functions = Object.keys(result);
|
|
let selectedFunction = this.state.selectedFunction; // one of the .clear calls below will end up resetting this
|
|
this.functionSelector.clear();
|
|
this.functionSelector.clearOptions();
|
|
const keys = Object.keys(results);
|
|
if (keys.length === 0) {
|
|
this.functionSelector.addOption({
|
|
title: '<No functions available>',
|
|
value: '<No functions available>',
|
|
});
|
|
}
|
|
for (const fn of keys) {
|
|
this.functionSelector.addOption({
|
|
title: fn,
|
|
value: fn,
|
|
});
|
|
}
|
|
this.passesList.empty();
|
|
if (keys.length > 0) {
|
|
if (selectedFunction === '' || !(selectedFunction in results)) {
|
|
selectedFunction = keys[0];
|
|
}
|
|
this.functionSelector.setValue(selectedFunction);
|
|
} else {
|
|
// restore this.selectedFunction, next time the compilation results aren't errors the selected function will
|
|
// still be the same
|
|
this.state.selectedFunction = selectedFunction;
|
|
}
|
|
}
|
|
|
|
selectFunction(name: string) {
|
|
this.state.selectedFunction = name;
|
|
if (!(name in this.results)) {
|
|
return;
|
|
}
|
|
const filterInconsequentialPasses = this.filters.get()['filter-inconsequential-passes'];
|
|
const passes = this.results[name];
|
|
this.passesList.empty();
|
|
let isFirstMachinePass = true;
|
|
for (const [i, pass] of passes.entries()) {
|
|
if (filterInconsequentialPasses && !pass.irChanged) {
|
|
continue;
|
|
}
|
|
let className = pass.irChanged ? 'changed' : '';
|
|
if (pass.machine && isFirstMachinePass) {
|
|
className += ' firstMachinePass';
|
|
isFirstMachinePass = false;
|
|
}
|
|
this.passesList.append(`<div data-i="${i}" class="pass ${className}">${escapeHTML(pass.name)}</div>`);
|
|
}
|
|
const passDivs = this.passesList.find('.pass');
|
|
passDivs.on('click', e => {
|
|
const target = e.target;
|
|
this.passesList.find('.active').removeClass('active');
|
|
$(target).addClass('active');
|
|
this.displayPass(parseInt(unwrap(target.getAttribute('data-i'))));
|
|
});
|
|
// try to select a pass
|
|
if (this.state.selectedIndex >= passes.length) {
|
|
this.state.selectedIndex = 0;
|
|
}
|
|
const selectedPassDiv = this.passesList.find(`[data-i=${this.state.selectedIndex}]`);
|
|
selectedPassDiv.addClass('active');
|
|
// displayPass updates state
|
|
this.displayPass(this.state.selectedIndex);
|
|
// if loading from a url center the active pass
|
|
if (this.firstResults) {
|
|
this.firstResults = false;
|
|
const activePass = this.passesList.find('.active').get(0);
|
|
if (activePass) {
|
|
scrollIntoView(activePass, {
|
|
scrollMode: 'if-needed',
|
|
block: 'center',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
displayPass(i: number) {
|
|
if (this.state.selectedFunction in this.results && i < this.results[this.state.selectedFunction].length) {
|
|
this.state.selectedIndex = i;
|
|
const pass = this.results[this.state.selectedFunction][i];
|
|
const before = pass.before.map(x => x.text).join('\n');
|
|
const after = pass.after.map(x => x.text).join('\n');
|
|
this.editor.getModel()?.original.setValue(before);
|
|
this.editor.getModel()?.modified.setValue(after);
|
|
this.updateState();
|
|
}
|
|
}
|
|
|
|
onClickCallback(e: JQuery.ClickEvent) {
|
|
this.isPassListSelected = unwrap(this.passesList.get(0)).contains(e.target);
|
|
}
|
|
|
|
onKeydownCallback(e: JQuery.KeyDownEvent) {
|
|
if (this.isPassListSelected) {
|
|
if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
const active = this.passesList.find('.active');
|
|
const prev = active.prev().get(0);
|
|
if (prev) {
|
|
active.removeClass('active');
|
|
$(prev).addClass('active');
|
|
scrollIntoView(prev, {
|
|
scrollMode: 'if-needed',
|
|
block: 'nearest',
|
|
});
|
|
this.displayPass(parseInt(unwrap(prev.getAttribute('data-i'))));
|
|
}
|
|
}
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
const active = this.passesList.find('.active');
|
|
const next = active.next().get(0);
|
|
if (next) {
|
|
active.removeClass('active');
|
|
$(next).addClass('active');
|
|
scrollIntoView(next, {
|
|
scrollMode: 'if-needed',
|
|
block: 'nearest',
|
|
});
|
|
this.displayPass(parseInt(unwrap(next.getAttribute('data-i'))));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override getCurrentState() {
|
|
return {
|
|
...this.options.get(),
|
|
...this.filters.get(),
|
|
...super.getCurrentState(),
|
|
selectedFunction: this.state.selectedFunction,
|
|
selectedIndex: this.state.selectedIndex,
|
|
sidebarWidth: this.state.sidebarWidth,
|
|
};
|
|
}
|
|
|
|
override resize() {
|
|
_.defer(() => {
|
|
const topBarHeight = utils.updateAndCalcTopBarHeight(this.domRoot, this.topBar, this.hideable);
|
|
const otherWidth = unwrap(this.passesColumn.width()) + unwrap(this.passesColumnResizer.width());
|
|
const domWidth = unwrap(this.domRoot.width());
|
|
if (otherWidth > domWidth) {
|
|
this.passesColumn.get()[0].style.width = domWidth + 'px';
|
|
}
|
|
this.editor.layout({
|
|
width: domWidth - otherWidth,
|
|
height: unwrap(this.domRoot.height()) - topBarHeight,
|
|
});
|
|
unwrap(this.body.get(0)).style.height = unwrap(this.domRoot.height()) - topBarHeight + 'px';
|
|
});
|
|
}
|
|
|
|
override close(): void {
|
|
$(document).off('click', this.clickCallback);
|
|
$(document).off('keydown', this.keydownCallback);
|
|
this.eventHub.unsubscribe();
|
|
this.eventHub.emit('llvmOptPipelineViewClosed', this.compilerInfo.compilerId);
|
|
this.editor.dispose();
|
|
}
|
|
}
|