Skip to content

Commit c1ae4ec

Browse files
author
woojinnn
committed
feat(revme): add benchmarking capability to statetest command
1 parent 3b17ee6 commit c1ae4ec

File tree

3 files changed

+239
-9
lines changed

3 files changed

+239
-9
lines changed

bins/revme/src/cmd/statetest.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod bench;
12
pub mod merkle_trie;
23
mod runner;
34
pub mod utils;
@@ -36,6 +37,15 @@ pub struct Cmd {
3637
/// Keep going after a test failure
3738
#[arg(long, alias = "no-fail-fast")]
3839
keep_going: bool,
40+
/// Run benchmarks instead of tests
41+
#[arg(long)]
42+
bench: bool,
43+
/// Warmup time for benchmarks (default: 300 milliseconds)
44+
#[arg(short = 'w', long)]
45+
warmup: Option<u64>,
46+
/// Measurement time for benchmarks (default: 2 seconds)
47+
#[arg(short = 'm', long)]
48+
time: Option<u64>,
3949
}
4050

4151
impl Cmd {
@@ -50,7 +60,6 @@ impl Cmd {
5060
});
5161
}
5262

53-
println!("\nRunning tests in {}...", path.display());
5463
let test_files = find_all_json_tests(path);
5564

5665
if test_files.is_empty() {
@@ -61,13 +70,19 @@ impl Cmd {
6170
});
6271
}
6372

64-
run(
65-
test_files,
66-
self.single_thread,
67-
self.json,
68-
self.json_outcome,
69-
self.keep_going,
70-
)?
73+
if self.bench {
74+
println!("\nRunning benchmarks in {}...", path.display());
75+
bench::run_benchmarks(test_files, self.warmup, self.time);
76+
} else {
77+
println!("\nRunning tests in {}...", path.display());
78+
run(
79+
test_files,
80+
self.single_thread,
81+
self.json,
82+
self.json_outcome,
83+
self.keep_going,
84+
)?;
85+
}
7186
}
7287
Ok(())
7388
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
use super::runner::skip_test;
2+
use criterion::{BatchSize, Criterion};
3+
use revm::{
4+
context::{block::BlockEnv, cfg::CfgEnv, tx::TxEnv},
5+
database::{self, CacheState},
6+
primitives::{hardfork::SpecId, U256},
7+
statetest_types::{SpecName, Test, TestSuite, TestUnit},
8+
Context, ExecuteCommitEvm, MainBuilder, MainContext,
9+
};
10+
use std::path::{Path, PathBuf};
11+
12+
/// Configuration for benchmark execution
13+
struct BenchConfig {
14+
cfg: CfgEnv,
15+
block: BlockEnv,
16+
tx: TxEnv,
17+
cache_state: CacheState,
18+
}
19+
20+
impl BenchConfig {
21+
/// Create a new benchmark configuration from test unit and test
22+
fn new(unit: &TestUnit, test: &Test, spec_name: &SpecName) -> Option<Self> {
23+
// Setup base configuration
24+
let mut cfg = CfgEnv::default();
25+
cfg.chain_id = unit
26+
.env
27+
.current_chain_id
28+
.unwrap_or(U256::ONE)
29+
.try_into()
30+
.unwrap_or(1);
31+
32+
cfg.spec = spec_name.to_spec_id();
33+
34+
// Configure max blobs per spec
35+
if cfg.spec.is_enabled_in(SpecId::OSAKA) {
36+
cfg.set_max_blobs_per_tx(6);
37+
} else if cfg.spec.is_enabled_in(SpecId::PRAGUE) {
38+
cfg.set_max_blobs_per_tx(9);
39+
} else {
40+
cfg.set_max_blobs_per_tx(6);
41+
}
42+
43+
// Setup block environment
44+
let block = unit.block_env(&mut cfg);
45+
46+
// Setup transaction environment
47+
let tx = match test.tx_env(unit) {
48+
Ok(tx) => tx,
49+
Err(_) => return None,
50+
};
51+
52+
// Prepare initial state
53+
let cache_state = unit.state();
54+
55+
Some(Self {
56+
cfg,
57+
block,
58+
tx,
59+
cache_state,
60+
})
61+
}
62+
}
63+
64+
/// Execute a single benchmark iteration
65+
fn execute_bench_iteration(config: &BenchConfig) {
66+
// Clone fresh state (Must clone because `transact_commit` modifies state)
67+
let mut cache = config.cache_state.clone(); // Clones the pre-state
68+
cache.set_state_clear_flag(config.cfg.spec.is_enabled_in(SpecId::SPURIOUS_DRAGON));
69+
70+
// Build state database
71+
let mut state = database::State::builder()
72+
.with_cached_prestate(cache)
73+
.with_bundle_update()
74+
.build();
75+
76+
// Build EVM instance
77+
let mut evm = Context::mainnet()
78+
.with_block(&config.block) // block number, timestamp, coinbase, etc.
79+
.with_tx(&config.tx) // caller, value, data, gas limit, etc.
80+
.with_cfg(&config.cfg) // chain_id, spec_id (Cancun, Prague, etc.)
81+
.with_db(&mut state)
82+
.build_mainnet();
83+
84+
// Execute transaction and commit state changes
85+
let _ = evm.transact_commit(&config.tx);
86+
87+
// Benchmarks measure execution speed, not correctness
88+
}
89+
90+
/// Result type for benchmarking files
91+
enum BenchmarkResult {
92+
/// Successfully benchmarked
93+
Success,
94+
/// File is not a state test (e.g., difficulty test)
95+
/// or filtered out by `skip_test` function
96+
Skip,
97+
/// Actual error during benchmarking
98+
Error(Box<dyn std::error::Error>),
99+
}
100+
101+
/// Check if a deserialization error indicates a non-state-test file
102+
///
103+
/// This function detects when a JSON file cannot be deserialized as a state test
104+
/// because it's missing required fields like `env`, `pre`, `post`, or `transaction`.
105+
/// This typically indicates the file is a different type of test (e.g., difficulty test)
106+
/// rather than a state test.
107+
///
108+
/// # Arguments
109+
///
110+
/// * `error` - The serde JSON deserialization error
111+
///
112+
/// # Returns
113+
///
114+
/// `true` if the error indicates a non-state-test file, `false` otherwise
115+
fn is_non_state_test_error(error: &serde_json::Error) -> bool {
116+
// Check if the error message indicates missing required fields like "env"
117+
// State tests require these fields, but other test types (like difficulty tests) don't have them
118+
let error_msg = error.to_string();
119+
error_msg.contains("missing field")
120+
&& (error_msg.contains("`env`")
121+
|| error_msg.contains("`pre`")
122+
|| error_msg.contains("`post`")
123+
|| error_msg.contains("`transaction`"))
124+
}
125+
126+
/// Benchmark a single test file
127+
fn benchmark_test_file(criterion: &mut Criterion, path: &Path) -> BenchmarkResult {
128+
if skip_test(path) {
129+
return BenchmarkResult::Skip;
130+
}
131+
132+
let s = match std::fs::read_to_string(path) {
133+
Ok(s) => s,
134+
Err(e) => return BenchmarkResult::Error(Box::new(e)),
135+
};
136+
137+
let suite: TestSuite = match serde_json::from_str(&s) {
138+
Ok(suite) => suite,
139+
Err(e) => {
140+
// Check if this is a non-state-test file (like difficulty tests)
141+
if is_non_state_test_error(&e) {
142+
return BenchmarkResult::Skip;
143+
}
144+
return BenchmarkResult::Error(Box::new(e));
145+
}
146+
};
147+
148+
let Some(group_name) = path.parent().and_then(|p| p.as_os_str().to_str()) else {
149+
return BenchmarkResult::Error(Box::new(std::io::Error::other("Invalid group name")));
150+
};
151+
let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
152+
return BenchmarkResult::Error(Box::new(std::io::Error::other("Invalid file name")));
153+
};
154+
for (_name, test_unit) in suite.0 {
155+
// Benchmark only the first valid spec/test to avoid excessive runs
156+
for (spec_name, tests) in &test_unit.post {
157+
// Skip Constantinople spec never actually deployed on Ethereum mainnet)
158+
// Refer to the SpecName enum documentation for more details
159+
if *spec_name == SpecName::Constantinople {
160+
continue;
161+
}
162+
163+
// Take first test that we can create a valid config for
164+
for test in tests {
165+
if let Some(config) = BenchConfig::new(&test_unit, test, spec_name) {
166+
let mut criterion_group = criterion.benchmark_group(group_name);
167+
criterion_group.bench_function(file_name, |b| {
168+
b.iter_batched(|| &config, execute_bench_iteration, BatchSize::SmallInput);
169+
});
170+
criterion_group.finish();
171+
172+
// Only benchmark first valid test per test unit
173+
return BenchmarkResult::Success;
174+
}
175+
}
176+
}
177+
}
178+
179+
BenchmarkResult::Success
180+
}
181+
182+
/// Run benchmarks on all test files
183+
pub fn run_benchmarks(test_files: Vec<PathBuf>, warmup: Option<u64>, time: Option<u64>) {
184+
let mut criterion = Criterion::default()
185+
.warm_up_time(std::time::Duration::from_millis(warmup.unwrap_or(300)))
186+
.measurement_time(std::time::Duration::from_secs(time.unwrap_or(2)))
187+
.without_plots();
188+
189+
let mut success_count = 0;
190+
let mut skip_count = 0;
191+
let mut error_count = 0;
192+
193+
for path in &test_files {
194+
match benchmark_test_file(&mut criterion, path) {
195+
BenchmarkResult::Success => success_count += 1,
196+
BenchmarkResult::Skip => {
197+
skip_count += 1;
198+
}
199+
BenchmarkResult::Error(e) => {
200+
eprintln!("Failed to benchmark {}: {}", path.display(), e);
201+
error_count += 1;
202+
}
203+
}
204+
}
205+
206+
criterion.final_summary();
207+
208+
println!(
209+
"\nBenchmark summary: {} succeeded, {} skipped, {} failed out of {} total",
210+
success_count,
211+
skip_count,
212+
error_count,
213+
test_files.len()
214+
);
215+
}

bins/revme/src/cmd/statetest/runner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ pub fn find_all_json_tests(path: &Path) -> Vec<PathBuf> {
8484

8585
/// Check if a test should be skipped based on its filename
8686
/// Some tests are known to be problematic or take too long
87-
fn skip_test(path: &Path) -> bool {
87+
pub(super) fn skip_test(path: &Path) -> bool {
8888
let path_str = path.to_str().unwrap_or_default();
8989

9090
// Skip tets that have storage for newly created account.

0 commit comments

Comments
 (0)