UNPKG

@g4g/eth-gas-reporter

Version:

Mocha reporter which shows gas used per unit test.

291 lines (249 loc) 9.43 kB
const colors = require("colors/safe"); const _ = require("lodash"); const fs = require("fs"); const Table = require("cli-table3"); const utils = require("./utils"); const CodeChecksReport = require("./codechecksReport"); class GasTable { constructor(config) { this.config = config; } /** * Formats and prints a gas statistics table. Optionally writes to a file. * Based on Alan Lu's (github.com/@cag) stats for Gnosis * @param {Object} info GasData instance with `methods` and `deployments` data */ generate(info) { colors.enabled = !this.config.noColors || false; // --------------------------------------------------------------------------------------------- // Assemble section: methods // --------------------------------------------------------------------------------------------- const methodRows = []; _.forEach(info.methods, (data, methodId) => { if (!data) return; let stats = {}; if (data.gasData.length) { const total = data.gasData.reduce((acc, datum) => acc + datum, 0); stats.average = Math.round(total / data.gasData.length); stats.cost = this.config.ethPrice && this.config.gasPrice ? utils.gasToCost( stats.average, this.config.ethPrice, this.config.gasPrice ) : colors.grey("-"); } else { stats.average = colors.grey("-"); stats.cost = colors.grey("-"); } const sortedData = data.gasData.sort((a, b) => a - b); stats.min = sortedData[0]; stats.max = sortedData[sortedData.length - 1]; const uniform = stats.min === stats.max; stats.min = uniform ? "-" : colors.cyan(stats.min.toString()); stats.max = uniform ? "-" : colors.red(stats.max.toString()); stats.numberOfCalls = colors.grey(data.numberOfCalls.toString()); const fnName = this.config.showMethodSig ? data.fnSig : data.method; if (!this.config.onlyCalledMethods || data.numberOfCalls > 0) { const section = []; section.push(colors.grey(data.contract)); section.push(fnName); section.push({ hAlign: "right", content: stats.min }); section.push({ hAlign: "right", content: stats.max }); section.push({ hAlign: "right", content: stats.average }); section.push({ hAlign: "right", content: stats.numberOfCalls }); section.push({ hAlign: "right", content: colors.green(stats.cost.toString()) }); methodRows.push(section); } }); // --------------------------------------------------------------------------------------------- // Assemble section: deployments // --------------------------------------------------------------------------------------------- const deployRows = []; // Alphabetize contract names info.deployments.sort((a, b) => a.name.localeCompare(b.name)); info.deployments.forEach(contract => { let stats = {}; if (!contract.gasData.length) return; const total = contract.gasData.reduce((acc, datum) => acc + datum, 0); stats.average = Math.round(total / contract.gasData.length); stats.percent = utils.gasToPercentOfLimit(stats.average, info.blockLimit); stats.cost = this.config.ethPrice && this.config.gasPrice ? utils.gasToCost( stats.average, this.config.ethPrice, this.config.gasPrice ) : colors.grey("-"); const sortedData = contract.gasData.sort((a, b) => a - b); stats.min = sortedData[0]; stats.max = sortedData[sortedData.length - 1]; const uniform = stats.min === stats.max; stats.min = uniform ? "-" : colors.cyan(stats.min.toString()); stats.max = uniform ? "-" : colors.red(stats.max.toString()); const section = []; section.push({ hAlign: "left", colSpan: 2, content: contract.name }); section.push({ hAlign: "right", content: stats.min }); section.push({ hAlign: "right", content: stats.max }); section.push({ hAlign: "right", content: stats.average }); section.push({ hAlign: "right", content: colors.grey(`${stats.percent} %`) }); section.push({ hAlign: "right", content: colors.green(stats.cost.toString()) }); deployRows.push(section); }); // --------------------------------------------------------------------------------------------- // Assemble section: headers // --------------------------------------------------------------------------------------------- // Configure indentation for RTD const leftPad = this.config.rst ? " " : ""; // Format table const table = new Table({ style: { head: [], border: [], "padding-left": 2, "padding-right": 2 }, chars: { mid: "·", "top-mid": "|", "left-mid": `${leftPad}·`, "mid-mid": "|", "right-mid": "·", left: `${leftPad}|`, "top-left": `${leftPad}·`, "top-right": "·", "bottom-left": `${leftPad}·`, "bottom-right": "·", middle: "·", top: "-", bottom: "-", "bottom-mid": "|" } }); // Format and load methods metrics const solc = utils.getSolcInfo(this.config.metadata); let title = [ { hAlign: "center", colSpan: 2, content: colors.grey(`Solc version: ${solc.version}`) }, { hAlign: "center", colSpan: 2, content: colors.grey(`Optimizer enabled: ${solc.optimizer}`) }, { hAlign: "center", colSpan: 1, content: colors.grey(`Runs: ${solc.runs}`) }, { hAlign: "center", colSpan: 2, content: colors.grey(`Block limit: ${info.blockLimit} gas`) } ]; let methodSubtitle; if (this.config.ethPrice && this.config.gasPrice) { const gwei = parseInt(this.config.gasPrice); const rate = parseFloat(this.config.ethPrice).toFixed(2); const currency = `${this.config.currency.toLowerCase()}`; methodSubtitle = [ { hAlign: "left", colSpan: 2, content: colors.green.bold("Methods") }, { hAlign: "center", colSpan: 3, content: colors.grey(`${gwei} gwei/gas`) }, { hAlign: "center", colSpan: 2, content: colors.red(`${rate} ${currency}/eth`) } ]; } else { methodSubtitle = [ { hAlign: "left", colSpan: 7, content: colors.green.bold("Methods") } ]; } const header = [ colors.bold("Contract"), colors.bold("Method"), colors.green("Min"), colors.green("Max"), colors.green("Avg"), colors.bold("# calls"), colors.bold(`${this.config.currency.toLowerCase()} (avg)`) ]; // --------------------------------------------------------------------------------------------- // Final assembly // --------------------------------------------------------------------------------------------- table.push(title); table.push(methodSubtitle); table.push(header); methodRows.sort((a, b) => { const contractName = a[0].localeCompare(b[0]); const methodName = a[1].localeCompare(b[1]); return contractName || methodName; }); methodRows.forEach(row => table.push(row)); if (deployRows.length) { const deploymentsSubtitle = [ { hAlign: "left", colSpan: 2, content: colors.green.bold("Deployments") }, { hAlign: "right", colSpan: 3, content: "" }, { hAlign: "left", colSpan: 1, content: colors.bold(`% of limit`) } ]; table.push(deploymentsSubtitle); deployRows.forEach(row => table.push(row)); } // --------------------------------------------------------------------------------------------- // RST / ReadTheDocs / Sphinx output // --------------------------------------------------------------------------------------------- let rstOutput = ""; if (this.config.rst) { rstOutput += `${this.config.rstTitle}\n`; rstOutput += `${"=".repeat(this.config.rstTitle.length)}\n\n`; rstOutput += `.. code-block:: shell\n\n`; } let tableOutput = rstOutput + table.toString(); // --------------------------------------------------------------------------------------------- // Print // --------------------------------------------------------------------------------------------- this.config.outputFile ? fs.writeFileSync(this.config.outputFile, tableOutput) : console.log(tableOutput); this.saveCodeChecksData(info); // For integration tests if (process.env.DEBUG_CODECHECKS_TABLE) { const report = new CodeChecksReport(this.config); console.log(report.generate(info)); } } /** * Writes acccumulated data and the current config to gasReporterOutput.json so it * can be consumed by codechecks * @param {Object} info GasData instance */ saveCodeChecksData(info) { const output = { namespace: "ethGasReporter", config: this.config, info: info }; if (process.env.CI) { fs.writeFileSync("./gasReporterOutput.json", JSON.stringify(output)); } } } module.exports = GasTable;