From 90bc91753a4786413125766f073208e6d3e46d2b Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Thu, 15 Jan 2026 16:07:02 +0000 Subject: [PATCH] perf(provider): flatten trie updates before persisting Accumulates TrieUpdatesSorted from all blocks and batch writes at the end. Prior benchmark done in #19739 indicates this should improve batch processing performance. Key changes: - Accumulate trie updates into a single TrieUpdatesSorted during save_blocks loop - Pass accumulated overlay to write_trie_changesets so each block sees correct state - Call write_trie_updates_sorted once after the loop with accumulated updates Closes #20611 --- .../src/providers/database/provider.rs | 133 +++++++++++++++++- 1 file changed, 129 insertions(+), 4 deletions(-) diff --git a/crates/storage/provider/src/providers/database/provider.rs b/crates/storage/provider/src/providers/database/provider.rs index 692bc7737cd..1dc0a120d91 100644 --- a/crates/storage/provider/src/providers/database/provider.rs +++ b/crates/storage/provider/src/providers/database/provider.rs @@ -496,6 +496,8 @@ impl DatabaseProvider DatabaseProvider().unwrap(); + cursor.upsert(account_nibbles_key.clone(), &node0).unwrap(); + } + + // Simulate save_blocks accumulation logic: + // For block 1: write changeset with no overlay (sees db state = node0) + // For block 2: write changeset with overlay containing block 1's updates (sees node1) + + let trie_updates_block1 = TrieUpdatesSorted::new( + vec![(account_nibbles, Some(node1.clone()))], + B256Map::default(), + ); + let trie_updates_block2 = TrieUpdatesSorted::new( + vec![(account_nibbles, Some(node2.clone()))], + B256Map::default(), + ); + + // Accumulator starts empty + let mut trie_updates_acc = TrieUpdatesSorted::default(); + + // Block 1: no overlay (sees db) + let overlay = if trie_updates_acc.is_empty() { None } else { Some(&trie_updates_acc) }; + provider_rw.write_trie_changesets(1, &trie_updates_block1, overlay).unwrap(); + trie_updates_acc.extend_ref(&trie_updates_block1); + + // Block 2: overlay contains block 1's updates + let overlay = if trie_updates_acc.is_empty() { None } else { Some(&trie_updates_acc) }; + provider_rw.write_trie_changesets(2, &trie_updates_block2, overlay).unwrap(); + trie_updates_acc.extend_ref(&trie_updates_block2); + + // Write all accumulated trie updates in single batch + provider_rw.write_trie_updates_sorted(&trie_updates_acc).unwrap(); + + // Block 1 changeset should see the pre-existing db value (node0) + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_read::().unwrap(); + let entries = + cursor.walk_dup(Some(1u64), None).unwrap().collect::, _>>().unwrap(); + assert_eq!( + entries, + vec![( + 1u64, + TrieChangeSetsEntry { + nibbles: account_nibbles_subkey.clone(), + node: Some(node0) + } + )] + ); + } + + // Block 2 changeset should see block 1's update (node1) via the overlay + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_read::().unwrap(); + let entries = + cursor.walk_dup(Some(2u64), None).unwrap().collect::, _>>().unwrap(); + assert_eq!( + entries, + vec![( + 2u64, + TrieChangeSetsEntry { nibbles: account_nibbles_subkey, node: Some(node1) } + )] + ); + } + + // Final trie state should reflect the last update (node2) + { + let mut cursor = provider_rw.tx_ref().cursor_read::().unwrap(); + let entry = cursor.seek_exact(account_nibbles_key).unwrap(); + assert_eq!(entry.map(|(_, v)| v), Some(node2)); + } + + provider_rw.commit().unwrap(); + } }