Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Perf

### 2026-05-14

- Reduce BAL parallel-path overhead: overlap merkleization with execution, memoize per-BAL code derivation, swap `CachingDatabase` `RwLock<HashMap>` for `DashMap` to remove rayon-worker contention, and move per-tx BAL validation inside the exec closure [#6639](https://github.com/lambdaclass/ethrex/pull/6639)

### 2026-04-27

- Reduce peak disk usage during snap sync by moving SST files into the temp DB instead of copying [#6532](https://github.com/lambdaclass/ethrex/pull/6532)
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 5 additions & 7 deletions cmd/ethrex/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -973,13 +973,11 @@ pub async fn import_blocks_bench(
_ => warn!("Failed to add block {number} with hash {hash:#x}"),
})?;

// TODO: replace this
// This sleep is because we have a background process writing to disk the last layer
// And until it's done we can't execute the new block
// Because this wants to compare against running a real node in terms of reported performance
// It takes less than 500ms, so this is good enough, but we should report the performance
// without taking into account that wait.
tokio::time::sleep(Duration::from_millis(500)).await;
// Wait for the trie-update worker's Phase 2 (disk write of bottom-most
// diff layer) and Phase 3 (in-memory layer removal) for the block just
// applied to drain. Keeps the next block's per-block timer from
// absorbing the previous block's background persistence cost.
store.wait_for_persistence_idle().await?;
}

// Make head canonical and label all special blocks correctly.
Expand Down
48 changes: 26 additions & 22 deletions crates/blockchain/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -859,36 +859,40 @@ impl Blockchain {
const NUM_WORKERS: usize = 16;
let parent_state_root = parent_header.state_root;

// === Stage A: Drain + accumulate all AccountUpdates ===
// BAL guarantees completeness, so we block until execution finishes.
let mut all_updates: FxHashMap<Address, AccountUpdate> = FxHashMap::default();
for updates in rx {
let current_length = queue_length.fetch_sub(1, Ordering::Acquire);
*max_queue_length = current_length.max(*max_queue_length);
for update in updates {
match all_updates.entry(update.address) {
Entry::Vacant(e) => {
e.insert(update);
}
Entry::Occupied(mut e) => {
e.get_mut().merge(update);
}
}
// === Stage A: receive the single BAL-derived batch ===
// execute_block_parallel calls bal_to_account_updates BEFORE the rayon tx
// loop and sends exactly one Vec<AccountUpdate>. Receiving once (instead of
// draining until channel close = exec end) lets Stage B's parallel storage
// roots overlap with parallel exec instead of serializing after it.
//
// BAL accounts are unique by address (one entry per touched address), so
// no merge step is needed — skip the FxHashMap detour entirely.
let updates: Vec<AccountUpdate> = match rx.recv() {
Ok(updates) => {
let current_length = queue_length.fetch_sub(1, Ordering::Acquire);
*max_queue_length = current_length.max(*max_queue_length);
updates
}
}
Err(_) => {
// Channel closed without a message — execution failed before
// bal_to_account_updates ran. Return empty work so the exec
// error surfaces in execution_result rather than being masked.
Vec::new()
}
Comment on lines +876 to +881
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Err(_) path continues through Stage C (16 trie opens)

When the channel is closed without a message (execution failure before bal_to_account_updates), the function returns Vec::new() and falls through to Stage C, which unconditionally spawns 16 threads to open the parent state trie even though all shards will have no items. Returning an empty AccountUpdatesList early in the Err arm would avoid this overhead without changing the visible behaviour, since the execution error surfaces via execution_result? regardless.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/blockchain.rs
Line: 876-881

Comment:
**`Err(_)` path continues through Stage C (16 trie opens)**

When the channel is closed without a message (execution failure before `bal_to_account_updates`), the function returns `Vec::new()` and falls through to Stage C, which unconditionally spawns 16 threads to open the parent state trie even though all shards will have no items. Returning an empty `AccountUpdatesList` early in the `Err` arm would avoid this overhead without changing the visible behaviour, since the execution error surfaces via `execution_result?` regardless.

How can I resolve this? If you propose a fix, please make it concise.

};
Comment on lines +870 to +882
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Single-recv invariant has no defensive check

handle_merkleization_bal now consumes exactly one message and then never touches rx again. If execute_block_parallel is ever modified to send a second batch, the extra message will sit in the unbounded channel and be silently dropped — the merkleizer will proceed with only the first batch, producing a wrong state root with no error. A defensive debug_assert!(rx.try_recv().is_err(), "expected exactly one batch from execute_block_parallel") after the Ok arm would catch any accidental protocol change during development.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/blockchain.rs
Line: 870-882

Comment:
**Single-recv invariant has no defensive check**

`handle_merkleization_bal` now consumes exactly one message and then never touches `rx` again. If `execute_block_parallel` is ever modified to send a second batch, the extra message will sit in the unbounded channel and be silently dropped — the merkleizer will proceed with only the first batch, producing a wrong state root with no error. A defensive `debug_assert!(rx.try_recv().is_err(), "expected exactly one batch from execute_block_parallel")` after the `Ok` arm would catch any accidental protocol change during development.

How can I resolve this? If you propose a fix, please make it concise.


// Extract witness accumulator before consuming updates
// Witness accumulator (clone since we move `updates` into Stage B below).
let accumulated_updates = if self.options.precompute_witnesses {
Some(all_updates.values().cloned().collect::<Vec<_>>())
Some(updates.clone())
} else {
None
};

// Extract code updates and build work items with pre-hashed addresses
// Build work items with pre-hashed addresses + extract code updates.
let mut code_updates: Vec<(H256, Code)> = Vec::new();
let mut accounts: Vec<(H256, AccountUpdate)> = Vec::with_capacity(all_updates.len());
for (addr, update) in all_updates {
let hashed = keccak(addr);
let mut accounts: Vec<(H256, AccountUpdate)> = Vec::with_capacity(updates.len());
for update in updates {
let hashed = keccak(update.address);
if let Some(info) = &update.info
&& let Some(code) = &update.code
{
Expand Down
64 changes: 64 additions & 0 deletions crates/guest-program/bin/openvm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 58 additions & 0 deletions crates/guest-program/bin/risc0/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading