hardhat-gas-reporter
Version:
Gas Analytics plugin for Hardhat
456 lines (389 loc) • 14.4 kB
text/typescript
import chalk, {Chalk} from "chalk";
import _ from "lodash";
import Table, { HorizontalTableRow } from "cli-table3";
import { commify } from "@ethersproject/units";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { UNICODE_CIRCLE, UNICODE_TRIANGLE } from "../../constants";
import { GasData } from "../gasData";
import {
getCommonTableVals,
indentText,
indentTextWithSymbol,
costIsBelowPrecision,
renderWithGasDelta
} from "../../utils/ui";
import { GasReporterOptions, MethodDataItem } from "../../types";
interface Section {row: HorizontalTableRow, contractName: string, methodName: string}
/**
* Generates a gas statistics text table formatted for terminal or file.
* Based on Alan Lu's (github.com/cag) stats for Gnosis
* @param {HardhatRuntimeEnvironment} hre
* @param {GasData} data
* @param {GasReporterOptions} options
* @param {string} string
*/
export function generateTerminalTextTable(
hre: HardhatRuntimeEnvironment,
data: GasData,
options: GasReporterOptions,
toolchain: string
): string {
// Default cols (without L2)
let numberOfCols = 7;
let blockLimitColumnWidth = 2;
let deploymentsTitleSpacerWidth = 2;
let contractTitleSpacerWidth = 5;
let executionGasAverageTitle = "Avg"
let calldataGasAverageTitle = "";
let optionalColor: Chalk;
if (options.noColors || options.outputFile !== undefined) {
chalk.level = 0;
} else {
chalk.level = 1;
}
if (options.darkMode) {
optionalColor = chalk.cyan;
} else {
optionalColor = chalk.bold;
}
if (options.L2 !== undefined) {
numberOfCols = 8;
blockLimitColumnWidth = 3;
deploymentsTitleSpacerWidth = 3;
contractTitleSpacerWidth = 6;
executionGasAverageTitle = "L2 Avg (Exec)";
if (options.L2 === "optimism" || options.L2 === "base") {
calldataGasAverageTitle = "L1 Avg (Data)";
}
if (options.L2 === "arbitrum") {
calldataGasAverageTitle = "L1 Avg (Bytes)";
}
}
// eslint-disable-next-line
const emptyRow = [{ colSpan: numberOfCols, content:"" }]
// ---------------------------------------------------------------------------------------------
// Methods: section assembly
// ---------------------------------------------------------------------------------------------
const methodRows: Section[] = [];
const addedContracts: any[] = [];
_.forEach(data.methods, (method: MethodDataItem) => {
if (!method) return;
// Contracts name row
if (!addedContracts.includes(method.contract) && method.gasData.length > 0) {
addedContracts.push(method.contract);
const contractNameSection: Section = {
row: [
{ hAlign: "left", colSpan: 2, content: `${optionalColor(method.contract)}` },
{ hAlign: "left", colSpan: contractTitleSpacerWidth, content: "" }
],
contractName: method.contract,
methodName: "0"
};
methodRows.push(contractNameSection)
}
const stats: any = {};
if (method.gasData.length > 0) {
stats.executionGasAverage = commify(method.executionGasAverage!);
stats.cost = (method.cost === undefined) ? chalk.grey("-") : method.cost;
// Also writes dash when average is zero
stats.calldataGasAverage = (method.calldataGasAverage)
? commify(method.calldataGasAverage)
: chalk.grey("-");
if (options.trackGasDeltas) {
stats.executionGasAverage = renderWithGasDelta(stats.executionGasAverage, method.executionGasAverageDelta || 0, true);
stats.calldataGasAverage = renderWithGasDelta(stats.calldataGasAverage, method.calldataGasAverageDelta || 0, true);
}
} else {
stats.executionGasAverage = chalk.grey("-");
stats.cost = chalk.grey("-");
}
if (costIsBelowPrecision(stats.cost, options)){
stats.cost = chalk.magenta.bold(UNICODE_TRIANGLE);
}
if (method.min && method.max) {
const uniform = (method.min === method.max);
let min = chalk.cyan(commify(method.min!));
let max = chalk.red(commify(method.max!));
if (options.trackGasDeltas) {
min = renderWithGasDelta(min, method.minDelta || 0, true);
max = renderWithGasDelta(max, method.maxDelta || 0, true);
}
stats.min = uniform ? chalk.grey("-") : min;
stats.max = uniform ? chalk.grey("-") : max;
}
const fnName = options.showMethodSig ? method.fnSig : method.method;
const indented = (method.isCall)
? indentTextWithSymbol(fnName, chalk.magenta.bold(UNICODE_CIRCLE))
: indentText(fnName);
if (options.showUncalledMethods || method.numberOfCalls > 0) {
const row: HorizontalTableRow = [];
row.push({ hAlign: "left", colSpan: 2, content: indented });
row.push({ hAlign: "right", colSpan: 1, content: stats.min });
row.push({ hAlign: "right", colSpan: 1, content: stats.max });
row.push({ hAlign: "right", colSpan: 1, content: stats.executionGasAverage });
if (options.L2 !== undefined) {
row.push({ hAlign: "right", colSpan: 1, content: stats.calldataGasAverage });
}
row.push({ hAlign: "right", colSpan: 1, content: method.numberOfCalls });
row.push({
hAlign: "right",
colSpan: 1,
content: chalk.green(stats.cost.toString())
});
const section: Section = {
row,
contractName: method.contract,
methodName: fnName
};
methodRows.push(section);
}
});
// ---------------------------------------------------------------------------------------------
// Deployments: section assembly
// ---------------------------------------------------------------------------------------------
const deployRows: any = [];
// Alphabetize contract names
data.deployments.sort((a, b) => a.name.localeCompare(b.name));
data.deployments.forEach(deployment => {
const stats: any = {};
if (deployment.gasData.length === 0) return;
stats.cost = (deployment.cost === undefined) ? chalk.grey("-") : deployment.cost;
if (costIsBelowPrecision(stats.cost, options)){
stats.cost = chalk.magenta.bold(UNICODE_TRIANGLE);
}
stats.executionGasAverage = commify(deployment.executionGasAverage!);
stats.calldataGasAverage = (deployment.calldataGasAverage === undefined )
? ""
: commify(deployment.calldataGasAverage);
if (options.trackGasDeltas) {
stats.executionGasAverage = renderWithGasDelta(stats.executionGasAverage, deployment.executionGasAverageDelta || 0, true);
stats.calldataGasAverage = renderWithGasDelta(stats.calldataGasAverage, deployment.calldataGasAverageDelta || 0, true);
}
if (deployment.min && deployment.max) {
const uniform = (deployment.min === deployment.max);
let min = chalk.cyan(commify(deployment.min!));
let max = chalk.red(commify(deployment.max!));
if (options.trackGasDeltas) {
min = renderWithGasDelta(min, deployment.minDelta || 0, true);
max = renderWithGasDelta(max, deployment.maxDelta || 0, true);
}
stats.min = uniform ? chalk.grey("-") : min;
stats.max = uniform ? chalk.grey("-") : max;
}
const section: any = [];
section.push({ hAlign: "left", colSpan: 2, content: chalk.bold(deployment.name) });
section.push({ hAlign: "right", colSpan: 1, content: stats.min });
section.push({ hAlign: "right", colSpan: 1, content: stats.max });
section.push({ hAlign: "right", colSpan: 1, content: stats.executionGasAverage! });
if (options.L2 !== undefined) {
section.push({ hAlign: "right", colSpan: 1, content: stats.calldataGasAverage! })
}
section.push({
hAlign: "right",
colSpan: 1,
content: `${deployment.percent!} %`
});
section.push({
hAlign: "right",
colSpan: 1,
content: chalk.green(stats.cost.toString())
});
deployRows.push(section);
});
// ---------------------------------------------------------------------------------------------
// Headers: section assembly
// ---------------------------------------------------------------------------------------------
// Configure indentation for RTD
const leftPad = options.rst ? " " : "";
// Format table
const table = new Table({
style: { head: [], border: [], "padding-left": 2, "padding-right": 2 },
colWidths: [numberOfCols],
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": "|"
}
});
const title: HorizontalTableRow = [
{
hAlign: "left",
colSpan: numberOfCols,
content: chalk.green.bold(`Solidity and Network Configuration`)
}
];
// ============
// SOLC CONFIG
// ============
// Format and load methods metrics
const solcConfig: HorizontalTableRow = [
{
hAlign: "left",
colSpan: 2,
content: chalk.cyan(`Solidity: ${options.solcInfo.version}`)
},
{
hAlign: "left",
colSpan: 1,
content: chalk.cyan(`Optim: ${options.solcInfo.optimizer}`)
},
{
hAlign: "left",
colSpan: 1,
content: chalk.cyan(`Runs: ${options.solcInfo.runs}`)
},
{
hAlign: "left",
colSpan: 1,
content: chalk.cyan(`viaIR: ${options.solcInfo.viaIR.toString()}`)
},
{
hAlign: "center",
colSpan: blockLimitColumnWidth,
content: chalk.cyan(`Block: ${commify(options.blockGasLimit!)} gas`)
}
];
// ==============
// NETWORK CONFIG
// ==============
let networkConfig: HorizontalTableRow = [];
const opStackConfig: HorizontalTableRow = [];
if (options.tokenPrice && options.gasPrice) {
const {
l1gwei,
l2gwei,
l1gweiNote,
l2gweiNote,
l1GweiBlobBaseFee,
network,
rate,
currency,
token
} = getCommonTableVals(options);
networkConfig.push({
hAlign: "left",
colSpan: 2,
content: chalk.cyan(`Network: ${network}`)
});
networkConfig.push({
hAlign: "left",
colSpan: 2,
content: chalk.cyan(`L1: ${l1gwei} gwei ${l1gweiNote}`)
});
if (options.L2 !== undefined) {
networkConfig.push({
hAlign: "left",
colSpan: 2,
content: chalk.cyan(`L2: ${l2gwei} gwei ${l2gweiNote}`)
});
} else {
networkConfig.push({ colSpan: 1, content: " " })
}
networkConfig.push({
hAlign: "center",
colSpan: 2,
content: chalk.magenta(`${rate} ${currency}/${token}`)
});
if (options.L2 === "optimism" || options.L2 === "base") {
opStackConfig.push({ colSpan: 2, content: " " })
opStackConfig.push({
hAlign: "left",
colSpan: 2,
content: chalk.cyan(`L1: ${l1GweiBlobBaseFee!} gwei (blobBaseFee)`)
});
opStackConfig.push({
hAlign: "left",
colSpan: 2,
content: chalk.cyan(`Base Fee Scalar: ${options.opStackBaseFeeScalar!}`)
});
opStackConfig.push({
hAlign: "left",
colSpan: 2,
content: chalk.cyan(`Blob Fee Scalar: ${options.opStackBlobBaseFeeScalar!}`)
});
}
} else {
networkConfig = [
{ hAlign: "left", colSpan: numberOfCols, content: chalk.green.bold("Methods") }
];
}
// ===============
// METHODS HEADER
// ===============
const methodsHeader: HorizontalTableRow = [];
methodsHeader.push({ hAlign: "left", colSpan: 2, content: chalk.green.bold("Contracts / Methods") });
methodsHeader.push({ hAlign: "left", colSpan: 1, content: chalk.bold("Min") });
methodsHeader.push({ hAlign: "left", colSpan: 1, content: chalk.bold("Max") });
methodsHeader.push({ hAlign: "left", colSpan: 1, content: chalk.bold(executionGasAverageTitle) })
if (options.L2 !== undefined) {
methodsHeader.push({ hAlign: "left", colSpan: 1, content: chalk.bold(calldataGasAverageTitle) })
}
methodsHeader.push({ hAlign: "left", colSpan: 1, content: chalk.bold("# calls") });
methodsHeader.push({ hAlign: "left", colSpan: 1, content: chalk.bold(`${options.currency!.toLowerCase()} (avg)`) });
// ===============
// SYMBOL KEY
// ===============
const {
intrinsicMsg,
nonZeroMsg
} = getCommonTableVals(options);
const keyTitle: HorizontalTableRow = [{
hAlign: "left", colSpan: numberOfCols, content: chalk.green.bold("Key")
}];
const keyCall: HorizontalTableRow =[{
hAlign: "left", colSpan: numberOfCols, content: `${chalk.magenta.bold(UNICODE_CIRCLE)} ${intrinsicMsg}`
}];
const keyAir: HorizontalTableRow = [{
hAlign: "left", colSpan: numberOfCols, content: `${chalk.magenta.bold(UNICODE_TRIANGLE)} ${nonZeroMsg}`
}];
const keyToolchain: HorizontalTableRow = [{
hAlign: "left", colSpan: numberOfCols, content: `${chalk.magenta("Toolchain:")} ${toolchain}`
}];
// ---------------------------------------------------------------------------------------------
// Final table assembly
// ---------------------------------------------------------------------------------------------
table.push(title);
table.push(solcConfig);
table.push(networkConfig);
if (opStackConfig.length > 0) {
table.push(opStackConfig);
}
table.push(methodsHeader);
methodRows.sort((a, b) => {
const contractName = a.contractName.localeCompare(b.contractName);
const methodName = a.methodName.localeCompare(b.methodName);
return contractName || methodName;
});
const rows = methodRows.map(val => val.row);
rows.forEach(row => table.push(row));
if (deployRows.length) {
const deploymentsSubtitle = [
{
hAlign: "left",
colSpan: 3,
content: chalk.green.bold("Deployments")
},
{ hAlign: "right", colSpan: deploymentsTitleSpacerWidth, content: "" },
{ hAlign: "left", colSpan: 1, content: chalk.bold(`% of limit`) },
{ hAlign: "left", colSpan: 1, content: "" }
];
table.push(deploymentsSubtitle as HorizontalTableRow);
deployRows.forEach((row: any) => table.push(row));
}
table.push(keyTitle);
table.push(keyCall);
table.push(keyAir);
table.push(keyToolchain);
return table.toString();
}