diff --git a/mix/CodeModel.cpp b/mix/CodeModel.cpp index 5c6ec07c0..b0edb20f9 100644 --- a/mix/CodeModel.cpp +++ b/mix/CodeModel.cpp @@ -33,6 +33,8 @@ #include #include #include +#include +#include #include #include #include "QContractDefinition.h" @@ -50,6 +52,7 @@ const std::set c_predefinedContracts = namespace { +using namespace dev::eth; using namespace dev::solidity; class CollectLocalsVisitor: public ASTConstVisitor @@ -305,6 +308,7 @@ void CodeModel::runCompilationJob(int _jobId) } } cs.compile(false); + gasEstimation(cs); collectContracts(cs, sourceNames); } catch (dev::Exception const& _exception) @@ -327,6 +331,44 @@ void CodeModel::runCompilationJob(int _jobId) emit stateChanged(); } +void CodeModel::gasEstimation(solidity::CompilerStack const& _cs) +{ + if (m_gasCostsMaps) + m_gasCostsMaps->deleteLater(); + m_gasCostsMaps = new GasMapWrapper(this); + for (std::string n: _cs.getContractNames()) + { + ContractDefinition const& contractDefinition = _cs.getContractDefinition(n); + QString sourceName = QString::fromStdString(*contractDefinition.getLocation().sourceName); + + if (!m_gasCostsMaps->contains(sourceName)) + m_gasCostsMaps->insert(sourceName, QVariantList()); + + if (!contractDefinition.isFullyImplemented()) + continue; + dev::solidity::SourceUnit const& sourceUnit = _cs.getAST(*contractDefinition.getLocation().sourceName); + AssemblyItems const* items = _cs.getRuntimeAssemblyItems(n); + StructuralGasEstimator estimator; + std::map gasCosts = estimator.breakToStatementLevel(estimator.performEstimation(*items, std::vector({&sourceUnit})), {&sourceUnit}); + for (auto gasItem = gasCosts.begin(); gasItem != gasCosts.end(); ++gasItem) + { + SourceLocation const& location = gasItem->first->getLocation(); + GasMeter::GasConsumption cost = gasItem->second; + std::stringstream v; + v << cost.value; + m_gasCostsMaps->push(sourceName, location.start, location.end, QString::fromStdString(v.str()), cost.isInfinite); + } + } +} + +QVariantList CodeModel::gasCostByDocumentId(QString const& _documentId) const +{ + if (m_gasCostsMaps) + return m_gasCostsMaps->gasCostsByDocId(_documentId); + else + return QVariantList(); +} + void CodeModel::collectContracts(dev::solidity::CompilerStack const& _cs, std::vector const& _sourceNames) { Guard pl(x_pendingContracts); @@ -516,3 +558,29 @@ QString CodeModel::resolveFunctionName(dev::SourceLocation const& _location) } return QString(); } + +void GasMapWrapper::push(QString _source, int _start, int _end, QString _value, bool _isInfinite) +{ + GasMap* gas = new GasMap(_start, _end, _value, _isInfinite, this); + m_gasMaps.find(_source).value().push_back(QVariant::fromValue(gas)); +} + +bool GasMapWrapper::contains(QString _key) +{ + return m_gasMaps.contains(_key); +} + +void GasMapWrapper::insert(QString _source, QVariantList _variantList) +{ + m_gasMaps.insert(_source, _variantList); +} + +QVariantList GasMapWrapper::gasCostsByDocId(QString _source) +{ + auto gasIter = m_gasMaps.find(_source); + if (gasIter != m_gasMaps.end()) + return gasIter.value(); + else + return QVariantList(); +} + diff --git a/mix/CodeModel.h b/mix/CodeModel.h index a0b03951f..994e9936a 100644 --- a/mix/CodeModel.h +++ b/mix/CodeModel.h @@ -32,6 +32,7 @@ #include #include #include "SolidityType.h" +#include "QBigInt.h" class QTextDocument; @@ -127,6 +128,42 @@ struct SourceMap }; using SourceMaps = QMap; //by source id +using GasCostsMaps = QMap; //gas cost by contract name + +class GasMapWrapper: public QObject +{ + Q_OBJECT + + Q_PROPERTY(GasCostsMaps gasMaps MEMBER m_gasMaps CONSTANT) + +public: + GasMapWrapper(QObject* _parent): QObject(_parent){} + void push(QString _source, int _start, int _end, QString _value, bool _isInfinite); + bool contains(QString _key); + void insert(QString _source, QVariantList _variantList); + QVariantList gasCostsByDocId(QString _source); + +private: + GasCostsMaps m_gasMaps; +}; + +class GasMap: public QObject +{ + Q_OBJECT + + Q_PROPERTY(int start MEMBER m_start CONSTANT) + Q_PROPERTY(int end MEMBER m_end CONSTANT) + Q_PROPERTY(QString gas MEMBER m_gas CONSTANT) + Q_PROPERTY(bool isInfinite MEMBER m_isInfinite CONSTANT) + +public: + GasMap(int _start, int _end, QString _gas, bool _isInfinite, QObject* _parent): QObject(_parent), m_start(_start), m_end(_end), m_gas(_gas), m_isInfinite(_isInfinite) {} + + int m_start; + int m_end; + QString m_gas; + bool m_isInfinite; +}; /// Code compilation model. Compiles contracts in background an provides compiled contract data class CodeModel: public QObject @@ -168,6 +205,10 @@ public: bool isContractOrFunctionLocation(dev::SourceLocation const& _location); /// Get funciton name by location QString resolveFunctionName(dev::SourceLocation const& _location); + /// Gas estimation for compiled sources + void gasEstimation(solidity::CompilerStack const& _cs); + /// Gas cost by doc id + Q_INVOKABLE QVariantList gasCostByDocumentId(QString const& _documentId) const; signals: /// Emited on compilation state change @@ -203,6 +244,7 @@ private: mutable dev::Mutex x_contractMap; ContractMap m_contractMap; SourceMaps m_sourceMaps; + GasMapWrapper* m_gasCostsMaps = 0; std::unique_ptr m_codeHighlighterSettings; QThread m_backgroundThread; BackgroundWorker m_backgroundWorker; @@ -216,3 +258,5 @@ private: } } + +//Q_DECLARE_METATYPE(dev::mix::GasMap) diff --git a/mix/qml/Application.qml b/mix/qml/Application.qml index fe62efe12..161f7141a 100644 --- a/mix/qml/Application.qml +++ b/mix/qml/Application.qml @@ -116,6 +116,10 @@ ApplicationWindow { MenuSeparator {} MenuItem { action: toggleAssemblyDebuggingAction } } + Menu { + title: qsTr("Tools") + MenuItem { action: gasEstimationAction } + } Menu { title: qsTr("Windows") MenuItem { action: openNextDocumentAction } @@ -409,4 +413,15 @@ ApplicationWindow { mainContent.codeEditor.goToCompilationError(); } } + + Action { + id: gasEstimationAction + text: qsTr("Display gas estimation") + shortcut: "Ctrl+G" + checkable: true + onTriggered: + { + mainContent.codeEditor.displayGasEstimation(checked); + } + } } diff --git a/mix/qml/CodeEditorView.qml b/mix/qml/CodeEditorView.qml index bb7e203bf..80449c89a 100644 --- a/mix/qml/CodeEditorView.qml +++ b/mix/qml/CodeEditorView.qml @@ -168,6 +168,13 @@ Item { editors.itemAt(i).item.setFontSize(size); } + function displayGasEstimation(checked) + { + var editor = getEditor(currentDocumentId); + if (editor) + editor.displayGasEstimation(checked); + } + Component.onCompleted: projectModel.codeEditor = codeEditorView; Connections { @@ -177,6 +184,10 @@ Item { } onCompilationComplete: { sourceInError = ""; + var gasCosts = codeModel.gasCostByDocumentId(currentDocumentId); + var editor = getEditor(currentDocumentId); + if (editor) + editor.setGasCosts(gasCosts); } } @@ -280,6 +291,7 @@ Item { messageDialog.doc = editorListModel.get(index); messageDialog.open(); } + loader.item.displayGasEstimation(gasEstimationAction.checked); } } Component.onCompleted: { diff --git a/mix/qml/WebCodeEditor.qml b/mix/qml/WebCodeEditor.qml index 38f2327b1..562c9716b 100644 --- a/mix/qml/WebCodeEditor.qml +++ b/mix/qml/WebCodeEditor.qml @@ -83,6 +83,16 @@ Item { editorBrowser.runJavaScript("setFontSize(" + size + ")", function(result) {}); } + function setGasCosts(gasCosts) { + if (initialized && editorBrowser) + editorBrowser.runJavaScript("setGasCosts('" + JSON.stringify(gasCosts) + "')", function(result) {}); + } + + function displayGasEstimation(show) { + if (initialized && editorBrowser) + editorBrowser.runJavaScript("displayGasEstimation('" + show + "')", function(result) {}); + } + Clipboard { id: clipboard @@ -134,7 +144,12 @@ Item { function compilationComplete() { if (editorBrowser) + { editorBrowser.runJavaScript("compilationComplete()", function(result) { }); + parent.displayGasEstimation(gasEstimationAction.checked); + } + + } function compilationError(error, sourceName) diff --git a/mix/qml/html/cm/solarized.css b/mix/qml/html/cm/solarized.css index b8cede806..1d298b990 100644 --- a/mix/qml/html/cm/solarized.css +++ b/mix/qml/html/cm/solarized.css @@ -189,3 +189,9 @@ view-port } span.CodeMirror-selectedtext { color: #586e75 !important; } + +/* Gas Costs */ +.CodeMirror-gasCosts { + border-bottom: double 1px #2aa198; +} + diff --git a/mix/qml/html/codeeditor.js b/mix/qml/html/codeeditor.js index d25fbd091..6499922ae 100644 --- a/mix/qml/html/codeeditor.js +++ b/mix/qml/html/codeeditor.js @@ -203,5 +203,112 @@ setFontSize = function(size) editor.refresh(); } +makeGasCostMarker = function(value) { + var marker = document.createElement("div"); + marker.style.color = "#822"; + marker.innerHTML = value; + marker.className = "CodeMirror-errorannotation-context"; + return marker; +}; + +var gasCosts = null; +setGasCosts = function(_gasCosts) +{ + gasCosts = JSON.parse(_gasCosts); + if (showingGasEstimation) + { + displayGasEstimation(false); + displayGasEstimation(true); + } +} + +var showingGasEstimation = false; +var gasMarkText = []; +var gasMarkRef = {}; +displayGasEstimation = function(show) +{ + show = JSON.parse(show); + showingGasEstimation = show; + if (show) + { + var maxGas = 20000; + var step = colorGradient.length / maxGas; // 20000 max gas + clearGasMark(); + gasMarkText = []; + gasMarkRef = {}; + for (var i in gasCosts) + { + if (gasCosts[i].gas !== "0") + { + var color; + var colorIndex = Math.round(step * gasCosts[i].gas); + if (gasCosts[i].isInfinite || colorIndex > colorGradient.length) + color = colorGradient[colorGradient.length - 1]; + else + color = colorGradient[colorIndex]; + var className = "CodeMirror-gasCosts" + i; + var line = editor.posFromIndex(gasCosts[i].start) + gasMarkText.push(editor.markText(line, editor.posFromIndex(gasCosts[i].end), { inclusiveLeft: true, inclusiveRight: true, handleMouseEvents: true, className: className, css: "background-color:" + color })); + gasMarkRef[className] = { line: line.line, value: gasCosts[i] }; + } + } + CodeMirror.on(editor.getWrapperElement(), "mouseover", listenMouseOver); + } + else + { + CodeMirror.off(editor.getWrapperElement(), "mouseover", listenMouseOver); + clearGasMark(); + if (gasAnnotation) + { + gasAnnotation.clear(); + gasAnnotation = null; + } + } +} + +function clearGasMark() +{ + if (gasMarkText) + for (var k in gasMarkText) + gasMarkText[k].clear(); +} + +var gasAnnotation; +function listenMouseOver(e) +{ + var node = e.target || e.srcElement; + if (node) + { + if (node.className && node.className.indexOf("CodeMirror-gasCosts") !== -1) + { + if (gasAnnotation) + gasAnnotation.clear(); + var cl = getGasCostClass(node); + var gasTitle = gasMarkRef[cl].value.isInfinite ? "infinite" : gasMarkRef[cl].value.gas; + gasTitle = gasTitle + " gas"; + gasAnnotation = editor.addLineWidget(gasMarkRef[cl].line + 1, makeGasCostMarker(gasTitle), { coverGutter: false, above: true }); + } + else if (gasAnnotation) + { + gasAnnotation.clear(); + gasAnnotation = null; + } + } +} + +function getGasCostClass(node) +{ + var classes = node.className.split(" "); + for (var k in classes) + { + if (classes[k].indexOf("CodeMirror-gasCosts") !== -1) + return classes[k]; + } + return ""; +} + +// blue => red ["#1515ED", "#1714EA", "#1914E8", "#1B14E6", "#1D14E4", "#1F14E2", "#2214E0", "#2414DE", "#2614DC", "#2813DA", "#2A13D8", "#2D13D6", "#2F13D4", "#3113D2", "#3313D0", "#3513CE", "#3713CC", "#3A12CA", "#3C12C8", "#3E12C6", "#4012C4", "#4212C2", "#4512C0", "#4712BE", "#4912BC", "#4B11BA", "#4D11B8", "#4F11B6", "#5211B4", "#5411B2", "#5611B0", "#5811AE", "#5A11AC", "#5D11AA", "#5F10A7", "#6110A5", "#6310A3", "#6510A1", "#67109F", "#6A109D", "#6C109B", "#6E1099", "#700F97", "#720F95", "#750F93", "#770F91", "#790F8F", "#7B0F8D", "#7D0F8B", "#7F0F89", "#820E87", "#840E85", "#860E83", "#880E81", "#8A0E7F", "#8D0E7D", "#8F0E7B", "#910E79", "#930D77", "#950D75", "#970D73", "#9A0D71", "#9C0D6F", "#9E0D6D", "#A00D6B", "#A20D69", "#A50D67", "#A70C64", "#A90C62", "#AB0C60", "#AD0C5E", "#AF0C5C", "#B20C5A", "#B40C58", "#B60C56", "#B80B54", "#BA0B52", "#BD0B50", "#BF0B4E", "#C10B4C", "#C30B4A", "#C50B48", "#C70B46", "#CA0A44", "#CC0A42", "#CE0A40", "#D00A3E", "#D20A3C", "#D50A3A", "#D70A38", "#D90A36", "#DB0934", "#DD0932", "#DF0930", "#E2092E", "#E4092C", "#E6092A", "#E80928", "#EA0926", "#ED0924"] +/* green => red */ var colorGradient = ["#429C27", "#439A26", "#449926", "#469726", "#479626", "#489525", "#4A9325", "#4B9225", "#4D9025", "#4E8F25", "#4F8E24", "#518C24", "#528B24", "#548924", "#558824", "#568723", "#588523", "#598423", "#5B8223", "#5C8122", "#5D8022", "#5F7E22", "#607D22", "#627B22", "#637A21", "#647921", "#667721", "#677621", "#697421", "#6A7320", "#6B7220", "#6D7020", "#6E6F20", "#706E20", "#716C1F", "#726B1F", "#74691F", "#75681F", "#76671E", "#78651E", "#79641E", "#7B621E", "#7C611E", "#7D601D", "#7F5E1D", "#805D1D", "#825B1D", "#835A1D", "#84591C", "#86571C", "#87561C", "#89541C", "#8A531B", "#8B521B", "#8D501B", "#8E4F1B", "#904D1B", "#914C1A", "#924B1A", "#94491A", "#95481A", "#97461A", "#984519", "#994419", "#9B4219", "#9C4119", "#9E4019", "#9F3E18", "#A03D18", "#A23B18", "#A33A18", "#A43917", "#A63717", "#A73617", "#A93417", "#AA3317", "#AB3216", "#AD3016", "#AE2F16", "#B02D16", "#B12C16", "#B22B15", "#B42915", "#B52815", "#B72615", "#B82514", "#B92414", "#BB2214", "#BC2114", "#BE1F14", "#BF1E13", "#C01D13", "#C21B13", "#C31A13", "#C51813", "#C61712", "#C71612", "#C91412", "#CA1312", "#CC1212"] + editor.setOption("extraKeys", extraKeys);