Skip to content

Commit cebab85

Browse files
authored
feat(l1,l2): make write path APIs async (#2336)
**Motivation** Some of our sync APIs can produce starving when running on Tokio due to taking a long time to reach the next `await`-point. Specifically, writing to the DB tends to take a long time, which blocks other tasks, sometimes the whole runtime due to how the scheduler in Tokio works. Thus, we need a way to inform the runtime we're going to be working for a while, and give it control while we wait for stuff. **Description** Take the mutable APIs for the DB and mark them `async`. Then bubble that up to their users. Then make the functions non-blocking by using `spawn_blocking` to run on the blocking thread, releasing the runtime to handle more work. The DB writing APIs had to change to pass-by-value to satisfy the borrow-checker in the blocking task context. I think I can use proper lifetime bounds with a helper crate, if that's preferred. The values were already being discarded after passing to the DB, so passing by value should not be a problem either way. Special considerations: - For some work performed before benchmarks and EF tests which are inherently synchronous I opted for calling with an ad-hoc runtime instance and `block_on`, as that might reduce the changes needed by localizing the async work. If desired, that can be changed up to making a `tokio::main`. The same is true for some setup functions for tests. - For the DBs I had to separate the Tokio import. This is because they need to compile with L2, which means provers' custom compilers, and those don't support the networking functions in the stdlib, which Tokio with full features (as the workspace dep declares) brings them in. - The InMemoryDB was left untouched other than updating the interfaces, given hashmap access should be quick enough. - I need to comment on [this hack](https://github.com/lambdaclass/ethrex/pull/2336/files#diff-264636d3ee6ee67bd6e136b8c98f74152de6a8e2a07f597cfb5f622d4e0d815aR143-R146): `and_then` can't be used on futures and everything became a mess without that little helper. - I'm unsure about whether or not we also want to cover the read APIs, at least for consistency I would think so, but for now I left them out. closes #2402
1 parent b672cd0 commit cebab85

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1295
-870
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Perf
44

5+
#### 2025-04-01
6+
7+
- Asyncify DB write APIs, as well as its users [#2336](https://github.com/lambdaclass/ethrex/pull/2336)
8+
59
#### 2025-03-30
610

711
- Faster block import, use a slice instead of copy

Cargo.lock

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ ethrex-prover = { path = "./crates/l2/prover" }
5151
tracing = { version = "0.1", features = ["log"] }
5252
tracing-subscriber = { version = "0.3.0", features = ["env-filter"] }
5353

54+
async-trait = "0.1.88"
5455
ethereum-types = { version = "0.15.1", features = ["serialize"] }
5556
serde = { version = "1.0.203", features = ["derive"] }
5657
serde_json = "1.0.117"

bench/criterion_benchmark.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ use tracing_subscriber::{filter::Directive, EnvFilter, FmtSubscriber};
1515
fn block_import() {
1616
let data_dir = DEFAULT_DATADIR;
1717
set_datadir(data_dir);
18-
1918
remove_db(data_dir);
2019

2120
let evm_engine = "revm".to_owned().try_into().unwrap();
2221

2322
let network = "../../test_data/genesis-l2-ci.json";
2423

25-
import_blocks(
24+
let rt = tokio::runtime::Runtime::new().unwrap();
25+
rt.block_on(import_blocks(
2626
"../../test_data/l2-1k-erc20.rlp",
2727
data_dir,
2828
network,
2929
evm_engine,
30-
);
30+
));
3131
}
3232

3333
pub fn criterion_benchmark(c: &mut Criterion) {

cmd/ef_tests/blockchain/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ serde_json.workspace = true
1515
bytes.workspace = true
1616
hex.workspace = true
1717
lazy_static.workspace = true
18+
tokio.workspace = true
1819

1920
[dev-dependencies]
2021
datatest-stable = "0.2.9"

cmd/ef_tests/blockchain/test_runner.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ use ethrex_common::types::{
88
use ethrex_rlp::decode::RLPDecode;
99
use ethrex_storage::{EngineType, Store};
1010

11-
pub fn run_ef_test(test_key: &str, test: &TestUnit) {
11+
pub async fn run_ef_test(test_key: &str, test: &TestUnit) {
1212
// check that the decoded genesis block header matches the deserialized one
1313
let genesis_rlp = test.genesis_rlp.clone();
1414
let decoded_block = CoreBlock::decode(&genesis_rlp).unwrap();
1515
let genesis_block_header = CoreBlockHeader::from(test.genesis_block_header.clone());
1616
assert_eq!(decoded_block.header, genesis_block_header);
1717

18-
let store = build_store_for_test(test);
18+
let store = build_store_for_test(test).await;
1919

2020
// Check world_state
2121
check_prestate_against_db(test_key, test, &store);
@@ -33,7 +33,7 @@ pub fn run_ef_test(test_key: &str, test: &TestUnit) {
3333
let hash = block.hash();
3434

3535
// Attempt to add the block as the head of the chain
36-
let chain_result = blockchain.add_block(block);
36+
let chain_result = blockchain.add_block(block).await;
3737
match chain_result {
3838
Err(error) => {
3939
assert!(
@@ -50,7 +50,7 @@ pub fn run_ef_test(test_key: &str, test: &TestUnit) {
5050
test_key,
5151
block_fixture.expect_exception.clone().unwrap()
5252
);
53-
apply_fork_choice(&store, hash, hash, hash).unwrap();
53+
apply_fork_choice(&store, hash, hash, hash).await.unwrap();
5454
}
5555
}
5656
}
@@ -82,12 +82,13 @@ pub fn parse_test_file(path: &Path) -> HashMap<String, TestUnit> {
8282
}
8383

8484
/// Creats a new in-memory store and adds the genesis state
85-
pub fn build_store_for_test(test: &TestUnit) -> Store {
85+
pub async fn build_store_for_test(test: &TestUnit) -> Store {
8686
let store =
8787
Store::new("store.db", EngineType::InMemory).expect("Failed to build DB for testing");
8888
let genesis = test.get_genesis();
8989
store
9090
.add_initial_state(genesis)
91+
.await
9192
.expect("Failed to add genesis state");
9293
store
9394
}

cmd/ef_tests/blockchain/tests/cancun.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use std::path::Path;
1414
// test or set of tests
1515

1616
fn parse_and_execute_until_cancun(path: &Path) -> datatest_stable::Result<()> {
17+
let rt = tokio::runtime::Runtime::new().unwrap();
1718
let tests = parse_test_file(path);
1819

1920
for (test_key, test) in tests {
@@ -22,22 +23,23 @@ fn parse_and_execute_until_cancun(path: &Path) -> datatest_stable::Result<()> {
2223
// them. This produces false positives
2324
continue;
2425
}
25-
run_ef_test(&test_key, &test);
26+
rt.block_on(run_ef_test(&test_key, &test));
2627
}
2728

2829
Ok(())
2930
}
3031

3132
#[allow(dead_code)]
3233
fn parse_and_execute_all(path: &Path) -> datatest_stable::Result<()> {
34+
let rt = tokio::runtime::Runtime::new().unwrap();
3335
let tests = parse_test_file(path);
3436

3537
for (test_key, test) in tests {
3638
if test.network < Network::Merge {
3739
// These tests fall into the not supported forks. This produces false positives
3840
continue;
3941
}
40-
run_ef_test(&test_key, &test);
42+
rt.block_on(run_ef_test(&test_key, &test));
4143
}
4244

4345
Ok(())

cmd/ef_tests/blockchain/tests/prague.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const SKIPPED_TEST: [&str; 2] = [
1313

1414
#[allow(dead_code)]
1515
fn parse_and_execute(path: &Path) -> datatest_stable::Result<()> {
16+
let rt = tokio::runtime::Runtime::new().unwrap();
1617
let tests = parse_test_file(path);
1718

1819
for (test_key, test) in tests {
@@ -21,7 +22,7 @@ fn parse_and_execute(path: &Path) -> datatest_stable::Result<()> {
2122
continue;
2223
}
2324

24-
run_ef_test(&test_key, &test);
25+
rt.block_on(run_ef_test(&test_key, &test));
2526
}
2627
Ok(())
2728
}

cmd/ef_tests/blockchain/tests/shanghai.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use ef_tests_blockchain::{
66
};
77

88
fn parse_and_execute(path: &Path) -> datatest_stable::Result<()> {
9+
let rt = tokio::runtime::Runtime::new().unwrap();
910
let tests = parse_test_file(path);
1011

1112
for (test_key, test) in tests {
@@ -14,7 +15,7 @@ fn parse_and_execute(path: &Path) -> datatest_stable::Result<()> {
1415
continue;
1516
}
1617

17-
run_ef_test(&test_key, &test);
18+
rt.block_on(run_ef_test(&test_key, &test));
1819
}
1920
Ok(())
2021
}

cmd/ef_tests/state/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ revm = { version = "19.0.0", features = [
2929
"optional_no_base_fee",
3030
"optional_block_gas_limit",
3131
], default-features = false }
32+
tokio.workspace = true
3233

3334
[dev-dependencies]
3435
hex = "0.4.3"

cmd/ef_tests/state/runner/levm_runner.rs

+10-9
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use ethrex_vm::backends;
1818
use keccak_hash::keccak;
1919
use std::collections::HashMap;
2020

21-
pub fn run_ef_test(test: &EFTest) -> Result<EFTestReport, EFTestRunnerError> {
21+
pub async fn run_ef_test(test: &EFTest) -> Result<EFTestReport, EFTestRunnerError> {
2222
// There are some tests that don't have a hash, unwrap will panic
2323
let hash = test
2424
._info
@@ -35,7 +35,7 @@ pub fn run_ef_test(test: &EFTest) -> Result<EFTestReport, EFTestRunnerError> {
3535
if !test.post.has_vector_for_fork(vector, *fork) {
3636
continue;
3737
}
38-
match run_ef_test_tx(vector, test, fork) {
38+
match run_ef_test_tx(vector, test, fork).await {
3939
Ok(_) => continue,
4040
Err(EFTestRunnerError::VMInitializationFailed(reason)) => {
4141
ef_test_report_fork.register_vm_initialization_failure(reason, *vector);
@@ -78,16 +78,16 @@ pub fn run_ef_test(test: &EFTest) -> Result<EFTestReport, EFTestRunnerError> {
7878
Ok(ef_test_report)
7979
}
8080

81-
pub fn run_ef_test_tx(
81+
pub async fn run_ef_test_tx(
8282
vector: &TestVector,
8383
test: &EFTest,
8484
fork: &Fork,
8585
) -> Result<(), EFTestRunnerError> {
86-
let mut db = utils::load_initial_state_levm(test);
86+
let mut db = utils::load_initial_state_levm(test).await;
8787
let mut levm = prepare_vm_for_tx(vector, test, fork, &mut db)?;
8888
ensure_pre_state(&levm, test)?;
8989
let levm_execution_result = levm.execute();
90-
ensure_post_state(&levm_execution_result, vector, test, fork, &mut db)?;
90+
ensure_post_state(&levm_execution_result, vector, test, fork, &mut db).await?;
9191
Ok(())
9292
}
9393

@@ -282,7 +282,7 @@ fn exception_is_expected(
282282
})
283283
}
284284

285-
pub fn ensure_post_state(
285+
pub async fn ensure_post_state(
286286
levm_execution_result: &Result<ExecutionReport, VMError>,
287287
vector: &TestVector,
288288
test: &EFTest,
@@ -326,7 +326,7 @@ pub fn ensure_post_state(
326326
.to_owned(),
327327
)
328328
})?;
329-
let pos_state_root = post_state_root(&levm_account_updates, test);
329+
let pos_state_root = post_state_root(&levm_account_updates, test).await;
330330
let expected_post_state_root_hash =
331331
test.post.vector_post_value(vector, *fork).hash;
332332
if expected_post_state_root_hash != pos_state_root {
@@ -381,12 +381,13 @@ pub fn ensure_post_state(
381381
Ok(())
382382
}
383383

384-
pub fn post_state_root(account_updates: &[AccountUpdate], test: &EFTest) -> H256 {
385-
let (initial_state, block_hash) = utils::load_initial_state(test);
384+
pub async fn post_state_root(account_updates: &[AccountUpdate], test: &EFTest) -> H256 {
385+
let (initial_state, block_hash) = utils::load_initial_state(test).await;
386386
initial_state
387387
.database()
388388
.unwrap()
389389
.apply_account_updates(block_hash, account_updates)
390+
.await
390391
.unwrap()
391392
.unwrap()
392393
}

cmd/ef_tests/state/runner/mod.rs

+10-10
Original file line numberDiff line numberDiff line change
@@ -70,27 +70,27 @@ pub struct EFTestRunnerOptions {
7070
pub revm: bool,
7171
}
7272

73-
pub fn run_ef_tests(
73+
pub async fn run_ef_tests(
7474
ef_tests: Vec<EFTest>,
7575
opts: &EFTestRunnerOptions,
7676
) -> Result<(), EFTestRunnerError> {
7777
let mut reports = report::load()?;
7878
if reports.is_empty() {
7979
if opts.revm {
80-
run_with_revm(&mut reports, &ef_tests, opts)?;
80+
run_with_revm(&mut reports, &ef_tests, opts).await?;
8181
return Ok(());
8282
} else {
83-
run_with_levm(&mut reports, &ef_tests, opts)?;
83+
run_with_levm(&mut reports, &ef_tests, opts).await?;
8484
}
8585
}
8686
if opts.summary {
8787
return Ok(());
8888
}
89-
re_run_with_revm(&mut reports, &ef_tests, opts)?;
89+
re_run_with_revm(&mut reports, &ef_tests, opts).await?;
9090
write_report(&reports)
9191
}
9292

93-
fn run_with_levm(
93+
async fn run_with_levm(
9494
reports: &mut Vec<EFTestReport>,
9595
ef_tests: &[EFTest],
9696
opts: &EFTestRunnerOptions,
@@ -113,7 +113,7 @@ fn run_with_levm(
113113
if !opts.spinner && opts.verbose {
114114
println!("Running test: {:?}", test.name);
115115
}
116-
let ef_test_report = match levm_runner::run_ef_test(test) {
116+
let ef_test_report = match levm_runner::run_ef_test(test).await {
117117
Ok(ef_test_report) => ef_test_report,
118118
Err(EFTestRunnerError::Internal(err)) => return Err(EFTestRunnerError::Internal(err)),
119119
non_internal_errors => {
@@ -154,7 +154,7 @@ fn run_with_levm(
154154
}
155155

156156
/// ### Runs all tests with REVM
157-
fn run_with_revm(
157+
async fn run_with_revm(
158158
reports: &mut Vec<EFTestReport>,
159159
ef_tests: &[EFTest],
160160
opts: &EFTestRunnerOptions,
@@ -183,7 +183,7 @@ fn run_with_revm(
183183
),
184184
opts.spinner,
185185
);
186-
let ef_test_report = match revm_runner::_run_ef_test_revm(test) {
186+
let ef_test_report = match revm_runner::_run_ef_test_revm(test).await {
187187
Ok(ef_test_report) => ef_test_report,
188188
Err(EFTestRunnerError::Internal(err)) => return Err(EFTestRunnerError::Internal(err)),
189189
non_internal_errors => {
@@ -210,7 +210,7 @@ fn run_with_revm(
210210
Ok(())
211211
}
212212

213-
fn re_run_with_revm(
213+
async fn re_run_with_revm(
214214
reports: &mut [EFTestReport],
215215
ef_tests: &[EFTest],
216216
opts: &EFTestRunnerOptions,
@@ -262,7 +262,7 @@ fn re_run_with_revm(
262262
})
263263
.unwrap(),
264264
failed_test_report,
265-
) {
265+
).await {
266266
Ok(re_run_report) => {
267267
failed_test_report.register_re_run_report(re_run_report.clone());
268268
}

0 commit comments

Comments
 (0)