Skip to content

Commit

Permalink
perf/refactor: partial test runner refactor (#7109)
Browse files Browse the repository at this point in the history
* perf/refactor: partial test runner refactor

* fix: update test output

* fix: identify addresses if *any* trace

* fix: clear addresses before decoding traces

* fix: keep default labels

* fix: mean overflow

* chore: reorder some stuff

* perf: speed up bytecode_diff_score in debug mode
  • Loading branch information
DaniPopes authored Feb 14, 2024
1 parent 73383b5 commit 85669c2
Show file tree
Hide file tree
Showing 24 changed files with 617 additions and 573 deletions.
13 changes: 10 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ resolver = "2"
[workspace.package]
version = "0.2.0"
edition = "2021"
rust-version = "1.74" # Remember to update clippy.toml as well
rust-version = "1.75" # Remember to update clippy.toml as well
authors = ["Foundry Contributors"]
license = "MIT OR Apache-2.0"
homepage = "https://github.com/foundry-rs/foundry"
Expand All @@ -49,7 +49,10 @@ solang-parser.opt-level = 3
serde_json.opt-level = 3

# EVM
alloy-dyn-abi.opt-level = 3
alloy-json-abi.opt-level = 3
alloy-primitives.opt-level = 3
alloy-sol-type-parser.opt-level = 3
alloy-sol-types.opt-level = 3
hashbrown.opt-level = 3
keccak.opt-level = 3
Expand All @@ -62,12 +65,16 @@ sha2.opt-level = 3
sha3.opt-level = 3
tiny-keccak.opt-level = 3

# keystores
scrypt.opt-level = 3
# fuzzing
proptest.opt-level = 3
foundry-evm-fuzz.opt-level = 3

# forking
axum.opt-level = 3

# keystores
scrypt.opt-level = 3

# Local "release" mode, more optimized than dev but much faster to compile than release.
[profile.local]
inherits = "dev"
Expand Down
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
msrv = "1.74"
msrv = "1.75"
56 changes: 24 additions & 32 deletions crates/common/src/calc.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
//! Commonly used calculations.
use alloy_primitives::{Sign, U256};
use std::ops::Div;

/// Returns the mean of the slice
/// Returns the mean of the slice.
#[inline]
pub fn mean(values: &[U256]) -> U256 {
pub fn mean(values: &[u64]) -> u64 {
if values.is_empty() {
return U256::ZERO
return 0;
}

values.iter().copied().fold(U256::ZERO, |sum, val| sum + val).div(U256::from(values.len()))
(values.iter().map(|x| *x as u128).sum::<u128>() / values.len() as u128) as u64
}

/// Returns the median of a _sorted_ slice
/// Returns the median of a _sorted_ slice.
#[inline]
pub fn median_sorted(values: &[U256]) -> U256 {
pub fn median_sorted(values: &[u64]) -> u64 {
if values.is_empty() {
return U256::ZERO
return 0;
}

let len = values.len();
let mid = len / 2;
if len % 2 == 0 {
(values[mid - 1] + values[mid]) / U256::from(2u64)
(values[mid - 1] + values[mid]) / 2
} else {
values[mid]
}
Expand Down Expand Up @@ -70,49 +69,42 @@ mod tests {

#[test]
fn calc_mean_empty() {
let values: [U256; 0] = [];
let m = mean(&values);
assert_eq!(m, U256::ZERO);
let m = mean(&[]);
assert_eq!(m, 0);
}

#[test]
fn calc_mean() {
let values = [
U256::ZERO,
U256::from(1),
U256::from(2u64),
U256::from(3u64),
U256::from(4u64),
U256::from(5u64),
U256::from(6u64),
];
let m = mean(&values);
assert_eq!(m, U256::from(3u64));
let m = mean(&[0, 1, 2, 3, 4, 5, 6]);
assert_eq!(m, 3);
}

#[test]
fn calc_mean_overflow() {
let m = mean(&[0, 1, 2, u32::MAX as u64, 3, u16::MAX as u64, u64::MAX, 6]);
assert_eq!(m, 2305843009750573057);
}

#[test]
fn calc_median_empty() {
let values: Vec<U256> = vec![];
let m = median_sorted(&values);
assert_eq!(m, U256::from(0));
let m = median_sorted(&[]);
assert_eq!(m, 0);
}

#[test]
fn calc_median() {
let mut values =
vec![29, 30, 31, 40, 59, 61, 71].into_iter().map(U256::from).collect::<Vec<_>>();
let mut values = vec![29, 30, 31, 40, 59, 61, 71];
values.sort();
let m = median_sorted(&values);
assert_eq!(m, U256::from(40));
assert_eq!(m, 40);
}

#[test]
fn calc_median_even() {
let mut values =
vec![80, 90, 30, 40, 50, 60, 10, 20].into_iter().map(U256::from).collect::<Vec<_>>();
let mut values = vec![80, 90, 30, 40, 50, 60, 10, 20];
values.sort();
let m = median_sorted(&values);
assert_eq!(m, U256::from(45));
assert_eq!(m, 45);
}

#[test]
Expand Down
28 changes: 27 additions & 1 deletion crates/common/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub type ContractsByAddress = BTreeMap<Address, (String, JsonAbi)>;
///
/// Returns a value between `0.0` (identical) and `1.0` (completely different).
pub fn bytecode_diff_score<'a>(mut a: &'a [u8], mut b: &'a [u8]) -> f64 {
// Make sure `a` is the longer one.
if a.len() < b.len() {
std::mem::swap(&mut a, &mut b);
}
Expand All @@ -119,11 +120,36 @@ pub fn bytecode_diff_score<'a>(mut a: &'a [u8], mut b: &'a [u8]) -> f64 {
}

// Count different bytes.
n_different_bytes += std::iter::zip(a, b).filter(|(a, b)| a != b).count();
// SAFETY: `a` is longer than `b`.
n_different_bytes += unsafe { count_different_bytes(a, b) };

n_different_bytes as f64 / a.len() as f64
}

/// Returns the amount of different bytes between two slices.
///
/// # Safety
///
/// `a` must be at least as long as `b`.
unsafe fn count_different_bytes(a: &[u8], b: &[u8]) -> usize {
// This could've been written as `std::iter::zip(a, b).filter(|(x, y)| x != y).count()`,
// however this function is very hot, and has been written to be as primitive as
// possible for lower optimization levels.

let a_ptr = a.as_ptr();
let b_ptr = b.as_ptr();
let len = b.len();

let mut sum = 0;
let mut i = 0;
while i < len {
// SAFETY: `a` is at least as long as `b`, and `i` is in bound of `b`.
sum += unsafe { *a_ptr.add(i) != *b_ptr.add(i) } as usize;
i += 1;
}
sum
}

/// Flattens the contracts into (`id` -> (`JsonAbi`, `Vec<u8>`)) pairs
pub fn flatten_contracts(
contracts: &BTreeMap<ArtifactId, ContractBytecodeSome>,
Expand Down
3 changes: 3 additions & 0 deletions crates/evm/core/src/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub fn decode_revert(
})
}

/// Tries to decode an error message from the given revert bytes.
///
/// See [`decode_revert`] for more information.
pub fn maybe_decode_revert(
err: &[u8],
maybe_abi: Option<&JsonAbi>,
Expand Down
22 changes: 9 additions & 13 deletions crates/evm/fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
extern crate tracing;

use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
use alloy_primitives::{Address, Bytes, Log, U256};
use alloy_primitives::{Address, Bytes, Log};
use foundry_common::{calc, contracts::ContractsByAddress};
use foundry_evm_coverage::HitMaps;
use foundry_evm_traces::CallTraceArena;
Expand Down Expand Up @@ -157,18 +157,16 @@ pub struct FuzzTestResult {
impl FuzzTestResult {
/// Returns the median gas of all test cases
pub fn median_gas(&self, with_stipend: bool) -> u64 {
let mut values =
self.gas_values(with_stipend).into_iter().map(U256::from).collect::<Vec<_>>();
let mut values = self.gas_values(with_stipend);
values.sort_unstable();
calc::median_sorted(&values).to::<u64>()
calc::median_sorted(&values)
}

/// Returns the average gas use of all test cases
pub fn mean_gas(&self, with_stipend: bool) -> u64 {
let mut values =
self.gas_values(with_stipend).into_iter().map(U256::from).collect::<Vec<_>>();
let mut values = self.gas_values(with_stipend);
values.sort_unstable();
calc::mean(&values).to::<u64>()
calc::mean(&values)
}

fn gas_values(&self, with_stipend: bool) -> Vec<u64> {
Expand Down Expand Up @@ -223,19 +221,17 @@ impl FuzzedCases {
/// Returns the median gas of all test cases
#[inline]
pub fn median_gas(&self, with_stipend: bool) -> u64 {
let mut values =
self.gas_values(with_stipend).into_iter().map(U256::from).collect::<Vec<_>>();
let mut values = self.gas_values(with_stipend);
values.sort_unstable();
calc::median_sorted(&values).to::<u64>()
calc::median_sorted(&values)
}

/// Returns the average gas use of all test cases
#[inline]
pub fn mean_gas(&self, with_stipend: bool) -> u64 {
let mut values =
self.gas_values(with_stipend).into_iter().map(U256::from).collect::<Vec<_>>();
let mut values = self.gas_values(with_stipend);
values.sort_unstable();
calc::mean(&values).to::<u64>()
calc::mean(&values)
}

#[inline]
Expand Down
20 changes: 16 additions & 4 deletions crates/evm/traces/src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,14 @@ pub struct CallTraceDecoder {
pub labels: HashMap<Address, String>,
/// Contract addresses that have a receive function.
pub receive_contracts: Vec<Address>,

/// All known functions.
pub functions: HashMap<Selector, Vec<Function>>,
/// All known events.
pub events: BTreeMap<(B256, usize), Vec<Event>>,
/// All known errors.
pub errors: JsonAbi,

/// A signature identifier for events and functions.
pub signature_identifier: Option<SingleSignaturesIdentifier>,
/// Verbosity level
Expand Down Expand Up @@ -143,7 +145,6 @@ impl CallTraceDecoder {

Self {
contracts: Default::default(),

labels: [
(CHEATCODE_ADDRESS, "VM".to_string()),
(HARDHAT_CONSOLE_ADDRESS, "console".to_string()),
Expand All @@ -152,6 +153,7 @@ impl CallTraceDecoder {
(TEST_CONTRACT_ADDRESS, "DefaultTestContract".to_string()),
]
.into(),
receive_contracts: Default::default(),

functions: hh_funcs()
.chain(
Expand All @@ -162,20 +164,30 @@ impl CallTraceDecoder {
)
.map(|(selector, func)| (selector, vec![func]))
.collect(),

events: Console::abi::events()
.into_values()
.flatten()
.map(|event| ((event.selector(), indexed_inputs(&event)), vec![event]))
.collect(),

errors: Default::default(),

signature_identifier: None,
receive_contracts: Default::default(),
verbosity: 0,
}
}

/// Clears all known addresses.
pub fn clear_addresses(&mut self) {
self.contracts.clear();

let default_labels = &Self::new().labels;
if self.labels.len() > default_labels.len() {
self.labels = default_labels.clone();
}

self.receive_contracts.clear();
}

/// Identify unknown addresses in the specified call trace using the specified identifier.
///
/// Unknown contracts are contracts that either lack a label or an ABI.
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/traces/src/identifier/etherscan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl TraceIdentifier for EtherscanIdentifier {
where
A: Iterator<Item = (&'a Address, Option<&'a [u8]>)>,
{
trace!(target: "etherscanidentifier", "identify {:?} addresses", addresses.size_hint().1);
trace!("identify {:?} addresses", addresses.size_hint().1);

let Some(client) = self.client.clone() else {
// no client was configured
Expand Down
2 changes: 1 addition & 1 deletion crates/forge/bin/cmd/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ impl CoverageArgs {
..Default::default()
})
.set_coverage(true)
.build(root.clone(), output, env, evm_opts)?;
.build(&root, output, env, evm_opts)?;

// Run tests
let known_contracts = runner.known_contracts.clone();
Expand Down
15 changes: 6 additions & 9 deletions crates/forge/bin/cmd/snapshot.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
use super::{
test,
test::{Test, TestOutcome},
};
use super::test;
use alloy_primitives::U256;
use clap::{builder::RangedU64ValueParser, Parser, ValueHint};
use eyre::{Context, Result};
use forge::result::TestKindReport;
use forge::result::{SuiteTestResult, TestKindReport, TestOutcome};
use foundry_cli::utils::STATIC_FUZZ_SEED;
use once_cell::sync::Lazy;
use regex::Regex;
Expand Down Expand Up @@ -175,7 +172,7 @@ impl SnapshotConfig {
true
}

fn apply(&self, outcome: TestOutcome) -> Vec<Test> {
fn apply(&self, outcome: TestOutcome) -> Vec<SuiteTestResult> {
let mut tests = outcome
.into_tests()
.filter(|test| self.is_in_gas_range(test.gas_used()))
Expand Down Expand Up @@ -274,7 +271,7 @@ fn read_snapshot(path: impl AsRef<Path>) -> Result<Vec<SnapshotEntry>> {

/// Writes a series of tests to a snapshot file after sorting them
fn write_to_snapshot_file(
tests: &[Test],
tests: &[SuiteTestResult],
path: impl AsRef<Path>,
_format: Option<Format>,
) -> Result<()> {
Expand Down Expand Up @@ -318,7 +315,7 @@ impl SnapshotDiff {
/// Compares the set of tests with an existing snapshot
///
/// Returns true all tests match
fn check(tests: Vec<Test>, snaps: Vec<SnapshotEntry>, tolerance: Option<u32>) -> bool {
fn check(tests: Vec<SuiteTestResult>, snaps: Vec<SnapshotEntry>, tolerance: Option<u32>) -> bool {
let snaps = snaps
.into_iter()
.map(|s| ((s.contract_name, s.signature), s.gas_used))
Expand Down Expand Up @@ -352,7 +349,7 @@ fn check(tests: Vec<Test>, snaps: Vec<SnapshotEntry>, tolerance: Option<u32>) ->
}

/// Compare the set of tests with an existing snapshot
fn diff(tests: Vec<Test>, snaps: Vec<SnapshotEntry>) -> Result<()> {
fn diff(tests: Vec<SuiteTestResult>, snaps: Vec<SnapshotEntry>) -> Result<()> {
let snaps = snaps
.into_iter()
.map(|s| ((s.contract_name, s.signature), s.gas_used))
Expand Down
Loading

0 comments on commit 85669c2

Please sign in to comment.