diff --git a/README.md b/README.md index f0fc098..9e90b8a 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,11 @@ const config: HardhatUserConfig = { ## Options -+ Option setups for common and advanced use cases can be seen in the [Config Examples][2] docs]. ++ Option setups for common and advanced use cases can be seen in the [Config Examples][2] docs. + Get a [free tier Coinmarketcap API key][3] if you want price data | Options | Type | Default | Description | | :------------------------------ | :--------: | :--------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| checkGasDeltas | _bool_ | `false` | Compare gas values to previous results | | currency | _string_ | `USD` | National currency to represent gas costs in. Exchange rates are loaded at runtime from the `coinmarketcap` api. Available currency codes can be found [here][5] | | coinmarketcap | _string_ | - | [API key][3] to use when fetching live token price data | | enabled | _bool_ | `true` | Produce gas reports with `hardhat test` | @@ -85,13 +84,14 @@ const config: HardhatUserConfig = { | includeIntrinsicGas | _bool_ | `true` | Include standard 21_000 + calldata bytes overhead in method gas usage data. (Setting to `false` can be useful for modelling contract infra that will never be called by an EOA) | | L1 | _string_ | `ethereum` | Auto-configure reporter to emulate an L1 network. (See [supported networks][6]) | | L2 | _string_ | - | Auto-configure reporter to emulate an L2 network (See [supported networks][6]) | -| L1Etherscan | _string_ | - | [API key][4] to use when fetching live gasPrice, baseFee, and blobBaseFee data from an L1 network. (Optional, see [Supported Networks][6]) | -| L2Etherscan | _string_ | - | [API key][4] to use when fetching live gasPrice data from an L2 network (Optional, see [Supported Networks][6]) | +| L1Etherscan | _string_ | - | [API key][4] to use when fetching live gasPrice and baseFee data from an L1 network. (Optional, see [Supported Networks][6]) | +| L2Etherscan | _string_ | - | [API key][4] to use when fetching live gasPrice and blobBaseFee data from an L2 network (Optional, see [Supported Networks][6]) | | offline | _bool_ | `false` | Turn off remote calls to fetch data | | optimismHardfork | _string_ | `ecotone` | Optimism hardfork to emulate L1 & L2 gas costs for. | | proxyResolver | _Class_ | - | User-defined class which helps reporter identify contract targets of proxied calls. (See [Advanced Usage][7]) | | remoteContracts | _Array_ | - | List of forked-network deployed contracts to track execution costs for.(See [Advanced Usage][8]) | | reportPureAndViewMethods | _bool_ | `false` | Track gas usage for methods invoked via `eth_call`. (Incurs a performance penalty that can be significant for large test suites) | +| trackGasDeltas | _bool_ | `false` | Track and report changes in gas usage between test runs. (Useful for gas golfing) | | :high_brightness: **DISPLAY** | | | | | currencyDisplayPrecision | _number_ | `2` | Decimal precision to show nation state currency costs in | | darkMode | _bool_ | `false` | Use colors better for dark backgrounds when printing to stdout | diff --git a/docs/advanced.md b/docs/advanced.md index 543df98..9ded55e 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -66,6 +66,17 @@ const config: HardhatUserConfig = { } ``` +### Gas Golfing +*...track changes in gas usage between test runs (conditionally, using an environment variable)* + +```ts +const config: HardhatUserConfig = { + gasReporter: { + trackGasDeltas: process.env.GAS_GOLF === "true" + } +} +``` + ### Documentation *...writing report in markdown format to file while displaying regular report on stdout* diff --git a/scripts/gen-options-md.ts b/scripts/gen-options-md.ts index cfe4b9d..4aa7704 100644 --- a/scripts/gen-options-md.ts +++ b/scripts/gen-options-md.ts @@ -91,7 +91,7 @@ title, "L1Etherscan", "_string_", "-", - "[API key][4] to use when fetching live gasPrice, baseFee, and blobBaseFee data " + + "[API key][4] to use when fetching live gasPrice and baseFee data " + "from an L1 network. (Optional, see [Supported Networks][6])" ], // L1Etherscan @@ -99,7 +99,7 @@ title, "L2Etherscan", "_string_", "-", - "[API key][4] to use when fetching live gasPrice data from an L2 network " + + "[API key][4] to use when fetching live gasPrice and blobBaseFee data from an L2 network " + "(Optional, see [Supported Networks][6])" ], // offline @@ -140,6 +140,13 @@ title, "Track gas usage for methods invoked via `eth_call`. (Incurs a performance penalty that can be " + "significant for large test suites)" ], +// trackGasDeltas +[ + "trackGasDeltas", + "_bool_", + "`false`", + "Track and report changes in gas usage between test runs. (Useful for gas golfing)" +], displaySubtitle, // currencyDisplayPrecision [ diff --git a/src/lib/gasData.ts b/src/lib/gasData.ts index a83ff2a..a1676e0 100644 --- a/src/lib/gasData.ts +++ b/src/lib/gasData.ts @@ -112,27 +112,21 @@ export class GasData { */ public addDeltas(previousData: GasData) { Object.keys(this.methods).forEach(key => { - if (!previousData.methods[key]) return; + if (!previousData.methods[key]) return; - const currentMethod = this.methods[key]; - const prevMethod = previousData.methods[key]; + const currentMethod = this.methods[key]; + const prevMethod = previousData.methods[key]; - if (currentMethod.min !== undefined && prevMethod.min !== undefined) { - currentMethod.minDelta = currentMethod.min! - prevMethod.min!; - } + this._calculateDeltas(prevMethod, currentMethod); + }); - if (currentMethod.max !== undefined && prevMethod.max !== undefined) { - currentMethod.maxDelta = currentMethod.max! - prevMethod.max!; - } + for (const currentDeployment of this.deployments) { + const prevDeployment = previousData.deployments.find((d)=> d.name === currentDeployment.name); - if (currentMethod.executionGasAverage !== undefined && prevMethod.executionGasAverage !== undefined) { - currentMethod.executionGasAverageDelta = currentMethod.executionGasAverage! - prevMethod.executionGasAverage!; - } + if (!prevDeployment) return; - if (currentMethod.calldataGasAverage !== undefined && prevMethod.calldataGasAverage !== undefined) { - currentMethod.calldataGasAverageDelta = currentMethod.calldataGasAverage! - prevMethod.calldataGasAverage!; - } - }) + this._calculateDeltas(prevDeployment, currentDeployment); + } } /** @@ -325,4 +319,27 @@ export class GasData { ) : undefined; } + + /** + * Calculate gas deltas for a given method or deployment item + * @param {MethodDataItem | Deployment} prev + * @param {MethodDataItem | Deployment} current + */ + private _calculateDeltas(prev: MethodDataItem | Deployment, current: MethodDataItem | Deployment) { + if (current.min !== undefined && prev.min !== undefined) { + current.minDelta = current.min! - prev.min!; + } + + if (current.max !== undefined && prev.max !== undefined) { + current.maxDelta = current.max! - prev.max!; + } + + if (current.executionGasAverage !== undefined && prev.executionGasAverage !== undefined) { + current.executionGasAverageDelta = current.executionGasAverage! - prev.executionGasAverage!; + } + + if (current.calldataGasAverage !== undefined && prev.calldataGasAverage !== undefined) { + current.calldataGasAverageDelta = current.calldataGasAverage! - prev.calldataGasAverage!; + } + } } diff --git a/src/lib/render/index.ts b/src/lib/render/index.ts index 3bed06d..c3dcedf 100644 --- a/src/lib/render/index.ts +++ b/src/lib/render/index.ts @@ -57,7 +57,7 @@ export function render( options.blockGasLimit = hre.__hhgrec.blockGasLimit; options.solcInfo = getSolcInfo(hre.config.solidity.compilers[0]); - if (options.checkGasDeltas) { + if (options.trackGasDeltas) { options.cachePath = options.cachePath || path.resolve( hre.config.paths.cache, CACHE_FILE_NAME @@ -120,7 +120,7 @@ export function render( generateJSONData(data, options, toolchain); } - if (options.checkGasDeltas) { + if (options.trackGasDeltas) { options.outputJSONFile = options.cachePath!; generateJSONData(data, options, toolchain); } diff --git a/src/lib/render/markdown.ts b/src/lib/render/markdown.ts index 5879c3d..deeb65e 100644 --- a/src/lib/render/markdown.ts +++ b/src/lib/render/markdown.ts @@ -12,8 +12,7 @@ import { entitleMarkdown, getCommonTableVals, costIsBelowPrecision, - markdownBold, - renderWithGasDelta + markdownBold } from "../../utils/ui"; import { GasReporterOptions, MethodDataItem } from "../../types"; @@ -136,11 +135,6 @@ export function generateMarkdownTable( ? "-" : commify(method.calldataGasAverage); }; - - if (options.checkGasDeltas) { - stats.executionGasAverage = renderWithGasDelta(stats.executionGasAverage, method.executionGasAverageDelta || 0); - stats.calldataGasAverage = renderWithGasDelta(stats.calldataGasAverage, method.calldataGasAverageDelta || 0); - } } else { stats.executionGasAverage = "-"; stats.cost = "-"; @@ -152,14 +146,8 @@ export function generateMarkdownTable( if (method.min && method.max) { const uniform = (method.min === method.max); - let min = commify(method.min!); - let max = commify(method.max!) - if (options.checkGasDeltas) { - min = renderWithGasDelta(min, method.minDelta || 0); - max = renderWithGasDelta(max, method.maxDelta || 0); - } - stats.min = uniform ? "-" : min; - stats.max = uniform ? "-" : max; + stats.min = uniform ? "-" : commify(method.min!); + stats.max = uniform ? "-" : commify(method.max!); } stats.numberOfCalls = method.numberOfCalls.toString(); @@ -309,4 +297,3 @@ export function generateMarkdownTable( // --------------------------------------------------------------------------------------------- return md; } - diff --git a/src/lib/render/terminal.ts b/src/lib/render/terminal.ts index eafda36..3495a06 100644 --- a/src/lib/render/terminal.ts +++ b/src/lib/render/terminal.ts @@ -104,13 +104,13 @@ export function generateTerminalTextTable( // Also writes dash when average is zero stats.calldataGasAverage = (method.calldataGasAverage) - ? commify(method.calldataGasAverage) + ? commify(method.calldataGasAverage) : chalk.grey("-"); - if (options.checkGasDeltas) { - stats.executionGasAverage = renderWithGasDelta(stats.executionGasAverage, method.executionGasAverageDelta || 0, true); - stats.calldataGasAverage = renderWithGasDelta(stats.calldataGasAverage, method.calldataGasAverageDelta || 0, true); - } + 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("-"); @@ -124,7 +124,7 @@ export function generateTerminalTextTable( const uniform = (method.min === method.max); let min = chalk.cyan(commify(method.min!)); let max = chalk.red(commify(method.max!)); - if (options.checkGasDeltas) { + if (options.trackGasDeltas) { min = renderWithGasDelta(min, method.minDelta || 0, true); max = renderWithGasDelta(max, method.maxDelta || 0, true); } @@ -183,21 +183,33 @@ export function generateTerminalTextTable( 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; - stats.min = uniform ? chalk.grey("-") : chalk.cyan(commify(deployment.min!)); - stats.max = uniform ? chalk.grey("-") : chalk.red(commify(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: commify(deployment.executionGasAverage!) }); + section.push({ hAlign: "right", colSpan: 1, content: stats.executionGasAverage! }); if (options.L2 !== undefined) { section.push({ hAlign: "right", colSpan: 1, content: stats.calldataGasAverage! }) diff --git a/src/types.ts b/src/types.ts index 15668d4..0d26547 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,9 +36,6 @@ export interface GasReporterOptions { /** @property Etherscan-like url to fetch blobBasefee from */ blobBaseFeeApi?: string; - /** @property Compare gas values to previous results */ - checkGasDeltas?: boolean; - /** @property API key to access token/currency market price data with */ coinmarketcap?: string; @@ -150,6 +147,9 @@ export interface GasReporterOptions { /** @property Network token price per nation state currency unit, to two decimal places (eg: "2145.00") */ tokenPrice?: string; + /** @property Show change in current method and deployment gas usage versus previous test run */ + trackGasDeltas?: boolean; + // ==================================== // INTERNAL: AUTOSET BY PLUGIN or STUBS // ===================================== @@ -239,7 +239,11 @@ export interface Deployment { executionGasAverage?: number, calldataGasAverage?: number, cost?: string, - percent?: number + percent?: number, + minDelta?: number, + maxDelta?: number, + executionGasAverageDelta? :number + calldataGasAverageDelta?: number, } export interface SolcInfo { diff --git a/test/integration/options.a.ts b/test/integration/options.a.ts index 70e8c71..d59dff8 100644 --- a/test/integration/options.a.ts +++ b/test/integration/options.a.ts @@ -89,16 +89,19 @@ describe("Options A", function () { assert.equal(methodB?.numberOfCalls, 1); }); - it("calculates gas deltas for method calls", async function(){ + it("calculates gas deltas for methods and deployments", async function(){ process.env.GAS_DELTA = "true"; await this.env.run(TASK_TEST, { testFiles: [] }); process.env.GAS_DELTA = ""; const _output = JSON.parse(readFileSync(options.cachePath!, 'utf-8')); const _methods = _output.data!.methods; + const _deployments = _output.data!.deployments; const _options = _output.options; const method = findMethod(_methods, "VariableCosts", "addToMap"); + const deployment = findDeployment(_deployments, "VariableConstructor"); + if (_options.cachePath) { try { @@ -108,5 +111,9 @@ describe("Options A", function () { assert.isNumber(method!.executionGasAverageDelta!); assert.notEqual(method!.executionGasAverageDelta!, 0); + + assert.isNumber(deployment!.executionGasAverage); + assert.notEqual(deployment!.executionGasAverageDelta, 0); }); + }); diff --git a/test/projects/options/hardhat.options.a.config.ts b/test/projects/options/hardhat.options.a.config.ts index cf174e9..1a45df3 100644 --- a/test/projects/options/hardhat.options.a.config.ts +++ b/test/projects/options/hardhat.options.a.config.ts @@ -45,7 +45,7 @@ const config: HardhatUserConfig = { darkMode: true, proxyResolver: new EtherRouterResolver(), includeBytecodeInJSON: true, - checkGasDeltas: true + trackGasDeltas: true } }; diff --git a/test/projects/options/test/variableconstructor.ts b/test/projects/options/test/variableconstructor.ts index b77b847..0cb1d15 100644 --- a/test/projects/options/test/variableconstructor.ts +++ b/test/projects/options/test/variableconstructor.ts @@ -2,22 +2,26 @@ import { ethers } from "hardhat"; describe("VariableConstructor", function() { let VariableConstructor: any; + const short = "s"; + const medium = process.env.GAS_DELTA === "true" + ? "medium_length_initializer" + : "medium_length_initializerrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr"; + + const long = "looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_initializer"; before(async function(){ VariableConstructor = await ethers.getContractFactory("VariableConstructor"); }) + it("should should initialize with a short string", async () => { - await VariableConstructor.deploy("Exit Visa"); + await VariableConstructor.deploy(short); }); it("should should initialize with a medium length string", async () => { - await VariableConstructor.deploy("Enclosed is my application for residency"); + await VariableConstructor.deploy(medium); }); it("should should initialize with a long string", async () => { - let msg = - "Enclosed is my application for permanent residency in NewZealand."; - msg += "I am a computer programmer."; - await VariableConstructor.deploy(msg); + await VariableConstructor.deploy(long); }); });