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

feat: gas snapshots over arbitrary sections #8755

Closed
wants to merge 68 commits into from

Conversation

zerosnacks
Copy link
Member

@zerosnacks zerosnacks commented Aug 27, 2024

Motivation

Closes: #2065 + Closes: #2056 + Closes: #8713

Supports:

Uses a snapshots directory without . prefix to indicate it is meant to be checked in.

Cheatcodes:

  • snapshotValue
  • snapshotGas (wraps lastCallGas)
  • startSnapshotGas
  • stopSnapshotGas

All cheatcodes support grouping, allowing you to group arbitrary snapshots together by group name. If the group name is not supplied the name is used as filename.

We support multiple outputs:

If no group is supplied, uses the contract name as file name e.g. snapshots/GasSnapshotTest.json:

{
 "a": "123",
 "b": "456",
 "c": "789",
 "d": "123",
 "e": "456",
 "f": "789",
 "testSnapshotGasSection": "17439512"
}

Where the order is always sorted alphabetically and deterministic.

If a group name is supplied, uses the custom name e.g. CustomGroup (stored in snapshots/CustomGroup.json)

{
  "e": "456",
  "i": "456",
  "o": "123",
  "q": "789",
  "x": "123",
  "z": "789"
}

Supports FORGE_SNAPSHOT_CHECK=true flag to assert gas snapshots haven't changed across runs

Screenshot from 2024-09-02 14-11-08

Breaking

  • Updates generic snapshot to snapshotState throughout the codebase in a breaking way

Notes

To emulate internal gas tracking one could use:

    function testInternalSnapshotValue() public {
        uint256 a = gasleft();

        _run(1);

        uint256 b = gasleft();

        vm.snapshotValue("testInternalSnapshotValue", a - b);
    }

Alternatively this could be added to forge-std?

  • Includes some formatting fixes raised by the Solidity compiler as warnings that were causing issues

To do

  • Snapshots are currently not cleared by themselves at the start of running the test suite. It seems desirable that this would be the case to prevent staining
  • A FORGE_SNAPSHOT_CHECK=true environment variable to revert on a mismatch in the snapshot with gas used
  • If no group is passed automatically generate the snapshot group: for example testContractName as a default if not overridden with a specific string.
  • Add vm.snapshotGasLastCall to wrap lastCallGas? Single call is a very common use case and would be nice to skip the wrapping steps

Follow up

Deriving function names automatically is quite complex as the test being ran isn't aware of the function specific context, only the contract.

I've set it up in a way (w/ name: Option<String>) to easily add it in a follow-up PR in a non-breaking way.

  • If no name is passed automatically generate the snapshot name: for example testFunction as a default if not overridden with a specific string.

@zerosnacks zerosnacks changed the title feat: gas snapshots feat: gas snapshots over arbitrary section Aug 27, 2024
@zerosnacks zerosnacks changed the title feat: gas snapshots over arbitrary section feat: gas snapshots over arbitrary sections Aug 27, 2024
crates/cheatcodes/spec/src/vm.rs Outdated Show resolved Hide resolved
create_dir_all(ccx.state.config.paths.snapshots.clone())?;

// Write the snapshot to a file
let snapshot_path = ccx.state.config.paths.snapshots.join(name).join(".json");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to have a check for multiple snapshots with the same name (common mistake causing confusing snapshot thrashing based on test runner ordering)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recognize this is probably harder but would love if name could be optional and autogenerated as testContract.testFunction

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supportive, I think it should be possible

Copy link
Member Author

@zerosnacks zerosnacks Sep 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to add support for generating group names by contract name but haven't figured out a way to support derived function names yet. This can be added as a follow-up PR in a non-breaking way once a good way has been found.

// -------- State Snapshots --------

/// Snapshot the current state of the evm.
/// Returns the ID of the snapshot that was created.
/// To revert a snapshot use `revertTo`.
#[cheatcode(group = Evm, safety = Unsafe)]
function snapshot() external returns (uint256 snapshotId);
function snapshotState() external returns (uint256 snapshotId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this set of name change for state snapshots, since the term "snapshot" is now broad, but just noting this is a breaking change so we'll need to make sure to communicate that

Copy link
Member

@DaniPopes DaniPopes Sep 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can should keep the old names and mark them as deprecated (#[cheatcode(... status = Deprecated)],

), these kinds of breaking changes are not a good experience.

I don't believe the deprecated warnings logic is implemented yet, but it should be straight forward to implement. CC @mattsse @klkvr

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-added the old cheatcodes and tagged them as Deprecated, sharing the inner logic with the new implementation

snapshot.insert(name.clone(), value);

// Write the snapshot to a file, asserting that the write was successful.
let result = write_pretty_json_file(&snapshot_path, &snapshot).is_ok();
Copy link
Contributor

@marktoda marktoda Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the idea of groups - one thought is, does BTreeMap serialize deterministically? Given test run ordering isn't deterministic afaik, worried thrashing of snapshot output will make diffs more difficult to read. Would ideally have deterministic output

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess since BTreeMap is ordered on name and presumably serde_json just iterates over it, it's likely deterministic

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

Given that upon applying an update the json is read into a BTreeMap, changes are applied and then serialized again the order should be deterministic. I'll need to add more tests to confirm this assumption is correct, but that is the goal.

crates/cheatcodes/src/evm.rs Outdated Show resolved Hide resolved
crates/cheatcodes/src/inspector.rs Outdated Show resolved Hide resolved
crates/cheatcodes/src/inspector.rs Outdated Show resolved Hide resolved
@zerosnacks
Copy link
Member Author

Going to split out the state snapshot renaming into its own PR as it makes it difficult to iterate and review here

@zerosnacks
Copy link
Member Author

zerosnacks commented Sep 24, 2024

Closing in favor of #8952

I've split out the work in this PR into: #8945 (snapshot renaming) and #8952 (gas snapshots over sections) that branches off #8945. This will make it easier to review and collaborate.

@zerosnacks zerosnacks closed this Sep 24, 2024
mattsse pushed a commit that referenced this pull request Oct 2, 2024
* update internal naming

* further internals

* deprecate cheats

* update Solidity tests and add dedicated test for testing deprecated cheatcodes

* clarify gas snapshots

* fix build

* final fixes

* fix build

* fix repro 6355 rename

* add gas snapshot setup from #8755

* fix build + clippy warnings

* fix cheatcodes

* account for fixed CREATE / CALL gas cost

* remove import

* add stipend

* recalculate after a - b setup

* clear call_stipend, update tests

* avoid double counting external calls

* update cheatcodes, remove debug prints

* enable assertions

* clean up tests

* clean up test names

* remove snapshot directory on `forge clean`

* do not remove all snapshots by default due to multiple test suites being able to be ran concurrently or sequentially + optimize gas snapshots check - skip if none were captured

* handle edge case where we ask to compare but file does not exist, remove snapshot directory at a top level before test suites are ran

* fix path issue when attempting removal

* Update crates/cheatcodes/src/evm.rs

Co-authored-by: Arsenii Kulikov <[email protected]>

* Update crates/cheatcodes/src/inspector.rs

Co-authored-by: Arsenii Kulikov <[email protected]>

* refactor, apply recommended changes for last_snapshot_group, last_snapshot_name

* remove gas snapshots from fuzz tests for now: this is largely due to it conflicting with the FORGE_SNAPSHOT_CHECK where it is highly likely that with different fuzzed input the gas measurement differs as well. In the future it would be an idea to capture the average gas

* fix clippy

* avoid setting to 0 unnecessarily

* use if let Some

* improve comments, clarify use of last_gas_used != 0

* fix merge conflict issue

* fix arg ordering to address group naming regression

* fix import

* move snapshot name derivation to helper

* only skip initial call w/ overhead, no special handling for call frames

* add flare test

* style nits + use helper method

---------

Co-authored-by: Arsenii Kulikov <[email protected]>
@zerosnacks zerosnacks deleted the zerosnacks/snapshots branch October 4, 2024 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
8 participants