Skip to content

Commit 169a1fb

Browse files
authored
fix(engine): flatten storage cache (#18880)
1 parent c661cd2 commit 169a1fb

File tree

1 file changed

+53
-89
lines changed

1 file changed

+53
-89
lines changed

crates/engine/tree/src/tree/cached_state.rs

Lines changed: 53 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
//! Execution cache implementation for block processing.
2-
use alloy_primitives::{Address, StorageKey, StorageValue, B256};
2+
use alloy_primitives::{
3+
map::{DefaultHashBuilder, HashSet},
4+
Address, StorageKey, StorageValue, B256,
5+
};
36
use metrics::Gauge;
47
use mini_moka::sync::CacheBuilder;
58
use reth_errors::ProviderResult;
@@ -14,7 +17,6 @@ use reth_trie::{
1417
updates::TrieUpdates, AccountProof, HashedPostState, HashedStorage, MultiProof,
1518
MultiProofTargets, StorageMultiProof, StorageProof, TrieInput,
1619
};
17-
use revm_primitives::map::DefaultHashBuilder;
1820
use std::{sync::Arc, time::Duration};
1921
use tracing::trace;
2022

@@ -300,65 +302,69 @@ pub(crate) struct ExecutionCache {
300302
/// Cache for contract bytecode, keyed by code hash.
301303
code_cache: Cache<B256, Option<Bytecode>>,
302304

303-
/// Per-account storage cache: outer cache keyed by Address, inner cache tracks that account’s
304-
/// storage slots.
305-
storage_cache: Cache<Address, AccountStorageCache>,
305+
/// Flattened storage cache: composite key of (`Address`, `StorageKey`) maps directly to
306+
/// values.
307+
storage_cache: Cache<(Address, StorageKey), Option<StorageValue>>,
306308

307309
/// Cache for basic account information (nonce, balance, code hash).
308310
account_cache: Cache<Address, Option<Account>>,
309311
}
310312

311313
impl ExecutionCache {
312-
/// Get storage value from hierarchical cache.
314+
/// Get storage value from flattened cache.
313315
///
314316
/// Returns a `SlotStatus` indicating whether:
315-
/// - `NotCached`: The account's storage cache doesn't exist
316-
/// - `Empty`: The slot exists in the account's cache but is empty
317+
/// - `NotCached`: The storage slot is not in the cache
318+
/// - `Empty`: The slot exists in the cache but is empty
317319
/// - `Value`: The slot exists and has a specific value
318320
pub(crate) fn get_storage(&self, address: &Address, key: &StorageKey) -> SlotStatus {
319-
match self.storage_cache.get(address) {
321+
match self.storage_cache.get(&(*address, *key)) {
320322
None => SlotStatus::NotCached,
321-
Some(account_cache) => account_cache.get_storage(key),
323+
Some(None) => SlotStatus::Empty,
324+
Some(Some(value)) => SlotStatus::Value(value),
322325
}
323326
}
324327

325-
/// Insert storage value into hierarchical cache
328+
/// Insert storage value into flattened cache
326329
pub(crate) fn insert_storage(
327330
&self,
328331
address: Address,
329332
key: StorageKey,
330333
value: Option<StorageValue>,
331334
) {
332-
self.insert_storage_bulk(address, [(key, value)]);
335+
self.storage_cache.insert((address, key), value);
333336
}
334337

335-
/// Insert multiple storage values into hierarchical cache for a single account
338+
/// Insert multiple storage values into flattened cache for a single account
336339
///
337-
/// This method is optimized for inserting multiple storage values for the same address
338-
/// by doing the account cache lookup only once instead of for each key-value pair.
340+
/// This method inserts multiple storage values for the same address directly
341+
/// into the flattened cache.
339342
pub(crate) fn insert_storage_bulk<I>(&self, address: Address, storage_entries: I)
340343
where
341344
I: IntoIterator<Item = (StorageKey, Option<StorageValue>)>,
342345
{
343-
let account_cache = self.storage_cache.get(&address).unwrap_or_else(|| {
344-
let account_cache = AccountStorageCache::default();
345-
self.storage_cache.insert(address, account_cache.clone());
346-
account_cache
347-
});
348-
349346
for (key, value) in storage_entries {
350-
account_cache.insert_storage(key, value);
347+
self.storage_cache.insert((address, key), value);
351348
}
352349
}
353350

354-
/// Invalidate storage for specific account
355-
pub(crate) fn invalidate_account_storage(&self, address: &Address) {
356-
self.storage_cache.invalidate(address);
357-
}
358-
359351
/// Returns the total number of storage slots cached across all accounts
360352
pub(crate) fn total_storage_slots(&self) -> usize {
361-
self.storage_cache.iter().map(|addr| addr.len()).sum()
353+
self.storage_cache.entry_count() as usize
354+
}
355+
356+
/// Invalidates the storage for all addresses in the set
357+
pub(crate) fn invalidate_storages(&self, addresses: HashSet<&Address>) {
358+
// NOTE: this must collect because the invalidate function should not be called while we
359+
// hold an iter for it
360+
let storage_entries = self
361+
.storage_cache
362+
.iter()
363+
.filter_map(|entry| addresses.contains(&entry.key().0).then_some(*entry.key()))
364+
.collect::<Vec<_>>();
365+
for key in storage_entries {
366+
self.storage_cache.invalidate(&key)
367+
}
362368
}
363369

364370
/// Inserts the post-execution state changes into the cache.
@@ -385,6 +391,7 @@ impl ExecutionCache {
385391
self.code_cache.insert(*code_hash, Some(Bytecode(bytecode.clone())));
386392
}
387393

394+
let mut invalidated_accounts = HashSet::default();
388395
for (addr, account) in &state_updates.state {
389396
// If the account was not modified, as in not changed and not destroyed, then we have
390397
// nothing to do w.r.t. this particular account and can move on
@@ -397,7 +404,7 @@ impl ExecutionCache {
397404
// Invalidate the account cache entry if destroyed
398405
self.account_cache.invalidate(addr);
399406

400-
self.invalidate_account_storage(addr);
407+
invalidated_accounts.insert(addr);
401408
continue
402409
}
403410

@@ -424,6 +431,9 @@ impl ExecutionCache {
424431
self.account_cache.insert(*addr, Some(Account::from(account_info)));
425432
}
426433

434+
// invalidate storage for all destroyed accounts
435+
self.invalidate_storages(invalidated_accounts);
436+
427437
Ok(())
428438
}
429439
}
@@ -452,11 +462,11 @@ impl ExecutionCacheBuilder {
452462
const TIME_TO_IDLE: Duration = Duration::from_secs(3600); // 1 hour
453463

454464
let storage_cache = CacheBuilder::new(self.storage_cache_entries)
455-
.weigher(|_key: &Address, value: &AccountStorageCache| -> u32 {
456-
// values based on results from measure_storage_cache_overhead test
457-
let base_weight = 39_000;
458-
let slots_weight = value.len() * 218;
459-
(base_weight + slots_weight) as u32
465+
.weigher(|_key: &(Address, StorageKey), _value: &Option<StorageValue>| -> u32 {
466+
// Size of composite key (Address + StorageKey) + Option<StorageValue>
467+
// Address: 20 bytes, StorageKey: 32 bytes, Option<StorageValue>: 33 bytes
468+
// Plus some overhead for the hash map entry
469+
120_u32
460470
})
461471
.max_capacity(storage_cache_size)
462472
.time_to_live(EXPIRY_TIME)
@@ -573,56 +583,6 @@ impl SavedCache {
573583
}
574584
}
575585

576-
/// Cache for an individual account's storage slots.
577-
///
578-
/// This represents the second level of the hierarchical storage cache.
579-
/// Each account gets its own `AccountStorageCache` to store accessed storage slots.
580-
#[derive(Debug, Clone)]
581-
pub(crate) struct AccountStorageCache {
582-
/// Map of storage keys to their cached values.
583-
slots: Cache<StorageKey, Option<StorageValue>>,
584-
}
585-
586-
impl AccountStorageCache {
587-
/// Create a new [`AccountStorageCache`]
588-
pub(crate) fn new(max_slots: u64) -> Self {
589-
Self {
590-
slots: CacheBuilder::new(max_slots).build_with_hasher(DefaultHashBuilder::default()),
591-
}
592-
}
593-
594-
/// Get a storage value from this account's cache.
595-
/// - `NotCached`: The slot is not in the cache
596-
/// - `Empty`: The slot is empty
597-
/// - `Value`: The slot has a specific value
598-
pub(crate) fn get_storage(&self, key: &StorageKey) -> SlotStatus {
599-
match self.slots.get(key) {
600-
None => SlotStatus::NotCached,
601-
Some(None) => SlotStatus::Empty,
602-
Some(Some(value)) => SlotStatus::Value(value),
603-
}
604-
}
605-
606-
/// Insert a storage value
607-
pub(crate) fn insert_storage(&self, key: StorageKey, value: Option<StorageValue>) {
608-
self.slots.insert(key, value);
609-
}
610-
611-
/// Returns the number of slots in the cache
612-
pub(crate) fn len(&self) -> usize {
613-
self.slots.entry_count() as usize
614-
}
615-
}
616-
617-
impl Default for AccountStorageCache {
618-
fn default() -> Self {
619-
// With weigher and max_capacity in place, this number represents
620-
// the maximum number of entries that can be stored, not the actual
621-
// memory usage which is controlled by storage cache's max_capacity.
622-
Self::new(1_000_000)
623-
}
624-
}
625-
626586
#[cfg(test)]
627587
mod tests {
628588
use super::*;
@@ -697,32 +657,36 @@ mod tests {
697657

698658
#[test]
699659
fn measure_storage_cache_overhead() {
700-
let (base_overhead, cache) = measure_allocation(|| AccountStorageCache::new(1000));
701-
println!("Base AccountStorageCache overhead: {base_overhead} bytes");
660+
let (base_overhead, cache) =
661+
measure_allocation(|| ExecutionCacheBuilder::default().build_caches(1000));
662+
println!("Base ExecutionCache overhead: {base_overhead} bytes");
702663
let mut rng = rand::rng();
703664

665+
let address = Address::random();
704666
let key = StorageKey::random();
705667
let value = StorageValue::from(rng.random::<u128>());
706668
let (first_slot, _) = measure_allocation(|| {
707-
cache.insert_storage(key, Some(value));
669+
cache.insert_storage(address, key, Some(value));
708670
});
709671
println!("First slot insertion overhead: {first_slot} bytes");
710672

711673
const TOTAL_SLOTS: usize = 10_000;
712674
let (test_slots, _) = measure_allocation(|| {
713675
for _ in 0..TOTAL_SLOTS {
676+
let addr = Address::random();
714677
let key = StorageKey::random();
715678
let value = StorageValue::from(rng.random::<u128>());
716-
cache.insert_storage(key, Some(value));
679+
cache.insert_storage(addr, key, Some(value));
717680
}
718681
});
719682
println!("Average overhead over {} slots: {} bytes", TOTAL_SLOTS, test_slots / TOTAL_SLOTS);
720683

721684
println!("\nTheoretical sizes:");
685+
println!("Address size: {} bytes", size_of::<Address>());
722686
println!("StorageKey size: {} bytes", size_of::<StorageKey>());
723687
println!("StorageValue size: {} bytes", size_of::<StorageValue>());
724688
println!("Option<StorageValue> size: {} bytes", size_of::<Option<StorageValue>>());
725-
println!("Option<B256> size: {} bytes", size_of::<Option<B256>>());
689+
println!("(Address, StorageKey) size: {} bytes", size_of::<(Address, StorageKey)>());
726690
}
727691

728692
#[test]

0 commit comments

Comments
 (0)