|
| 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 | + println!( |
| 207 | + "\nBenchmark summary: {} succeeded, {} skipped, {} failed out of {} total", |
| 208 | + success_count, |
| 209 | + skip_count, |
| 210 | + error_count, |
| 211 | + test_files.len() |
| 212 | + ); |
| 213 | +} |
0 commit comments