Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gas delta tracking for deployments #233

Merged
merged 4 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading