diff --git a/.changeset/cold-beers-knock.md b/.changeset/cold-beers-knock.md new file mode 100644 index 00000000000..83b65e4cce3 --- /dev/null +++ b/.changeset/cold-beers-knock.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Display contract runtime bytecode size in the gas stats table and JSON output diff --git a/packages/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts b/packages/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts index b6c84bf0189..46652b12725 100644 --- a/packages/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts +++ b/packages/hardhat/src/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts @@ -1,5 +1,6 @@ import type { ContractGasStatsJson, + DeploymentGasStatsJsonEntry, GasAnalyticsManager, GasMeasurement, GasStatsJson, @@ -10,7 +11,10 @@ import type { TableItem } from "@nomicfoundation/hardhat-utils/format"; import crypto from "node:crypto"; import path from "node:path"; -import { HardhatError } from "@nomicfoundation/hardhat-errors"; +import { + HardhatError, + assertHardhatInvariant, +} from "@nomicfoundation/hardhat-errors"; import { formatTable } from "@nomicfoundation/hardhat-utils/format"; import { ensureDir, @@ -31,8 +35,12 @@ const gasStatsLog = debug( "hardhat:core:gas-analytics:gas-analytics-manager:gas-stats", ); +interface DeploymentGasStats extends GasStats { + runtimeSize: number; +} + interface ContractGasStats { - deployment?: GasStats; + deployment?: DeploymentGasStats; functions: Map< string, // function name or signature (if overloaded) GasStats @@ -53,6 +61,7 @@ type GasMeasurementsByContract = Map; interface ContractGasMeasurements { deployments: number[]; + deploymentRuntimeSize?: number; functions: Map< string, // functionSig number[] @@ -176,12 +185,18 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { }; if (measurements.deployments.length > 0) { + assertHardhatInvariant( + measurements.deploymentRuntimeSize !== undefined, + "deploymentRuntimeSize must be set when deployments exist", + ); + contractGasStats.deployment = { 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, + runtimeSize: measurements.deploymentRuntimeSize, }; } @@ -236,6 +251,10 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { if (currentMeasurement.type === "deployment") { contractMeasurements.deployments.push(currentMeasurement.gas); + if (contractMeasurements.deploymentRuntimeSize === undefined) { + contractMeasurements.deploymentRuntimeSize = + currentMeasurement.runtimeSize; + } } else { let measurements = contractMeasurements.functions.get( currentMeasurement.functionSig, @@ -337,6 +356,13 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { `${contractGasStats.deployment.count}`, ], }); + rows.push({ + type: "header", + cells: [ + chalk.yellow("Bytecode size"), + `${contractGasStats.deployment.runtimeSize}`, + ], + }); } } @@ -361,7 +387,7 @@ export class GasAnalyticsManagerImplementation implements GasAnalyticsManager { for (const { userFqn, stats } of sortedContracts) { const { sourceName, contractName } = parseFullyQualifiedName(userFqn); - const deployment: GasStatsJsonEntry | null = + const deployment: DeploymentGasStatsJsonEntry | null = stats.deployment !== undefined ? { ...stats.deployment } : null; let functions: Record | null = null; diff --git a/packages/hardhat/src/internal/builtin-plugins/gas-analytics/types.ts b/packages/hardhat/src/internal/builtin-plugins/gas-analytics/types.ts index 01298f2e966..003f7f5a455 100644 --- a/packages/hardhat/src/internal/builtin-plugins/gas-analytics/types.ts +++ b/packages/hardhat/src/internal/builtin-plugins/gas-analytics/types.ts @@ -9,6 +9,13 @@ export interface GasStatsJsonEntry { count: number; } +/** + * Gas statistics for a deployment, including bytecode size. + */ +export interface DeploymentGasStatsJsonEntry extends GasStatsJsonEntry { + runtimeSize: number; +} + /** * Gas statistics for a single contract in the JSON output. * `deployment` is null when the contract was never deployed during the test run @@ -18,7 +25,7 @@ export interface GasStatsJsonEntry { export interface ContractGasStatsJson { sourceName: string; contractName: string; - deployment: GasStatsJsonEntry | null; + deployment: DeploymentGasStatsJsonEntry | null; functions: Record | null; } @@ -43,7 +50,7 @@ interface FunctionGasMeasurement extends BaseGasMeasurement { interface DeploymentGasMeasurement extends BaseGasMeasurement { type: "deployment"; - size: number; + runtimeSize: number; } export type GasMeasurement = FunctionGasMeasurement | DeploymentGasMeasurement; diff --git a/packages/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts b/packages/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts index 7c17dfe7186..cafa173220d 100644 --- a/packages/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts +++ b/packages/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts @@ -404,7 +404,7 @@ export function edrGasReportToHardhatGasMeasurements( contractFqn, type: "deployment", gas: Number(deployment.gas), - size: Number(deployment.size), + runtimeSize: Number(deployment.runtimeSize), }); } } diff --git a/packages/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts b/packages/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts index f27d635dd6a..5a4010bda6c 100644 --- a/packages/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts +++ b/packages/hardhat/test/internal/builtin-plugins/gas-analytics/gas-analytics-manager.ts @@ -67,7 +67,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }; manager.addGasMeasurement(deploymentMeasurement); @@ -90,7 +90,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }; manager.addGasMeasurement(measurement1); @@ -115,7 +115,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }; manager.addGasMeasurement(measurement1); @@ -154,7 +154,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }; manager.addGasMeasurement(measurement1); @@ -179,7 +179,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }; manager.addGasMeasurement(measurement1); @@ -217,7 +217,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }; manager.addGasMeasurement(measurement1); manager.addGasMeasurement(measurement2); @@ -274,7 +274,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }; manager.addGasMeasurement(measurement1); manager.addGasMeasurement(measurement2); @@ -430,7 +430,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }); const result = manager._aggregateGasMeasurements(); @@ -444,6 +444,7 @@ describe("gas-analytics-manager", () => { "Contract measurements should be defined", ); assert.deepEqual(contractMeasurements.deployments, [500000]); + assert.equal(contractMeasurements.deploymentRuntimeSize, 2048); assert.equal(contractMeasurements.functions.size, 0); }); @@ -453,7 +454,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }); manager.addGasMeasurement({ type: "function", @@ -480,6 +481,7 @@ describe("gas-analytics-manager", () => { ); assert.deepEqual(contractMeasurements.deployments, [500000]); + assert.equal(contractMeasurements.deploymentRuntimeSize, 2048); assert.equal(contractMeasurements.functions.size, 2); const transferMeasurements = contractMeasurements.functions.get( @@ -512,7 +514,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/TokenB.sol:TokenB", gas: 600000, - size: 3072, + runtimeSize: 3072, }); manager.addGasMeasurement({ type: "function", @@ -550,6 +552,7 @@ describe("gas-analytics-manager", () => { "TokenB measurements should be defined", ); assert.deepEqual(tokenBMeasurements.deployments, [600000]); + assert.equal(tokenBMeasurements.deploymentRuntimeSize, 3072); assert.equal(tokenBMeasurements.functions.size, 1); const burnMeasurements = tokenBMeasurements.functions.get("burn(uint256)"); @@ -655,13 +658,13 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }); manager.addGasMeasurement({ type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 600000, - size: 3072, + runtimeSize: 3072, }); const result = manager._aggregateGasMeasurements(); @@ -675,6 +678,7 @@ describe("gas-analytics-manager", () => { "Contract measurements should be defined", ); assert.deepEqual(contractMeasurements.deployments, [500000, 600000]); + assert.equal(contractMeasurements.deploymentRuntimeSize, 2048); }); }); @@ -730,19 +734,19 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 400000, - size: 2048, + runtimeSize: 2048, }); manager.addGasMeasurement({ type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }); manager.addGasMeasurement({ type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 600000, - size: 3072, + runtimeSize: 3072, }); const gasStats = manager._calculateGasStats(); @@ -764,6 +768,7 @@ describe("gas-analytics-manager", () => { assert.equal(contractStats.deployment.avg, 500000); assert.equal(contractStats.deployment.median, 500000); assert.equal(contractStats.deployment.count, 3); + assert.equal(contractStats.deployment.runtimeSize, 2048); }); it("should calculate stats for multiple contracts", () => { @@ -954,6 +959,7 @@ describe("gas-analytics-manager", () => { avg: 500000, median: 500000, count: 3, + runtimeSize: 2048, }, functions: new Map([ // Functions are added in non-alphabetical order to test sorting @@ -998,7 +1004,9 @@ describe("gas-analytics-manager", () => { ║ ${chalk.yellow("Deployment")} │ ${chalk.yellow("Min")} │ ${chalk.yellow("Average")} │ ${chalk.yellow("Median")} │ ${chalk.yellow("Max")} │ ${chalk.yellow("#deployments")} ║ ╟─────────────────────────────────┼────────┼─────────┼────────┼────────┼──────────────╢ ║ │ 400000 │ 500000 │ 500000 │ 600000 │ 3 ║ -╚═════════════════════════════════╧════════╧═════════╧════════╧════════╧══════════════╝ +╟─────────────────────────────────┼────────┼─────────┴────────┴────────┴──────────────╢ +║ ${chalk.yellow("Bytecode size")} │ 2048 │ ║ +╚═════════════════════════════════╧════════╧══════════════════════════════════════════╝ ╔═════════════════════════════════════════════════════════════════════════════════════╗ ║ ${chalk.cyan.bold("contracts/TokenA.sol:TokenA")} ║ ╟─────────────────────────────────┬────────┬─────────┬────────┬────────┬──────────────╢ @@ -1116,7 +1124,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }); manager.addGasMeasurement({ type: "function", @@ -1138,6 +1146,7 @@ describe("gas-analytics-manager", () => { avg: 500000, median: 500000, count: 1, + runtimeSize: 2048, }); assert.ok(contract.functions !== null, "functions should not be null"); assert.deepEqual(contract.functions.transfer, { @@ -1172,7 +1181,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/Factory.sol:Factory", gas: 300000, - size: 1024, + runtimeSize: 1024, }); const stats = manager._calculateGasStats(); const result = manager._generateGasStatsJson(stats); @@ -1192,13 +1201,13 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/ZContract.sol:ZContract", gas: 100000, - size: 512, + runtimeSize: 512, }); manager.addGasMeasurement({ type: "deployment", contractFqn: "project/contracts/AContract.sol:AContract", gas: 200000, - size: 512, + runtimeSize: 512, }); const stats = manager._calculateGasStats(); const result = manager._generateGasStatsJson(stats); @@ -1276,7 +1285,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 100000, - size: 512, + runtimeSize: 512, }); const stats = manager._calculateGasStats(); const result = manager._generateGasStatsJson(stats); @@ -1324,7 +1333,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: internalFqn, gas: 250000, - size: 1024, + runtimeSize: 1024, }); const stats = manager._calculateGasStats(); const result = manager._generateGasStatsJson(stats); @@ -1398,7 +1407,7 @@ describe("gas-analytics-manager", () => { type: "deployment", contractFqn: "project/contracts/MyContract.sol:MyContract", gas: 500000, - size: 2048, + runtimeSize: 2048, }); await manager.saveGasMeasurements("test-id");