// This file is part of Substrate. // Copyright (C) 2022 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Trie Cache //! //! Provides an implementation of the [`TrieCache`](trie_db::TrieCache) trait. //! The implementation is split into three types [`SharedTrieCache`], [`LocalTrieCache`] and //! [`TrieCache`]. The [`SharedTrieCache`] is the instance that should be kept around for the entire //! lifetime of the node. It will store all cached trie nodes and values on a global level. Then //! there is the [`LocalTrieCache`] that should be kept around per state instance requested from the //! backend. As there are very likely multiple accesses to the state per instance, this //! [`LocalTrieCache`] is used to cache the nodes and the values before they are merged back to the //! shared instance. Last but not least there is the [`TrieCache`] that is being used per access to //! the state. It will use the [`SharedTrieCache`] and the [`LocalTrieCache`] to fulfill cache //! requests. If both of them don't provide the requested data it will be inserted into the //! [`LocalTrieCache`] and then later into the [`SharedTrieCache`]. //! //! The [`SharedTrieCache`] is bound to some maximum number of bytes. It is ensured that it never //! runs above this limit. However as long as data is cached inside a [`LocalTrieCache`] it isn't //! taken into account when limiting the [`SharedTrieCache`]. This means that for the lifetime of a //! [`LocalTrieCache`] the actual memory usage could be above the allowed maximum. use crate::{Error, NodeCodec}; use hash_db::Hasher; use nohash_hasher::BuildNoHashHasher; use parking_lot::{Mutex, MutexGuard}; use schnellru::LruMap; use shared_cache::{ValueCacheKey, ValueCacheRef}; use std::{ collections::HashMap, sync::{ atomic::{AtomicU64, Ordering}, Arc, }, time::Duration, }; use trie_db::{node::NodeOwned, CachedValue}; mod shared_cache; pub use shared_cache::SharedTrieCache; use self::shared_cache::ValueCacheKeyHash; const LOG_TARGET: &str = "trie-cache"; /// The maximum amount of time we'll wait trying to acquire the shared cache lock /// when the local cache is dropped and synchronized with the share cache. /// /// This is just a failsafe; normally this should never trigger. const SHARED_CACHE_WRITE_LOCK_TIMEOUT: Duration = Duration::from_millis(100); /// The maximum number of existing keys in the shared cache that a single local cache /// can promote to the front of the LRU cache in one go. /// /// If we have a big shared cache and the local cache hits all of those keys we don't /// want to spend forever bumping all of them. const SHARED_NODE_CACHE_MAX_PROMOTED_KEYS: u32 = 1792; /// Same as [`SHARED_NODE_CACHE_MAX_PROMOTED_KEYS`]. const SHARED_VALUE_CACHE_MAX_PROMOTED_KEYS: u32 = 1792; /// The maximum portion of the shared cache (in percent) that a single local /// cache can replace in one go. /// /// We don't want a single local cache instance to have the ability to replace /// everything in the shared cache. const SHARED_NODE_CACHE_MAX_REPLACE_PERCENT: usize = 33; /// Same as [`SHARED_NODE_CACHE_MAX_REPLACE_PERCENT`]. const SHARED_VALUE_CACHE_MAX_REPLACE_PERCENT: usize = 33; /// The maximum inline capacity of the local cache, in bytes. /// /// This is just an upper limit; since the maps are resized in powers of two /// their actual size will most likely not exactly match this. const LOCAL_NODE_CACHE_MAX_INLINE_SIZE: usize = 512 * 1024; /// Same as [`LOCAL_NODE_CACHE_MAX_INLINE_SIZE`]. const LOCAL_VALUE_CACHE_MAX_INLINE_SIZE: usize = 512 * 1024; /// The maximum size of the memory allocated on the heap by the local cache, in bytes. const LOCAL_NODE_CACHE_MAX_HEAP_SIZE: usize = 2 * 1024 * 1024; /// Same as [`LOCAL_NODE_CACHE_MAX_HEAP_SIZE`]. const LOCAL_VALUE_CACHE_MAX_HEAP_SIZE: usize = 4 * 1024 * 1024; /// The size of the shared cache. #[derive(Debug, Clone, Copy)] pub struct CacheSize(usize); impl CacheSize { /// An unlimited cache size. pub const fn unlimited() -> Self { CacheSize(usize::MAX) } /// A cache size `bytes` big. pub const fn new(bytes: usize) -> Self { CacheSize(bytes) } } /// A limiter for the local node cache. This makes sure the local cache doesn't grow too big. #[derive(Default)] pub struct LocalNodeCacheLimiter { /// The current size (in bytes) of data allocated by this cache on the heap. /// /// This doesn't include the size of the map itself. current_heap_size: usize, } impl schnellru::Limiter> for LocalNodeCacheLimiter where H: AsRef<[u8]> + std::fmt::Debug, { type KeyToInsert<'a> = H; type LinkType = u32; #[inline] fn is_over_the_limit(&self, length: usize) -> bool { // Only enforce the limit if there's more than one element to make sure // we can always add a new element to the cache. if length <= 1 { return false } self.current_heap_size > LOCAL_NODE_CACHE_MAX_HEAP_SIZE } #[inline] fn on_insert<'a>( &mut self, _length: usize, key: H, cached_node: NodeCached, ) -> Option<(H, NodeCached)> { self.current_heap_size += cached_node.heap_size(); Some((key, cached_node)) } #[inline] fn on_replace( &mut self, _length: usize, _old_key: &mut H, _new_key: H, old_node: &mut NodeCached, new_node: &mut NodeCached, ) -> bool { debug_assert_eq!(_old_key.as_ref().len(), _new_key.as_ref().len()); self.current_heap_size = self.current_heap_size + new_node.heap_size() - old_node.heap_size(); true } #[inline] fn on_removed(&mut self, _key: &mut H, cached_node: &mut NodeCached) { self.current_heap_size -= cached_node.heap_size(); } #[inline] fn on_cleared(&mut self) { self.current_heap_size = 0; } #[inline] fn on_grow(&mut self, new_memory_usage: usize) -> bool { new_memory_usage <= LOCAL_NODE_CACHE_MAX_INLINE_SIZE } } /// A limiter for the local value cache. This makes sure the local cache doesn't grow too big. #[derive(Default)] pub struct LocalValueCacheLimiter { /// The current size (in bytes) of data allocated by this cache on the heap. /// /// This doesn't include the size of the map itself. current_heap_size: usize, } impl schnellru::Limiter, CachedValue> for LocalValueCacheLimiter where H: AsRef<[u8]>, { type KeyToInsert<'a> = ValueCacheRef<'a, H>; type LinkType = u32; #[inline] fn is_over_the_limit(&self, length: usize) -> bool { // Only enforce the limit if there's more than one element to make sure // we can always add a new element to the cache. if length <= 1 { return false } self.current_heap_size > LOCAL_VALUE_CACHE_MAX_HEAP_SIZE } #[inline] fn on_insert( &mut self, _length: usize, key: Self::KeyToInsert<'_>, value: CachedValue, ) -> Option<(ValueCacheKey, CachedValue)> { self.current_heap_size += key.storage_key.len(); Some((key.into(), value)) } #[inline] fn on_replace( &mut self, _length: usize, _old_key: &mut ValueCacheKey, _new_key: ValueCacheRef, _old_value: &mut CachedValue, _new_value: &mut CachedValue, ) -> bool { debug_assert_eq!(_old_key.storage_key.len(), _new_key.storage_key.len()); true } #[inline] fn on_removed(&mut self, key: &mut ValueCacheKey, _: &mut CachedValue) { self.current_heap_size -= key.storage_key.len(); } #[inline] fn on_cleared(&mut self) { self.current_heap_size = 0; } #[inline] fn on_grow(&mut self, new_memory_usage: usize) -> bool { new_memory_usage <= LOCAL_VALUE_CACHE_MAX_INLINE_SIZE } } /// A struct to gather hit/miss stats to aid in debugging the performance of the cache. #[derive(Default)] struct HitStats { shared_hits: AtomicU64, shared_fetch_attempts: AtomicU64, local_hits: AtomicU64, local_fetch_attempts: AtomicU64, } impl std::fmt::Display for HitStats { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { let shared_hits = self.shared_hits.load(Ordering::Relaxed); let shared_fetch_attempts = self.shared_fetch_attempts.load(Ordering::Relaxed); let local_hits = self.local_hits.load(Ordering::Relaxed); let local_fetch_attempts = self.local_fetch_attempts.load(Ordering::Relaxed); if shared_fetch_attempts == 0 && local_hits == 0 { write!(fmt, "empty") } else { let percent_local = (local_hits as f32 / local_fetch_attempts as f32) * 100.0; let percent_shared = (shared_hits as f32 / shared_fetch_attempts as f32) * 100.0; write!( fmt, "local hit rate = {}% [{}/{}], shared hit rate = {}% [{}/{}]", percent_local as u32, local_hits, local_fetch_attempts, percent_shared as u32, shared_hits, shared_fetch_attempts ) } } } /// A struct to gather hit/miss stats for the node cache and the value cache. #[derive(Default)] struct TrieHitStats { node_cache: HitStats, value_cache: HitStats, } /// An internal struct to store the cached trie nodes. pub(crate) struct NodeCached { /// The cached node. pub node: NodeOwned, /// Whether this node was fetched from the shared cache or not. pub is_from_shared_cache: bool, } impl NodeCached { /// Returns the number of bytes allocated on the heap by this node. fn heap_size(&self) -> usize { self.node.size_in_bytes() - std::mem::size_of::>() } } type NodeCacheMap = LruMap, LocalNodeCacheLimiter, schnellru::RandomState>; type ValueCacheMap = LruMap< ValueCacheKey, CachedValue, LocalValueCacheLimiter, BuildNoHashHasher>, >; type ValueAccessSet = LruMap>; /// The local trie cache. /// /// This cache should be used per state instance created by the backend. One state instance is /// referring to the state of one block. It will cache all the accesses that are done to the state /// which could not be fullfilled by the [`SharedTrieCache`]. These locally cached items are merged /// back to the shared trie cache when this instance is dropped. /// /// When using [`Self::as_trie_db_cache`] or [`Self::as_trie_db_mut_cache`], it will lock Mutexes. /// So, it is important that these methods are not called multiple times, because they otherwise /// deadlock. pub struct LocalTrieCache { /// The shared trie cache that created this instance. shared: SharedTrieCache, /// The local cache for the trie nodes. node_cache: Mutex>, /// The local cache for the values. value_cache: Mutex>, /// Keeps track of all values accessed in the shared cache. /// /// This will be used to ensure that these nodes are brought to the front of the lru when this /// local instance is merged back to the shared cache. This can actually lead to collision when /// two [`ValueCacheKey`]s with different storage roots and keys map to the same hash. However, /// as we only use this set to update the lru position it is fine, even if we bring the wrong /// value to the top. The important part is that we always get the correct value from the value /// cache for a given key. shared_value_cache_access: Mutex, stats: TrieHitStats, } impl LocalTrieCache { /// Return self as a [`TrieDB`](trie_db::TrieDB) compatible cache. /// /// The given `storage_root` needs to be the storage root of the trie this cache is used for. pub fn as_trie_db_cache(&self, storage_root: H::Out) -> TrieCache<'_, H> { let value_cache = ValueCache::ForStorageRoot { storage_root, local_value_cache: self.value_cache.lock(), shared_value_cache_access: self.shared_value_cache_access.lock(), buffered_value: None, }; TrieCache { shared_cache: self.shared.clone(), local_cache: self.node_cache.lock(), value_cache, stats: &self.stats, } } /// Return self as [`TrieDBMut`](trie_db::TrieDBMut) compatible cache. /// /// After finishing all operations with [`TrieDBMut`](trie_db::TrieDBMut) and having obtained /// the new storage root, [`TrieCache::merge_into`] should be called to update this local /// cache instance. If the function is not called, cached data is just thrown away and not /// propagated to the shared cache. So, accessing these new items will be slower, but nothing /// would break because of this. pub fn as_trie_db_mut_cache(&self) -> TrieCache<'_, H> { TrieCache { shared_cache: self.shared.clone(), local_cache: self.node_cache.lock(), value_cache: ValueCache::Fresh(Default::default()), stats: &self.stats, } } } impl Drop for LocalTrieCache { fn drop(&mut self) { tracing::debug!( target: LOG_TARGET, "Local node trie cache dropped: {}", self.stats.node_cache ); tracing::debug!( target: LOG_TARGET, "Local value trie cache dropped: {}", self.stats.value_cache ); let mut shared_inner = match self.shared.write_lock_inner() { Some(inner) => inner, None => { tracing::warn!( target: LOG_TARGET, "Timeout while trying to acquire a write lock for the shared trie cache" ); return }, }; shared_inner.node_cache_mut().update(self.node_cache.get_mut().drain()); shared_inner.value_cache_mut().update( self.value_cache.get_mut().drain(), self.shared_value_cache_access.get_mut().drain().map(|(key, ())| key), ); } } /// The abstraction of the value cache for the [`TrieCache`]. enum ValueCache<'a, H: Hasher> { /// The value cache is fresh, aka not yet associated to any storage root. /// This is used for example when a new trie is being build, to cache new values. Fresh(HashMap, CachedValue>), /// The value cache is already bound to a specific storage root. ForStorageRoot { shared_value_cache_access: MutexGuard<'a, ValueAccessSet>, local_value_cache: MutexGuard<'a, ValueCacheMap>, storage_root: H::Out, // The shared value cache needs to be temporarily locked when reading from it // so we need to clone the value that is returned, but we need to be able to // return a reference to the value, so we just buffer it here. buffered_value: Option>, }, } impl ValueCache<'_, H> { /// Get the value for the given `key`. fn get<'a>( &'a mut self, key: &[u8], shared_cache: &SharedTrieCache, stats: &HitStats, ) -> Option<&CachedValue> { stats.local_fetch_attempts.fetch_add(1, Ordering::Relaxed); match self { Self::Fresh(map) => if let Some(value) = map.get(key) { stats.local_hits.fetch_add(1, Ordering::Relaxed); Some(value) } else { None }, Self::ForStorageRoot { local_value_cache, shared_value_cache_access, storage_root, buffered_value, } => { // We first need to look up in the local cache and then the shared cache. // It can happen that some value is cached in the shared cache, but the // weak reference of the data can not be upgraded anymore. This for example // happens when the node is dropped that contains the strong reference to the data. // // So, the logic of the trie would lookup the data and the node and store both // in our local caches. let hash = ValueCacheKey::hash_data(key, storage_root); if let Some(value) = local_value_cache .peek_by_hash(hash.raw(), |existing_key, _| { existing_key.is_eq(storage_root, key) }) { stats.local_hits.fetch_add(1, Ordering::Relaxed); return Some(value) } stats.shared_fetch_attempts.fetch_add(1, Ordering::Relaxed); if let Some(value) = shared_cache.peek_value_by_hash(hash, storage_root, key) { stats.shared_hits.fetch_add(1, Ordering::Relaxed); shared_value_cache_access.insert(hash, ()); *buffered_value = Some(value.clone()); return buffered_value.as_ref() } None }, } } /// Insert some new `value` under the given `key`. fn insert(&mut self, key: &[u8], value: CachedValue) { match self { Self::Fresh(map) => { map.insert(key.into(), value); }, Self::ForStorageRoot { local_value_cache, storage_root, .. } => { local_value_cache.insert(ValueCacheRef::new(key, *storage_root), value); }, } } } /// The actual [`TrieCache`](trie_db::TrieCache) implementation. /// /// If this instance was created for using it with a [`TrieDBMut`](trie_db::TrieDBMut), it needs to /// be merged back into the [`LocalTrieCache`] with [`Self::merge_into`] after all operations are /// done. pub struct TrieCache<'a, H: Hasher> { shared_cache: SharedTrieCache, local_cache: MutexGuard<'a, NodeCacheMap>, value_cache: ValueCache<'a, H>, stats: &'a TrieHitStats, } impl<'a, H: Hasher> TrieCache<'a, H> { /// Merge this cache into the given [`LocalTrieCache`]. /// /// This function is only required to be called when this instance was created through /// [`LocalTrieCache::as_trie_db_mut_cache`], otherwise this method is a no-op. The given /// `storage_root` is the new storage root that was obtained after finishing all operations /// using the [`TrieDBMut`](trie_db::TrieDBMut). pub fn merge_into(self, local: &LocalTrieCache, storage_root: H::Out) { let cache = if let ValueCache::Fresh(cache) = self.value_cache { cache } else { return }; if !cache.is_empty() { let mut value_cache = local.value_cache.lock(); let partial_hash = ValueCacheKey::hash_partial_data(&storage_root); cache.into_iter().for_each(|(k, v)| { let hash = ValueCacheKeyHash::from_hasher_and_storage_key(partial_hash.clone(), &k); let k = ValueCacheRef { storage_root, storage_key: &k, hash }; value_cache.insert(k, v); }); } } } impl<'a, H: Hasher> trie_db::TrieCache> for TrieCache<'a, H> { fn get_or_insert_node( &mut self, hash: H::Out, fetch_node: &mut dyn FnMut() -> trie_db::Result, H::Out, Error>, ) -> trie_db::Result<&NodeOwned, H::Out, Error> { let mut is_local_cache_hit = true; self.stats.node_cache.local_fetch_attempts.fetch_add(1, Ordering::Relaxed); // First try to grab the node from the local cache. let node = self.local_cache.get_or_insert_fallible(hash, || { is_local_cache_hit = false; // It was not in the local cache; try the shared cache. self.stats.node_cache.shared_fetch_attempts.fetch_add(1, Ordering::Relaxed); if let Some(node) = self.shared_cache.peek_node(&hash) { self.stats.node_cache.shared_hits.fetch_add(1, Ordering::Relaxed); tracing::trace!(target: LOG_TARGET, ?hash, "Serving node from shared cache"); return Ok(NodeCached:: { node: node.clone(), is_from_shared_cache: true }) } // It was not in the shared cache; try fetching it from the database. match fetch_node() { Ok(node) => { tracing::trace!(target: LOG_TARGET, ?hash, "Serving node from database"); Ok(NodeCached:: { node, is_from_shared_cache: false }) }, Err(error) => { tracing::trace!(target: LOG_TARGET, ?hash, "Serving node from database failed"); Err(error) }, } }); if is_local_cache_hit { tracing::trace!(target: LOG_TARGET, ?hash, "Serving node from local cache"); self.stats.node_cache.local_hits.fetch_add(1, Ordering::Relaxed); } Ok(&node? .expect("you can always insert at least one element into the local cache; qed") .node) } fn get_node(&mut self, hash: &H::Out) -> Option<&NodeOwned> { let mut is_local_cache_hit = true; self.stats.node_cache.local_fetch_attempts.fetch_add(1, Ordering::Relaxed); // First try to grab the node from the local cache. let cached_node = self.local_cache.get_or_insert_fallible(*hash, || { is_local_cache_hit = false; // It was not in the local cache; try the shared cache. self.stats.node_cache.shared_fetch_attempts.fetch_add(1, Ordering::Relaxed); if let Some(node) = self.shared_cache.peek_node(&hash) { self.stats.node_cache.shared_hits.fetch_add(1, Ordering::Relaxed); tracing::trace!(target: LOG_TARGET, ?hash, "Serving node from shared cache"); Ok(NodeCached:: { node: node.clone(), is_from_shared_cache: true }) } else { tracing::trace!(target: LOG_TARGET, ?hash, "Serving node from cache failed"); Err(()) } }); if is_local_cache_hit { tracing::trace!(target: LOG_TARGET, ?hash, "Serving node from local cache"); self.stats.node_cache.local_hits.fetch_add(1, Ordering::Relaxed); } match cached_node { Ok(Some(cached_node)) => Some(&cached_node.node), Ok(None) => { unreachable!( "you can always insert at least one element into the local cache; qed" ); }, Err(()) => None, } } fn lookup_value_for_key(&mut self, key: &[u8]) -> Option<&CachedValue> { let res = self.value_cache.get(key, &self.shared_cache, &self.stats.value_cache); tracing::trace!( target: LOG_TARGET, key = ?sp_core::hexdisplay::HexDisplay::from(&key), found = res.is_some(), "Looked up value for key", ); res } fn cache_value_for_key(&mut self, key: &[u8], data: CachedValue) { tracing::trace!( target: LOG_TARGET, key = ?sp_core::hexdisplay::HexDisplay::from(&key), "Caching value for key", ); self.value_cache.insert(key, data); } } #[cfg(test)] mod tests { use super::*; use trie_db::{Bytes, Trie, TrieDBBuilder, TrieDBMutBuilder, TrieHash, TrieMut}; type MemoryDB = crate::MemoryDB; type Layout = crate::LayoutV1; type Cache = super::SharedTrieCache; type Recorder = crate::recorder::Recorder; const TEST_DATA: &[(&[u8], &[u8])] = &[(b"key1", b"val1"), (b"key2", &[2; 64]), (b"key3", b"val3"), (b"key4", &[4; 64])]; const CACHE_SIZE_RAW: usize = 1024 * 10; const CACHE_SIZE: CacheSize = CacheSize::new(CACHE_SIZE_RAW); fn create_trie() -> (MemoryDB, TrieHash) { let mut db = MemoryDB::default(); let mut root = Default::default(); { let mut trie = TrieDBMutBuilder::::new(&mut db, &mut root).build(); for (k, v) in TEST_DATA { trie.insert(k, v).expect("Inserts data"); } } (db, root) } #[test] fn basic_cache_works() { let (db, root) = create_trie(); let shared_cache = Cache::new(CACHE_SIZE); let local_cache = shared_cache.local_cache(); { let mut cache = local_cache.as_trie_db_cache(root); let trie = TrieDBBuilder::::new(&db, &root).with_cache(&mut cache).build(); assert_eq!(TEST_DATA[0].1.to_vec(), trie.get(TEST_DATA[0].0).unwrap().unwrap()); } // Local cache wasn't dropped yet, so there should nothing in the shared caches. assert!(shared_cache.read_lock_inner().value_cache().lru.is_empty()); assert!(shared_cache.read_lock_inner().node_cache().lru.is_empty()); drop(local_cache); // Now we should have the cached items in the shared cache. assert!(shared_cache.read_lock_inner().node_cache().lru.len() >= 1); let cached_data = shared_cache .read_lock_inner() .value_cache() .lru .peek(&ValueCacheKey::new_value(TEST_DATA[0].0, root)) .unwrap() .clone(); assert_eq!(Bytes::from(TEST_DATA[0].1.to_vec()), cached_data.data().flatten().unwrap()); let fake_data = Bytes::from(&b"fake_data"[..]); let local_cache = shared_cache.local_cache(); shared_cache.write_lock_inner().unwrap().value_cache_mut().lru.insert( ValueCacheKey::new_value(TEST_DATA[1].0, root), (fake_data.clone(), Default::default()).into(), ); { let mut cache = local_cache.as_trie_db_cache(root); let trie = TrieDBBuilder::::new(&db, &root).with_cache(&mut cache).build(); // We should now get the "fake_data", because we inserted this manually to the cache. assert_eq!(b"fake_data".to_vec(), trie.get(TEST_DATA[1].0).unwrap().unwrap()); } } #[test] fn trie_db_mut_cache_works() { let (mut db, root) = create_trie(); let new_key = b"new_key".to_vec(); // Use some long value to not have it inlined let new_value = vec![23; 64]; let shared_cache = Cache::new(CACHE_SIZE); let mut new_root = root; { let local_cache = shared_cache.local_cache(); let mut cache = local_cache.as_trie_db_mut_cache(); { let mut trie = TrieDBMutBuilder::::from_existing(&mut db, &mut new_root) .with_cache(&mut cache) .build(); trie.insert(&new_key, &new_value).unwrap(); } cache.merge_into(&local_cache, new_root); } // After the local cache is dropped, all changes should have been merged back to the shared // cache. let cached_data = shared_cache .read_lock_inner() .value_cache() .lru .peek(&ValueCacheKey::new_value(new_key, new_root)) .unwrap() .clone(); assert_eq!(Bytes::from(new_value), cached_data.data().flatten().unwrap()); } #[test] fn trie_db_cache_and_recorder_work_together() { let (db, root) = create_trie(); let shared_cache = Cache::new(CACHE_SIZE); for i in 0..5 { // Clear some of the caches. if i == 2 { shared_cache.reset_node_cache(); } else if i == 3 { shared_cache.reset_value_cache(); } let local_cache = shared_cache.local_cache(); let recorder = Recorder::default(); { let mut cache = local_cache.as_trie_db_cache(root); let mut recorder = recorder.as_trie_recorder(root); let trie = TrieDBBuilder::::new(&db, &root) .with_cache(&mut cache) .with_recorder(&mut recorder) .build(); for (key, value) in TEST_DATA { assert_eq!(*value, trie.get(&key).unwrap().unwrap()); } } let storage_proof = recorder.drain_storage_proof(); let memory_db: MemoryDB = storage_proof.into_memory_db(); { let trie = TrieDBBuilder::::new(&memory_db, &root).build(); for (key, value) in TEST_DATA { assert_eq!(*value, trie.get(&key).unwrap().unwrap()); } } } } #[test] fn trie_db_mut_cache_and_recorder_work_together() { const DATA_TO_ADD: &[(&[u8], &[u8])] = &[(b"key11", &[45; 78]), (b"key33", &[78; 89])]; let (db, root) = create_trie(); let shared_cache = Cache::new(CACHE_SIZE); // Run this twice so that we use the data cache in the second run. for i in 0..5 { // Clear some of the caches. if i == 2 { shared_cache.reset_node_cache(); } else if i == 3 { shared_cache.reset_value_cache(); } let recorder = Recorder::default(); let local_cache = shared_cache.local_cache(); let mut new_root = root; { let mut db = db.clone(); let mut cache = local_cache.as_trie_db_cache(root); let mut recorder = recorder.as_trie_recorder(root); let mut trie = TrieDBMutBuilder::::from_existing(&mut db, &mut new_root) .with_cache(&mut cache) .with_recorder(&mut recorder) .build(); for (key, value) in DATA_TO_ADD { trie.insert(key, value).unwrap(); } } let storage_proof = recorder.drain_storage_proof(); let mut memory_db: MemoryDB = storage_proof.into_memory_db(); let mut proof_root = root; { let mut trie = TrieDBMutBuilder::::from_existing(&mut memory_db, &mut proof_root) .build(); for (key, value) in DATA_TO_ADD { trie.insert(key, value).unwrap(); } } assert_eq!(new_root, proof_root) } } #[test] fn cache_lru_works() { let (db, root) = create_trie(); let shared_cache = Cache::new(CACHE_SIZE); { let local_cache = shared_cache.local_cache(); let mut cache = local_cache.as_trie_db_cache(root); let trie = TrieDBBuilder::::new(&db, &root).with_cache(&mut cache).build(); for (k, _) in TEST_DATA { trie.get(k).unwrap().unwrap(); } } // Check that all items are there. assert!(shared_cache .read_lock_inner() .value_cache() .lru .iter() .map(|d| d.0) .all(|l| TEST_DATA.iter().any(|d| &*l.storage_key == d.0))); // Run this in a loop. The first time we check that with the filled value cache, // the expected values are at the top of the LRU. // The second run is using an empty value cache to ensure that we access the nodes. for _ in 0..2 { { let local_cache = shared_cache.local_cache(); let mut cache = local_cache.as_trie_db_cache(root); let trie = TrieDBBuilder::::new(&db, &root).with_cache(&mut cache).build(); for (k, _) in TEST_DATA.iter().take(2) { trie.get(k).unwrap().unwrap(); } } // Ensure that the accessed items are most recently used items of the shared value // cache. assert!(shared_cache .read_lock_inner() .value_cache() .lru .iter() .take(2) .map(|d| d.0) .all(|l| { TEST_DATA.iter().take(2).any(|d| &*l.storage_key == d.0) })); // Delete the value cache, so that we access the nodes. shared_cache.reset_value_cache(); } let most_recently_used_nodes = shared_cache .read_lock_inner() .node_cache() .lru .iter() .map(|d| *d.0) .collect::>(); { let local_cache = shared_cache.local_cache(); let mut cache = local_cache.as_trie_db_cache(root); let trie = TrieDBBuilder::::new(&db, &root).with_cache(&mut cache).build(); for (k, _) in TEST_DATA.iter().skip(2) { trie.get(k).unwrap().unwrap(); } } // Ensure that the most recently used nodes changed as well. assert_ne!( most_recently_used_nodes, shared_cache .read_lock_inner() .node_cache() .lru .iter() .map(|d| *d.0) .collect::>() ); } #[test] fn cache_respects_bounds() { let (mut db, root) = create_trie(); let shared_cache = Cache::new(CACHE_SIZE); { let local_cache = shared_cache.local_cache(); let mut new_root = root; { let mut cache = local_cache.as_trie_db_cache(root); { let mut trie = TrieDBMutBuilder::::from_existing(&mut db, &mut new_root) .with_cache(&mut cache) .build(); let value = vec![10u8; 100]; // Ensure we add enough data that would overflow the cache. for i in 0..CACHE_SIZE_RAW / 100 * 2 { trie.insert(format!("key{}", i).as_bytes(), &value).unwrap(); } } cache.merge_into(&local_cache, new_root); } } assert!(shared_cache.used_memory_size() < CACHE_SIZE_RAW); } }