Skip to content

Commit f471d04

Browse files
committed
Add scripts to parse, save, compare and display gas reports from hardhat-gas-reporter
1 parent e4f70f8 commit f471d04

6 files changed

+421
-1
lines changed

hardhat.config.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } from "hardhat/builtin-tasks/task-names";
2-
import { subtask } from "hardhat/config";
2+
import { subtask, task } from "hardhat/config";
3+
4+
import { compareLastTwoReports } from "./scripts/compare_reports";
5+
import { printLastReport } from "./scripts/print_report";
6+
import { getReportPathForCommit } from "./scripts/utils";
7+
import { writeReports } from "./scripts/write_reports";
38

49
import type { HardhatUserConfig } from "hardhat/config";
510

@@ -20,6 +25,24 @@ subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS).setAction(
2025
}
2126
);
2227

28+
task("write-reports", "Write pending gas reports").setAction(
29+
async (taskArgs, hre) => {
30+
writeReports(hre);
31+
}
32+
);
33+
34+
task("compare-reports", "Compare last two gas reports").setAction(
35+
async (taskArgs, hre) => {
36+
compareLastTwoReports(hre);
37+
}
38+
);
39+
40+
task("print-report", "Print the last gas report").setAction(
41+
async (taskArgs, hre) => {
42+
printLastReport(hre);
43+
}
44+
);
45+
2346
const optimizerSettingsNoSpecializer = {
2447
enabled: true,
2548
runs: 20000,
@@ -111,6 +134,8 @@ const config: HardhatUserConfig = {
111134
gasReporter: {
112135
enabled: process.env.REPORT_GAS !== undefined,
113136
currency: "USD",
137+
outputFile: getReportPathForCommit(),
138+
noColors: true,
114139
},
115140
etherscan: {
116141
apiKey: process.env.EXPLORER_API_KEY,

scripts/comment-table.ts

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import chalk from "chalk";
2+
3+
export const err = chalk.bold.red;
4+
export const warn = chalk.hex("#FFA500");
5+
export const info = chalk.blue;
6+
export const success = chalk.green;
7+
8+
export function diffPctString(
9+
newValue: number,
10+
oldValue: number,
11+
warnOnIncrease?: boolean,
12+
diffOnly?: boolean
13+
): string {
14+
if (newValue === null && oldValue === null) {
15+
return warn("null");
16+
}
17+
const diff = newValue - oldValue;
18+
19+
if (diff === 0) return info(newValue.toString());
20+
const pct = +((100 * diff) / oldValue).toFixed(2);
21+
const pctPrefix = pct > 0 ? "+" : "";
22+
const color = diff > 0 ? (warnOnIncrease ? warn : err) : success;
23+
const valuePrefix = diffOnly && diff > 0 ? "+" : "";
24+
const value = diffOnly ? diff : newValue;
25+
return `${valuePrefix}${value} (${color(`${pctPrefix}${pct}%`)})`;
26+
}
27+
// eslint-disable-next-line no-control-regex
28+
const stripANSI = (str: string) => str.replace(/\u001b\[.*?m/g, "");
29+
30+
export function getColumnSizesAndAlignments(
31+
rows: string[][],
32+
padding = 0
33+
): Array<[number, boolean]> {
34+
const sizesAndAlignments: Array<[number, boolean]> = [];
35+
const numColumns = rows[0].length;
36+
for (let i = 0; i < numColumns; i++) {
37+
const entries = rows.map((row) => stripANSI(row[i]));
38+
const maxSize = Math.max(...entries.map((e) => e.length));
39+
const alignLeft = entries
40+
.slice(1)
41+
.filter((e) => !e.includes("null"))
42+
.some((e) => !!e.match(/[a-zA-Z]/g));
43+
sizesAndAlignments.push([maxSize + padding, alignLeft]);
44+
}
45+
return sizesAndAlignments;
46+
}
47+
48+
const padColumn = (
49+
col: string,
50+
size: number,
51+
padWith: string,
52+
alignLeft: boolean
53+
) => {
54+
const padSize = Math.max(0, size - stripANSI(col).length);
55+
const padding = padWith.repeat(padSize);
56+
if (alignLeft) return `${col}${padding}`;
57+
return `${padding}${col}`;
58+
};
59+
60+
export const toCommentTable = (rows: string[][]): string[] => {
61+
const sizesAndAlignments = getColumnSizesAndAlignments(rows);
62+
rows.forEach((row) => {
63+
row.forEach((col, c) => {
64+
const [size, alignLeft] = sizesAndAlignments[c];
65+
row[c] = padColumn(col, size, " ", alignLeft);
66+
});
67+
});
68+
69+
const completeRows = rows.map((row) => `| ${row.join(" | ")} |`);
70+
const rowSeparator = `==${sizesAndAlignments
71+
.map(([size]) => "=".repeat(size))
72+
.join("===")}==`;
73+
completeRows.splice(1, 0, rowSeparator);
74+
completeRows.unshift(rowSeparator);
75+
completeRows.push(rowSeparator);
76+
return completeRows;
77+
};

scripts/compare_reports.ts

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { diffPctString, toCommentTable } from "./comment-table";
2+
import { printLastReport } from "./print_report";
3+
import { getAllReports } from "./utils";
4+
import { writeReports } from "./write_reports";
5+
6+
import type { ContractReport } from "./utils";
7+
import type { HardhatRuntimeEnvironment } from "hardhat/types";
8+
9+
export function compareReports(
10+
oldReport: ContractReport,
11+
newReport: ContractReport
12+
) {
13+
const rows: string[][] = [];
14+
rows.push([`method`, `min`, `max`, `avg`, `calls`]);
15+
oldReport.methods.forEach((r1, i) => {
16+
const r2 = newReport.methods[i];
17+
rows.push([
18+
r1.method,
19+
diffPctString(r2.min, r1.min, false, true),
20+
diffPctString(r2.max, r1.max, false, true),
21+
diffPctString(r2.avg, r1.avg, false, true),
22+
diffPctString(r2.calls, r1.calls, false, true),
23+
]);
24+
});
25+
const { bytecodeSize: initSize1, deployedBytecodeSize: size1 } = oldReport;
26+
const { bytecodeSize: initSize2, deployedBytecodeSize: size2 } = newReport;
27+
rows.push([
28+
`runtime size`,
29+
diffPctString(size2, size1, false, true),
30+
"",
31+
"",
32+
"",
33+
]);
34+
rows.push([
35+
`init code size`,
36+
diffPctString(initSize2, initSize1, false, true),
37+
"",
38+
"",
39+
"",
40+
]);
41+
const table = toCommentTable(rows);
42+
const separator = table[0];
43+
table.splice(table.length - 3, 0, separator);
44+
console.log(table.join("\n"));
45+
}
46+
47+
export function compareLastTwoReports(hre: HardhatRuntimeEnvironment) {
48+
writeReports(hre);
49+
const reports = getAllReports();
50+
if (reports.length === 1) {
51+
printLastReport(hre);
52+
return;
53+
}
54+
if (reports.length < 2) {
55+
return;
56+
}
57+
const [currentReport, previousReport] = reports;
58+
const contractName = "Seaport";
59+
compareReports(
60+
previousReport.contractReports[contractName],
61+
currentReport.contractReports[contractName]
62+
);
63+
const ts = Math.floor(Date.now() / 1000);
64+
const currentSuffix =
65+
currentReport.name !== currentReport.commitHash
66+
? ` @ ${currentReport.commitHash}`
67+
: "";
68+
const previousSuffix =
69+
previousReport.name !== previousReport.commitHash
70+
? ` @ ${previousReport.commitHash}`
71+
: "";
72+
console.log(
73+
`Current Report: ${+((currentReport.timestamp - ts) / 60).toFixed(
74+
2
75+
)} min ago (${currentReport.name})${currentSuffix}`
76+
);
77+
console.log(
78+
`Previous Report: ${+((previousReport.timestamp - ts) / 60).toFixed(
79+
2
80+
)} min ago (${previousReport.name})${previousSuffix}`
81+
);
82+
}

scripts/print_report.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { err, toCommentTable, warn } from "./comment-table";
2+
import { getAllReports } from "./utils";
3+
import { writeReports } from "./write_reports";
4+
5+
import type { ContractReport } from "./utils";
6+
import type { HardhatRuntimeEnvironment } from "hardhat/types";
7+
8+
export function printReport(report: ContractReport) {
9+
const rows: string[][] = [];
10+
rows.push([`method`, `min`, `max`, `avg`, `calls`]);
11+
report.methods.forEach(({ method, min, max, avg, calls }) => {
12+
rows.push([
13+
method,
14+
min?.toString() ?? warn("null"),
15+
max?.toString() ?? warn("null"),
16+
avg?.toString() ?? warn("null"),
17+
calls?.toString() ?? warn("null"),
18+
]);
19+
});
20+
rows.push([
21+
`runtime size`,
22+
report.deployedBytecodeSize.toString(),
23+
"",
24+
"",
25+
"",
26+
]);
27+
rows.push([`init code size`, report.bytecodeSize.toString(), "", "", ""]);
28+
const table = toCommentTable(rows);
29+
const separator = table[0];
30+
table.splice(table.length - 3, 0, separator);
31+
console.log(table.join("\n"));
32+
}
33+
34+
export function printLastReport(hre: HardhatRuntimeEnvironment) {
35+
writeReports(hre);
36+
const reports = getAllReports();
37+
if (reports.length < 1) {
38+
console.log(err(`No gas reports found`));
39+
return;
40+
}
41+
const contractName = "Seaport";
42+
const [currentReport] = reports;
43+
printReport(currentReport.contractReports[contractName]);
44+
const ts = Math.floor(Date.now() / 1000);
45+
const suffix =
46+
currentReport.name !== currentReport.commitHash
47+
? ` @ ${currentReport.commitHash}`
48+
: "";
49+
console.log(
50+
`Current Report: ${+((currentReport.timestamp - ts) / 60).toFixed(
51+
2
52+
)} min ago (${currentReport.name}) ${suffix}`
53+
);
54+
}

scripts/utils.ts

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { execSync } from "child_process";
2+
import fs from "fs";
3+
import path from "path";
4+
5+
export const GAS_REPORTS_DIR = path.join(__dirname, "../.gas_reports");
6+
7+
if (!fs.existsSync(GAS_REPORTS_DIR)) {
8+
fs.mkdirSync(GAS_REPORTS_DIR);
9+
}
10+
11+
export type RawMethodReport = {
12+
contract: string;
13+
method: string;
14+
min: number;
15+
max: number;
16+
avg: number;
17+
calls: number;
18+
};
19+
20+
export type RawGasReport = {
21+
name: string;
22+
path: string;
23+
timestamp: number;
24+
report: RawMethodReport[];
25+
};
26+
27+
export type MethodReport = {
28+
method: string;
29+
min: number;
30+
max: number;
31+
avg: number;
32+
calls: number;
33+
};
34+
35+
export type ContractReport = {
36+
name: string;
37+
deployedBytecodeSize: number;
38+
bytecodeSize: number;
39+
methods: MethodReport[];
40+
};
41+
42+
export type CommitGasReport = {
43+
commitHash: string;
44+
contractReports: Record<string, ContractReport>;
45+
};
46+
47+
export function getCommitHash() {
48+
return execSync("git rev-parse HEAD").toString().trim();
49+
}
50+
51+
export function getReportPathForCommit(commit?: string): string {
52+
if (!commit) {
53+
commit = getCommitHash();
54+
}
55+
return path.join(GAS_REPORTS_DIR, `${commit}.md`);
56+
}
57+
58+
export function haveReportForCurrentCommit(): boolean {
59+
return fs.existsSync(getReportPathForCommit());
60+
}
61+
62+
export function fileLastUpdate(filePath: string): number {
63+
let timestamp = parseInt(
64+
execSync(`git log -1 --pretty="format:%ct" ${filePath}`)
65+
.toString()
66+
.trim() || "0"
67+
);
68+
if (!timestamp) {
69+
timestamp = Math.floor(+fs.statSync(filePath).mtime / 1000);
70+
}
71+
return timestamp;
72+
}
73+
74+
function parseRawReport(text: string): RawMethodReport[] {
75+
const lines = text
76+
.split("\n")
77+
.slice(6)
78+
.filter((ln) => ln.indexOf("·") !== 0);
79+
const rows = lines
80+
.map((ln) => ln.replace(/\|/g, "").replace(/\s/g, "").split("·"))
81+
.filter((row) => row.length === 7)
82+
.map(([contract, method, min, max, avg, calls]) => ({
83+
contract,
84+
method,
85+
min: +min,
86+
max: +max,
87+
avg: +avg,
88+
calls: +calls,
89+
}));
90+
return rows;
91+
}
92+
93+
export function getAllRawReports(): RawGasReport[] {
94+
const reports = fs
95+
.readdirSync(GAS_REPORTS_DIR)
96+
.filter((file) => path.extname(file) === ".md")
97+
.map((file) => {
98+
const reportPath = path.join(GAS_REPORTS_DIR, file);
99+
const timestamp = fileLastUpdate(reportPath);
100+
const text = fs.readFileSync(reportPath, "utf8");
101+
const report = parseRawReport(text);
102+
return {
103+
name: path.parse(file).name,
104+
path: reportPath,
105+
timestamp,
106+
report,
107+
};
108+
});
109+
110+
reports.sort((a, b) => b.timestamp - a.timestamp);
111+
return reports;
112+
}
113+
114+
export function getAllReports(): (CommitGasReport & {
115+
name: string;
116+
path: string;
117+
timestamp: number;
118+
})[] {
119+
const reports = fs
120+
.readdirSync(GAS_REPORTS_DIR)
121+
.filter((file) => path.extname(file) === ".json")
122+
.map((file) => {
123+
const reportPath = path.join(GAS_REPORTS_DIR, file);
124+
const timestamp = fileLastUpdate(reportPath);
125+
const report = require(reportPath) as CommitGasReport;
126+
return {
127+
name: path.parse(file).name,
128+
path: reportPath,
129+
timestamp,
130+
...report,
131+
};
132+
});
133+
134+
reports.sort((a, b) => b.timestamp - a.timestamp);
135+
return reports;
136+
}

0 commit comments

Comments
 (0)