Skip to content

Commit

Permalink
Add gas delta tracking for deployments (#233)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgewecke authored May 23, 2024
1 parent 3200cbe commit bbc4171
Show file tree
Hide file tree
Showing 11 changed files with 111 additions and 62 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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 |
Expand Down
11 changes: 11 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down
11 changes: 9 additions & 2 deletions scripts/gen-options-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,15 @@ 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
[
"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
Expand Down Expand Up @@ -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
[
Expand Down
49 changes: 33 additions & 16 deletions src/lib/gasData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/**
Expand Down Expand Up @@ -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!;
}
}
}
4 changes: 2 additions & 2 deletions src/lib/render/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
19 changes: 3 additions & 16 deletions src/lib/render/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
entitleMarkdown,
getCommonTableVals,
costIsBelowPrecision,
markdownBold,
renderWithGasDelta
markdownBold
} from "../../utils/ui";

import { GasReporterOptions, MethodDataItem } from "../../types";
Expand Down Expand Up @@ -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 = "-";
Expand All @@ -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();
Expand Down Expand Up @@ -309,4 +297,3 @@ export function generateMarkdownTable(
// ---------------------------------------------------------------------------------------------
return md;
}

32 changes: 22 additions & 10 deletions src/lib/render/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("-");
Expand All @@ -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);
}
Expand Down Expand Up @@ -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! })
Expand Down
12 changes: 8 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
// =====================================
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion test/integration/options.a.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
});

});
2 changes: 1 addition & 1 deletion test/projects/options/hardhat.options.a.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const config: HardhatUserConfig = {
darkMode: true,
proxyResolver: new EtherRouterResolver(),
includeBytecodeInJSON: true,
checkGasDeltas: true
trackGasDeltas: true
}
};

Expand Down
Loading

0 comments on commit bbc4171

Please sign in to comment.