diff --git a/.changeset/evil-beers-pump.md b/.changeset/evil-beers-pump.md new file mode 100644 index 00000000000..d910838eb6f --- /dev/null +++ b/.changeset/evil-beers-pump.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Aggregate deployment gas statistics (min, avg, median, max, count) instead of showing only the last deployment cost and size ([#8037](https://github.com/NomicFoundation/hardhat/issues/8037)) diff --git a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts index c8bac6008e3..4e69d06765c 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts @@ -21,7 +21,7 @@ const gasStatsLog = debug( ); interface ContractGasStats { - deployment?: { gas: number; size: number }; + deployment?: GasStats; functions: Map< string, // function name or signature (if overloaded) GasStats @@ -35,13 +35,13 @@ interface GasStats { max: number; avg: number; median: number; - calls: number; + count: number; } type GasMeasurementsByContract = Map; interface ContractGasMeasurements { - deployment?: { gas: number; size: number }; + deployments: number[]; functions: Map< string, // functionSig number[] @@ -138,10 +138,13 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { functions: new Map(), }; - if (measurements.deployment !== undefined) { + if (measurements.deployments.length > 0) { contractGasStats.deployment = { - gas: measurements.deployment.gas, - size: measurements.deployment.size, + min: Math.min(...measurements.deployments), + max: Math.max(...measurements.deployments), + avg: Math.round(avg(measurements.deployments)), + median: Math.round(median(measurements.deployments)), + count: measurements.deployments.length, }; } @@ -158,7 +161,7 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { max: Math.max(...gasValues), avg: Math.round(avg(gasValues)), median: Math.round(median(gasValues)), - calls: gasValues.length, + count: gasValues.length, }; contractGasStats.functions.set( @@ -185,6 +188,7 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { ); if (contractMeasurements === undefined) { contractMeasurements = { + deployments: [], functions: new Map(), }; measurementsByContract.set( @@ -194,10 +198,7 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { } if (currentMeasurement.type === "deployment") { - contractMeasurements.deployment = { - gas: currentMeasurement.gas, - size: currentMeasurement.size, - }; + contractMeasurements.deployments.push(currentMeasurement.gas); } else { let measurements = contractMeasurements.functions.get( currentMeasurement.functionSig, @@ -271,7 +272,7 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { `${gasStats.avg}`, `${gasStats.median}`, `${gasStats.max}`, - `${gasStats.calls}`, + `${gasStats.count}`, ], }); } @@ -279,15 +280,24 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { if (contractGasStats.deployment !== undefined) { rows.push({ type: "header", - cells: ["Deployment Cost", "Deployment Size"].map((s) => - chalk.yellow(s), - ), + cells: [ + "Deployment", + "Min", + "Average", + "Median", + "Max", + "#deployments", + ].map((s) => chalk.yellow(s)), }); rows.push({ type: "row", cells: [ - `${contractGasStats.deployment.gas}`, - `${contractGasStats.deployment.size}`, + "", + `${contractGasStats.deployment.min}`, + `${contractGasStats.deployment.avg}`, + `${contractGasStats.deployment.median}`, + `${contractGasStats.deployment.max}`, + `${contractGasStats.deployment.count}`, ], }); } diff --git a/v-next/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts b/v-next/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts index fa94756d408..2fca80952c6 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts @@ -403,7 +403,7 @@ describe("gas-analytics-manager", () => { contractMeasurements !== undefined, "Contract measurements should be defined", ); - assert.equal(contractMeasurements.deployment, undefined); + assert.deepEqual(contractMeasurements.deployments, []); assert.equal(contractMeasurements.functions.size, 1); const transferMeasurements = contractMeasurements.functions.get( @@ -435,12 +435,7 @@ describe("gas-analytics-manager", () => { contractMeasurements !== undefined, "Contract measurements should be defined", ); - assert.ok( - contractMeasurements.deployment !== undefined, - "Deployment should be defined", - ); - assert.equal(contractMeasurements.deployment.gas, 500000); - assert.equal(contractMeasurements.deployment.size, 2048); + assert.deepEqual(contractMeasurements.deployments, [500000]); assert.equal(contractMeasurements.functions.size, 0); }); @@ -476,12 +471,7 @@ describe("gas-analytics-manager", () => { "Contract measurements should be defined", ); - assert.ok( - contractMeasurements.deployment !== undefined, - "Deployment should be defined", - ); - assert.equal(contractMeasurements.deployment.gas, 500000); - assert.equal(contractMeasurements.deployment.size, 2048); + assert.deepEqual(contractMeasurements.deployments, [500000]); assert.equal(contractMeasurements.functions.size, 2); const transferMeasurements = contractMeasurements.functions.get( @@ -534,7 +524,7 @@ describe("gas-analytics-manager", () => { tokenAMeasurements !== undefined, "TokenA measurements should be defined", ); - assert.equal(tokenAMeasurements.deployment, undefined); + assert.deepEqual(tokenAMeasurements.deployments, []); assert.equal(tokenAMeasurements.functions.size, 1); const mintMeasurements = tokenAMeasurements.functions.get("mint(uint256)"); @@ -551,12 +541,7 @@ describe("gas-analytics-manager", () => { tokenBMeasurements !== undefined, "TokenB measurements should be defined", ); - assert.ok( - tokenBMeasurements.deployment !== undefined, - "TokenB deployment should be defined", - ); - assert.equal(tokenBMeasurements.deployment.gas, 600000); - assert.equal(tokenBMeasurements.deployment.size, 3072); + assert.deepEqual(tokenBMeasurements.deployments, [600000]); assert.equal(tokenBMeasurements.functions.size, 1); const burnMeasurements = tokenBMeasurements.functions.get("burn(uint256)"); @@ -656,7 +641,7 @@ describe("gas-analytics-manager", () => { assert.deepEqual(transfer2Measurements, [35000]); }); - it("should only keep latest deployment measurement per contract", () => { + it("should aggregate deployment measurements per contract", () => { const manager = new GasAnalyticsManagerImplementation(tmpDir); manager.addGasMeasurement({ type: "deployment", @@ -681,12 +666,7 @@ describe("gas-analytics-manager", () => { contractMeasurements !== undefined, "Contract measurements should be defined", ); - assert.ok( - contractMeasurements.deployment !== undefined, - "Deployment should be defined", - ); - assert.equal(contractMeasurements.deployment.gas, 600000); - assert.equal(contractMeasurements.deployment.size, 3072); + assert.deepEqual(contractMeasurements.deployments, [500000, 600000]); }); }); @@ -733,17 +713,29 @@ describe("gas-analytics-manager", () => { assert.equal(transferStats.max, 30000); assert.equal(transferStats.avg, 25000); assert.equal(transferStats.median, 25000); - assert.equal(transferStats.calls, 3); + assert.equal(transferStats.count, 3); }); it("should calculate stats for deployment gas measurements", () => { const manager = new GasAnalyticsManagerImplementation(tmpDir); + manager.addGasMeasurement({ + type: "deployment", + contractFqn: "project/contracts/MyContract.sol:MyContract", + gas: 400000, + size: 2048, + }); manager.addGasMeasurement({ type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, size: 2048, }); + manager.addGasMeasurement({ + type: "deployment", + contractFqn: "project/contracts/MyContract.sol:MyContract", + gas: 600000, + size: 3072, + }); const gasStats = manager._calculateGasStats(); @@ -759,8 +751,11 @@ describe("gas-analytics-manager", () => { contractStats.deployment !== undefined, "Deployment stats should be defined", ); - assert.equal(contractStats.deployment.gas, 500000); - assert.equal(contractStats.deployment.size, 2048); + assert.equal(contractStats.deployment.min, 400000); + assert.equal(contractStats.deployment.max, 600000); + assert.equal(contractStats.deployment.avg, 500000); + assert.equal(contractStats.deployment.median, 500000); + assert.equal(contractStats.deployment.count, 3); }); it("should calculate stats for multiple contracts", () => { @@ -837,7 +832,7 @@ describe("gas-analytics-manager", () => { assert.equal(transferOverload1.max, 25000); assert.equal(transferOverload1.avg, 25000); assert.equal(transferOverload1.median, 25000); - assert.equal(transferOverload1.calls, 1); + assert.equal(transferOverload1.count, 1); const transferOverload2 = contractStats.functions.get( "transfer(address,uint256,bytes)", @@ -850,7 +845,7 @@ describe("gas-analytics-manager", () => { assert.equal(transferOverload2.max, 35000); assert.equal(transferOverload2.avg, 35000); assert.equal(transferOverload2.median, 35000); - assert.equal(transferOverload2.calls, 1); + assert.equal(transferOverload2.count, 1); }); it("should handle empty gas measurements", () => { @@ -928,7 +923,7 @@ describe("gas-analytics-manager", () => { max: 36000, avg: 34000, median: 34000, - calls: 2, + count: 2, }, ], [ @@ -938,14 +933,20 @@ describe("gas-analytics-manager", () => { max: 28000, avg: 25000, median: 25000, - calls: 2, + count: 2, }, ], ]), }); gasStats.set("project/contracts/MyContract.sol:MyContract", { - deployment: { gas: 500000, size: 2048 }, + deployment: { + min: 400000, + max: 600000, + avg: 500000, + median: 500000, + count: 3, + }, functions: new Map([ // Functions are added in non-alphabetical order to test sorting [ @@ -955,7 +956,7 @@ describe("gas-analytics-manager", () => { max: 30000, avg: 25000, median: 25000, - calls: 3, + count: 3, }, ], [ @@ -965,7 +966,7 @@ describe("gas-analytics-manager", () => { max: 15000, avg: 15000, median: 15000, - calls: 1, + count: 1, }, ], ]), @@ -975,29 +976,29 @@ describe("gas-analytics-manager", () => { const report = manager._generateGasStatsReport(gasStats); const expectedReport = ` -╔═══════════════════════════════════════════════════════════════════════════════════════╗ -║ ${chalk.bold("Gas Usage Statistics")} ║ -╚═══════════════════════════════════════════════════════════════════════════════════════╝ -╔═══════════════════════════════════════════════════════════════════════════════════════╗ -║ ${chalk.cyan.bold("contracts/MyContract.sol:MyContract")} ║ -╟─────────────────────────────────┬─────────────────┬─────────┬────────┬───────┬────────╢ -║ ${chalk.yellow("Function name")} │ ${chalk.yellow("Min")} │ ${chalk.yellow("Average")} │ ${chalk.yellow("Median")} │ ${chalk.yellow("Max")} │ ${chalk.yellow("#calls")} ║ -╟─────────────────────────────────┼─────────────────┼─────────┼────────┼───────┼────────╢ -║ balanceOf │ 15000 │ 15000 │ 15000 │ 15000 │ 1 ║ -║ transfer │ 20000 │ 25000 │ 25000 │ 30000 │ 3 ║ -╟─────────────────────────────────┼─────────────────┼─────────┴────────┴───────┴────────╢ -║ ${chalk.yellow("Deployment Cost")} │ ${chalk.yellow("Deployment Size")} │ ║ -╟─────────────────────────────────┼─────────────────┤ ║ -║ 500000 │ 2048 │ ║ -╚═════════════════════════════════╧═════════════════╧═══════════════════════════════════╝ -╔═══════════════════════════════════════════════════════════════════════════════════════╗ -║ ${chalk.cyan.bold("contracts/TokenA.sol:TokenA")} ║ -╟─────────────────────────────────┬─────────────────┬─────────┬────────┬───────┬────────╢ -║ ${chalk.yellow("Function name")} │ ${chalk.yellow("Min")} │ ${chalk.yellow("Average")} │ ${chalk.yellow("Median")} │ ${chalk.yellow("Max")} │ ${chalk.yellow("#calls")} ║ -╟─────────────────────────────────┼─────────────────┼─────────┼────────┼───────┼────────╢ -║ transfer(address,uint256) │ 22000 │ 25000 │ 25000 │ 28000 │ 2 ║ -║ transfer(address,uint256,bytes) │ 32000 │ 34000 │ 34000 │ 36000 │ 2 ║ -╚═════════════════════════════════╧═════════════════╧═════════╧════════╧═══════╧════════╝ +╔═════════════════════════════════════════════════════════════════════════════════════╗ +║ ${chalk.bold("Gas Usage Statistics")} ║ +╚═════════════════════════════════════════════════════════════════════════════════════╝ +╔═════════════════════════════════════════════════════════════════════════════════════╗ +║ ${chalk.cyan.bold("contracts/MyContract.sol:MyContract")} ║ +╟─────────────────────────────────┬────────┬─────────┬────────┬────────┬──────────────╢ +║ ${chalk.yellow("Function name")} │ ${chalk.yellow("Min")} │ ${chalk.yellow("Average")} │ ${chalk.yellow("Median")} │ ${chalk.yellow("Max")} │ ${chalk.yellow("#calls")} ║ +╟─────────────────────────────────┼────────┼─────────┼────────┼────────┼──────────────╢ +║ balanceOf │ 15000 │ 15000 │ 15000 │ 15000 │ 1 ║ +║ transfer │ 20000 │ 25000 │ 25000 │ 30000 │ 3 ║ +╟─────────────────────────────────┼────────┼─────────┼────────┼────────┼──────────────╢ +║ ${chalk.yellow("Deployment")} │ ${chalk.yellow("Min")} │ ${chalk.yellow("Average")} │ ${chalk.yellow("Median")} │ ${chalk.yellow("Max")} │ ${chalk.yellow("#deployments")} ║ +╟─────────────────────────────────┼────────┼─────────┼────────┼────────┼──────────────╢ +║ │ 400000 │ 500000 │ 500000 │ 600000 │ 3 ║ +╚═════════════════════════════════╧════════╧═════════╧════════╧════════╧══════════════╝ +╔═════════════════════════════════════════════════════════════════════════════════════╗ +║ ${chalk.cyan.bold("contracts/TokenA.sol:TokenA")} ║ +╟─────────────────────────────────┬────────┬─────────┬────────┬────────┬──────────────╢ +║ ${chalk.yellow("Function name")} │ ${chalk.yellow("Min")} │ ${chalk.yellow("Average")} │ ${chalk.yellow("Median")} │ ${chalk.yellow("Max")} │ ${chalk.yellow("#calls")} ║ +╟─────────────────────────────────┼────────┼─────────┼────────┼────────┼──────────────╢ +║ transfer(address,uint256) │ 22000 │ 25000 │ 25000 │ 28000 │ 2 ║ +║ transfer(address,uint256,bytes) │ 32000 │ 34000 │ 34000 │ 36000 │ 2 ║ +╚═════════════════════════════════╧════════╧═════════╧════════╧════════╧══════════════╝ `.trim(); assert.equal(report, expectedReport); @@ -1016,7 +1017,7 @@ describe("gas-analytics-manager", () => { max: 25000, avg: 25000, median: 25000, - calls: 1, + count: 1, }, ], [ @@ -1026,7 +1027,7 @@ describe("gas-analytics-manager", () => { max: 23000, avg: 23000, median: 23000, - calls: 1, + count: 1, }, ], ]), @@ -1061,7 +1062,7 @@ describe("gas-analytics-manager", () => { max: 35000, avg: 35000, median: 35000, - calls: 1, + count: 1, }, ], [ @@ -1071,7 +1072,7 @@ describe("gas-analytics-manager", () => { max: 25000, avg: 25000, median: 25000, - calls: 1, + count: 1, }, ], ]),