diff --git a/src/coins.cpp b/src/coins.cpp index 7f2ffc38efa0..9f2a27c96846 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -13,6 +13,15 @@ TRACEPOINT_SEMAPHORE(utxocache, add); TRACEPOINT_SEMAPHORE(utxocache, spent); TRACEPOINT_SEMAPHORE(utxocache, uncache); +namespace { +void WarnOnCacheRehash(const CCoinsMap& cache_coins, size_t buckets_before) +{ + if (const size_t buckets_after{cache_coins.bucket_count()}; buckets_after > buckets_before) { + LogWarning("Coins cache buckets unexpectedly grew from %zu to %zu.", buckets_before, buckets_after); + } +} +} + std::optional CCoinsView::GetCoin(const COutPoint& outpoint) const { return std::nullopt; } uint256 CCoinsView::GetBestBlock() const { return uint256(); } std::vector CCoinsView::GetHeadBlocks() const { return std::vector(); } @@ -38,19 +47,76 @@ void CCoinsViewBacked::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& h std::unique_ptr CCoinsViewBacked::Cursor() const { return base->Cursor(); } size_t CCoinsViewBacked::EstimateSize() const { return base->EstimateSize(); } -CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic) : +CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic, size_t reserve_entries, float max_load_factor) : CCoinsViewBacked(baseIn), m_deterministic(deterministic), cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource) { + cacheCoins.max_load_factor(max_load_factor); + cacheCoins.reserve(reserve_entries); + Reset(); +} + +void CCoinsViewCache::Start() +{ + Assert(cacheCoins.empty()); + Assert(cachedCoinsUsage == 0); + Assert(hashBlock.IsNull()); + Assert(m_sentinel.second.Next() == &m_sentinel); + Assert(m_sentinel.second.Prev() == &m_sentinel); + (void)GetBestBlock(); // lazy init +} + +void CCoinsViewCache::Reset() noexcept +{ + cacheCoins.clear(); + cachedCoinsUsage = 0; + hashBlock.SetNull(); m_sentinel.second.SelfRef(m_sentinel); } -size_t CCoinsViewCache::DynamicMemoryUsage() const { +size_t CCoinsViewCache::ReservedEntries(size_t max_coins_cache_size_bytes, float max_load_factor) noexcept +{ + if (max_coins_cache_size_bytes == 0) return 0; + + assert(max_load_factor > 0); + + constexpr size_t BYTES_PER_ENTRY_LOWER_BOUND{sizeof(CoinsCachePair) + sizeof(void*)}; + const size_t bucket_bytes_per_entry_lower_bound{size_t(double(sizeof(void*)) / max_load_factor)}; + const size_t bytes_per_entry_lower_bound{BYTES_PER_ENTRY_LOWER_BOUND + bucket_bytes_per_entry_lower_bound}; + size_t result = max_coins_cache_size_bytes / bytes_per_entry_lower_bound; + // TODO remove + LogInfo("Reserving %zu entries (at least %zu bytes per entry) for a coins cache of size %zu bytes with max load factor %.2f", + result, bytes_per_entry_lower_bound, max_coins_cache_size_bytes, max_load_factor); + return result; +} + +void CCoinsViewCache::ReserveCache(size_t max_coins_cache_size_bytes, float max_load_factor) +{ + if (max_coins_cache_size_bytes == 0) return; + + assert(max_load_factor > 0); + assert(cacheCoins.max_load_factor() == max_load_factor); + + const size_t reserve_entries{std::min(ReservedEntries(max_coins_cache_size_bytes, max_load_factor), cacheCoins.max_size())}; + cacheCoins.reserve(reserve_entries); +} + +size_t CCoinsViewCache::DynamicMemoryUsage() const +{ return memusage::DynamicUsage(cacheCoins) + cachedCoinsUsage; } +size_t CCoinsViewCache::ActiveMemoryUsage() const +{ + const size_t node_pool_bytes{cacheCoins.get_allocator().resource()->BytesInUse()}; + const size_t bucket_array_bytes{memusage::MallocUsage(sizeof(void*) * cacheCoins.bucket_count())}; + return cachedCoinsUsage + node_pool_bytes + bucket_array_bytes; +} + CCoinsMap::iterator CCoinsViewCache::FetchCoin(const COutPoint &outpoint) const { + const size_t buckets_before{cacheCoins.bucket_count()}; const auto [ret, inserted] = cacheCoins.try_emplace(outpoint); + if (inserted) WarnOnCacheRehash(cacheCoins, buckets_before); if (inserted) { if (auto coin{base->GetCoin(outpoint)}) { ret->second.coin = std::move(*coin); @@ -78,7 +144,9 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi if (coin.out.scriptPubKey.IsUnspendable()) return; CCoinsMap::iterator it; bool inserted; + const size_t buckets_before{cacheCoins.bucket_count()}; std::tie(it, inserted) = cacheCoins.emplace(std::piecewise_construct, std::forward_as_tuple(outpoint), std::tuple<>()); + if (inserted) WarnOnCacheRehash(cacheCoins, buckets_before); bool fresh = false; if (!possible_overwrite) { if (!it->second.coin.IsSpent()) { @@ -116,8 +184,10 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi void CCoinsViewCache::EmplaceCoinInternalDANGER(COutPoint&& outpoint, Coin&& coin) { const auto mem_usage{coin.DynamicMemoryUsage()}; + const size_t buckets_before{cacheCoins.bucket_count()}; auto [it, inserted] = cacheCoins.try_emplace(std::move(outpoint), std::move(coin)); if (inserted) { + WarnOnCacheRehash(cacheCoins, buckets_before); CCoinsCacheEntry::SetDirty(*it, m_sentinel); cachedCoinsUsage += mem_usage; } @@ -193,8 +263,10 @@ void CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& ha if (!it->second.IsDirty()) { // TODO a cursor can only contain dirty entries continue; } + const size_t buckets_before{cacheCoins.bucket_count()}; auto [itUs, inserted]{cacheCoins.try_emplace(it->first)}; if (inserted) { + WarnOnCacheRehash(cacheCoins, buckets_before); if (it->second.IsFresh() && it->second.coin.IsSpent()) { cacheCoins.erase(itUs); // TODO fresh coins should have been removed at spend } else { @@ -253,15 +325,13 @@ void CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& ha hashBlock = hashBlockIn; } -void CCoinsViewCache::Flush(bool will_reuse_cache) +void CCoinsViewCache::Flush() { auto cursor{CoinsViewCacheCursor(m_sentinel, cacheCoins, /*will_erase=*/true)}; base->BatchWrite(cursor, hashBlock); cacheCoins.clear(); - if (will_reuse_cache) { - ReallocateCache(); - } cachedCoinsUsage = 0; + m_sentinel.second.SelfRef(m_sentinel); } void CCoinsViewCache::Sync() @@ -309,10 +379,14 @@ void CCoinsViewCache::ReallocateCache() { // Cache should be empty when we're calling this. assert(cacheCoins.size() == 0); + LogInfo("Reallocating coins cache (%zu buckets, %zu chunks)", + cacheCoins.bucket_count(), m_cache_coins_memory_resource.NumAllocatedChunks()); + const float max_load_factor{cacheCoins.max_load_factor()}; cacheCoins.~CCoinsMap(); m_cache_coins_memory_resource.~CCoinsMapMemoryResource(); ::new (&m_cache_coins_memory_resource) CCoinsMapMemoryResource{}; ::new (&cacheCoins) CCoinsMap{0, SaltedOutpointHasher{/*deterministic=*/m_deterministic}, CCoinsMap::key_equal{}, &m_cache_coins_memory_resource}; + cacheCoins.max_load_factor(max_load_factor); } void CCoinsViewCache::SanityCheck() const @@ -350,6 +424,7 @@ void CCoinsViewCache::SanityCheck() const static const uint64_t MIN_TRANSACTION_OUTPUT_WEIGHT{WITNESS_SCALE_FACTOR * ::GetSerializeSize(CTxOut())}; static const uint64_t MAX_OUTPUTS_PER_BLOCK{MAX_BLOCK_WEIGHT / MIN_TRANSACTION_OUTPUT_WEIGHT}; +const size_t CCoinsViewCache::CONNECT_BLOCK_VIEW_RESERVE_ENTRIES{size_t(2 * MAX_OUTPUTS_PER_BLOCK)}; const Coin& AccessByTxid(const CCoinsViewCache& view, const Txid& txid) { diff --git a/src/coins.h b/src/coins.h index 6da53829996d..8e9005d2a553 100644 --- a/src/coins.h +++ b/src/coins.h @@ -377,13 +377,22 @@ class CCoinsViewCache : public CCoinsViewBacked mutable size_t cachedCoinsUsage{0}; public: - CCoinsViewCache(CCoinsView *baseIn, bool deterministic = false); + static constexpr float DEFAULT_MAX_LOAD_FACTOR{1.0f}; + + static size_t ReservedEntries(size_t max_coins_cache_size_bytes, float max_load_factor) noexcept; + static const size_t CONNECT_BLOCK_VIEW_RESERVE_ENTRIES; + float GetMaxLoadFactor() const noexcept { return cacheCoins.max_load_factor(); } + + CCoinsViewCache(CCoinsView* baseIn, bool deterministic = false, size_t reserve_entries = 0, float max_load_factor = DEFAULT_MAX_LOAD_FACTOR); /** * By deleting the copy constructor, we prevent accidentally using it when one intends to create a cache on top of a base cache. */ CCoinsViewCache(const CCoinsViewCache &) = delete; + void Start(); + void Reset() noexcept; + // Standard CCoinsView methods std::optional GetCoin(const COutPoint& outpoint) const override; bool HaveCoin(const COutPoint &outpoint) const override; @@ -439,10 +448,8 @@ class CCoinsViewCache : public CCoinsViewBacked * Push the modifications applied to this cache to its base and wipe local state. * Failure to call this method or Sync() before destruction will cause the changes * to be forgotten. - * If will_reuse_cache is false, the cache will retain the same memory footprint - * after flushing and should be destroyed to deallocate. */ - void Flush(bool will_reuse_cache = true); + void Flush(); /** * Push the modifications applied to this cache to its base while retaining @@ -464,6 +471,9 @@ class CCoinsViewCache : public CCoinsViewBacked //! Calculate the size of the cache (in bytes) size_t DynamicMemoryUsage() const; + //! Calculate the active memory usage of the cache (in bytes) + size_t ActiveMemoryUsage() const; + //! Check whether all prevouts of the transaction are present in the UTXO set represented by this view bool HaveInputs(const CTransaction& tx) const; @@ -474,6 +484,10 @@ class CCoinsViewCache : public CCoinsViewBacked //! See: https://stackoverflow.com/questions/42114044/how-to-release-unordered-map-memory void ReallocateCache(); + //! Preallocate the cache map bucket count based on the expected maximum cache size. + //! This avoids expensive rehashing during cache growth. + void ReserveCache(size_t max_coins_cache_size_bytes, float max_load_factor); + //! Run an internal sanity check on the cache data structure. */ void SanityCheck() const; diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp index 930d843121a2..cb434268b9c8 100644 --- a/src/node/chainstate.cpp +++ b/src/node/chainstate.cpp @@ -114,7 +114,8 @@ static ChainstateLoadResult CompleteChainstateInitialization( } // The on-disk coinsdb is now in a good state, create the cache - chainstate->InitCoinsCache(chainman.m_total_coinstip_cache * init_cache_fraction); + chainstate->InitCoinsCache(chainman.m_total_coinstip_cache * init_cache_fraction, + CCoinsViewCache::CONNECT_BLOCK_VIEW_RESERVE_ENTRIES); assert(chainstate->CanFlushToDisk()); if (!is_coinsview_empty(*chainstate)) { diff --git a/src/support/allocators/pool.h b/src/support/allocators/pool.h index abca09ad9018..0fb3f819ef55 100644 --- a/src/support/allocators/pool.h +++ b/src/support/allocators/pool.h @@ -97,6 +97,8 @@ class PoolResource final */ const size_t m_chunk_size_bytes; + size_t m_bytes_in_use{0}; + /** * Contains all allocated pools of memory, used to free the data in the destructor. */ @@ -155,11 +157,13 @@ class PoolResource final void AllocateChunk() { // if there is still any available memory left, put it into the freelist. - size_t remaining_available_bytes = std::distance(m_available_memory_it, m_available_memory_end); - if (0 != remaining_available_bytes) { - ASAN_UNPOISON_MEMORY_REGION(m_available_memory_it, sizeof(ListNode)); - PlacementAddToList(m_available_memory_it, m_free_lists[remaining_available_bytes / ELEM_ALIGN_BYTES]); - ASAN_POISON_MEMORY_REGION(m_available_memory_it, sizeof(ListNode)); + if (m_available_memory_it != nullptr && m_available_memory_end != nullptr) { + size_t remaining_available_bytes = std::distance(m_available_memory_it, m_available_memory_end); + if (0 != remaining_available_bytes) { + ASAN_UNPOISON_MEMORY_REGION(m_available_memory_it, sizeof(ListNode)); + PlacementAddToList(m_available_memory_it, m_free_lists[remaining_available_bytes / ELEM_ALIGN_BYTES]); + ASAN_POISON_MEMORY_REGION(m_available_memory_it, sizeof(ListNode)); + } } void* storage = ::operator new (m_chunk_size_bytes, std::align_val_t{ELEM_ALIGN_BYTES}); @@ -227,6 +231,7 @@ class PoolResource final auto* next{m_free_lists[num_alignments]->m_next}; ASAN_POISON_MEMORY_REGION(m_free_lists[num_alignments], sizeof(ListNode)); ASAN_UNPOISON_MEMORY_REGION(m_free_lists[num_alignments], bytes); + m_bytes_in_use += num_alignments * ELEM_ALIGN_BYTES; return std::exchange(m_free_lists[num_alignments], next); } @@ -239,6 +244,7 @@ class PoolResource final // Make sure we use the right amount of bytes for that freelist (might be rounded up), ASAN_UNPOISON_MEMORY_REGION(m_available_memory_it, round_bytes); + m_bytes_in_use += num_alignments * ELEM_ALIGN_BYTES; return std::exchange(m_available_memory_it, m_available_memory_it + round_bytes); } @@ -253,6 +259,7 @@ class PoolResource final { if (IsFreeListUsable(bytes, alignment)) { const std::size_t num_alignments = NumElemAlignBytes(bytes); + m_bytes_in_use -= num_alignments * ELEM_ALIGN_BYTES; // put the memory block into the linked list. We can placement construct the FreeList // into the memory since we can be sure the alignment is correct. ASAN_UNPOISON_MEMORY_REGION(p, sizeof(ListNode)); @@ -279,6 +286,8 @@ class PoolResource final { return m_chunk_size_bytes; } + + size_t BytesInUse() const noexcept { return m_bytes_in_use; } }; diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp index 6396fce60ac3..3f2a315f943e 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -1120,4 +1120,50 @@ BOOST_AUTO_TEST_CASE(ccoins_emplace_duplicate_keeps_usage_balanced) BOOST_CHECK(cache.AccessCoin(outpoint) == coin1); } +BOOST_AUTO_TEST_CASE(ccoins_reset) +{ + CCoinsView root; + CCoinsViewCacheTest cache{&root}; + + const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()}; + + const Coin coin{CTxOut{m_rng.randrange(10), CScript{} << m_rng.randbytes(CScriptBase::STATIC_SIZE + 1)}, 1, false}; + cache.EmplaceCoinInternalDANGER(COutPoint{outpoint}, Coin{coin}); + cache.SetBestBlock(uint256::ONE); + cache.SelfTest(); + + BOOST_CHECK(cache.AccessCoin(outpoint) == coin); + BOOST_CHECK(!cache.AccessCoin(outpoint).IsSpent()); + BOOST_CHECK_EQUAL(cache.GetCacheSize(), 1); + BOOST_CHECK_EQUAL(cache.GetBestBlock(), uint256::ONE); + + cache.Reset(); + + BOOST_CHECK(cache.AccessCoin(outpoint).IsSpent()); + BOOST_CHECK_EQUAL(cache.GetCacheSize(), 0); + BOOST_CHECK_EQUAL(cache.GetBestBlock(), uint256::ZERO); +} + +BOOST_AUTO_TEST_CASE(ccoins_start) +{ + test_only_CheckFailuresAreExceptionsNotAborts mock_checks{}; + + CCoinsView root; + CCoinsViewCacheTest cache{&root}; + + // Start fails if state wasn't reset + cache.Start(); + cache.SetBestBlock(uint256::ONE); + BOOST_CHECK_THROW(cache.Start(), NonFatalCheckError); + + // Resetting allows start again + cache.Reset(); + cache.Start(); + + // Reset is idempotent + cache.Reset(); + cache.Reset(); + cache.Start(); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index 09595678ad98..aadd027ff1de 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -74,7 +74,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend } }, [&] { - coins_view_cache.Flush(/*will_reuse_cache=*/fuzzed_data_provider.ConsumeBool()); + coins_view_cache.Flush(); }, [&] { coins_view_cache.Sync(); @@ -85,6 +85,11 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend if (is_db && best_block.IsNull()) best_block = uint256::ONE; coins_view_cache.SetBestBlock(best_block); }, + [&] { + coins_view_cache.Reset(); + // Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite(). + if (is_db) coins_view_cache.SetBestBlock(uint256::ONE); + }, [&] { Coin move_to; (void)coins_view_cache.SpendCoin(random_out_point, fuzzed_data_provider.ConsumeBool() ? &move_to : nullptr); diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp index f57c25210e3c..bf646b30cb47 100644 --- a/src/test/fuzz/coinscache_sim.cpp +++ b/src/test/fuzz/coinscache_sim.cpp @@ -391,7 +391,7 @@ FUZZ_TARGET(coinscache_sim) // Apply to simulation data. flush(); // Apply to real caches. - caches.back()->Flush(/*will_reuse_cache=*/provider.ConsumeBool()); + caches.back()->Flush(); }, [&]() { // Sync. @@ -401,6 +401,12 @@ FUZZ_TARGET(coinscache_sim) caches.back()->Sync(); }, + [&]() { // Reset. + sim_caches[caches.size()].Wipe(); + // Apply to real caches. + caches.back()->Reset(); + }, + [&]() { // GetCacheSize (void)caches.back()->GetCacheSize(); }, diff --git a/src/test/pool_tests.cpp b/src/test/pool_tests.cpp index 9965a520bf99..af184cedbdc7 100644 --- a/src/test/pool_tests.cpp +++ b/src/test/pool_tests.cpp @@ -186,6 +186,12 @@ BOOST_AUTO_TEST_CASE(memusage_test) auto max_nodes_per_chunk = resource.ChunkSizeBytes() / sizeof(Map::value_type); auto min_num_allocated_chunks = resource_map.size() / max_nodes_per_chunk + 1; BOOST_TEST(resource.NumAllocatedChunks() >= min_num_allocated_chunks); + + size_t chunks_before_clear = resource.NumAllocatedChunks(); + BOOST_CHECK(chunks_before_clear > 0); + resource_map.clear(); + BOOST_CHECK_EQUAL(resource.NumAllocatedChunks(), chunks_before_clear); + BOOST_CHECK_EQUAL(resource.BytesInUse(), 0); } PoolResourceTester::CheckAllDataAccountedFor(resource); diff --git a/src/test/validation_flush_tests.cpp b/src/test/validation_flush_tests.cpp index 66c284b97914..2ea0bccc1ca7 100644 --- a/src/test/validation_flush_tests.cpp +++ b/src/test/validation_flush_tests.cpp @@ -22,10 +22,15 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate) LOCK(::cs_main); CCoinsViewCache& view{chainstate.CoinsTip()}; - // Sanity: an empty cache should be ≲ 1 chunk (~ 256 KiB). - BOOST_CHECK_LT(view.DynamicMemoryUsage() / (256 * 1024.0), 1.1); + // Sanity: an empty cache should comfortably fit within its configured size. + const size_t empty_cache_bytes{view.DynamicMemoryUsage()}; + BOOST_CHECK_GT(empty_cache_bytes, 0U); + BOOST_CHECK_LT(empty_cache_bytes, chainstate.m_coinstip_cache_size_bytes); - constexpr size_t MAX_COINS_BYTES{8_MiB}; + // Use a small growth target on top of the current baseline so the test runs fast + // regardless of cache preallocation heuristics. + const size_t baseline_cache_bytes{view.ActiveMemoryUsage()}; + const size_t MAX_COINS_BYTES{baseline_cache_bytes + 8_MiB}; constexpr size_t MAX_MEMPOOL_BYTES{4_MiB}; constexpr size_t MAX_ATTEMPTS{50'000}; @@ -36,14 +41,14 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate) // OK → LARGE auto state{chainstate.GetCoinsCacheSizeState(MAX_COINS_BYTES, max_mempool_size_bytes)}; - for (size_t i{0}; i < MAX_ATTEMPTS && int64_t(view.DynamicMemoryUsage()) <= large_cap; ++i) { + for (size_t i{0}; i < MAX_ATTEMPTS && int64_t(view.ActiveMemoryUsage()) <= large_cap; ++i) { BOOST_CHECK_EQUAL(state, CoinsCacheSizeState::OK); AddTestCoin(m_rng, view); state = chainstate.GetCoinsCacheSizeState(MAX_COINS_BYTES, max_mempool_size_bytes); } // LARGE → CRITICAL - for (size_t i{0}; i < MAX_ATTEMPTS && int64_t(view.DynamicMemoryUsage()) <= full_cap; ++i) { + for (size_t i{0}; i < MAX_ATTEMPTS && int64_t(view.ActiveMemoryUsage()) <= full_cap; ++i) { BOOST_CHECK_EQUAL(state, CoinsCacheSizeState::LARGE); AddTestCoin(m_rng, view); state = chainstate.GetCoinsCacheSizeState(MAX_COINS_BYTES, max_mempool_size_bytes); diff --git a/src/validation.cpp b/src/validation.cpp index e919552ad230..a1b9e466eabd 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1852,10 +1852,14 @@ CoinsViews::CoinsViews(DBParams db_params, CoinsViewOptions options) : m_dbview{std::move(db_params), std::move(options)}, m_catcherview(&m_dbview) {} -void CoinsViews::InitCache() +void CoinsViews::InitCache(size_t cache_size_bytes, float max_load_factor, size_t connect_block_view_reserve_entries) { AssertLockHeld(::cs_main); - m_cacheview = std::make_unique(&m_catcherview); + + const size_t reserved_entries{CCoinsViewCache::ReservedEntries(cache_size_bytes, max_load_factor)}; + m_cacheview = std::make_unique(&m_catcherview, /*deterministic=*/false, reserved_entries, max_load_factor); + + m_connect_block_view = std::make_unique(&*m_cacheview, /*deterministic=*/false, connect_block_view_reserve_entries, max_load_factor); } Chainstate::Chainstate( @@ -1926,12 +1930,12 @@ void Chainstate::InitCoinsDB( m_coinsdb_cache_size_bytes = cache_size_bytes; } -void Chainstate::InitCoinsCache(size_t cache_size_bytes) +void Chainstate::InitCoinsCache(size_t cache_size_bytes, size_t connect_block_view_reserve_entries, float max_load_factor) { AssertLockHeld(::cs_main); assert(m_coins_views != nullptr); m_coinstip_cache_size_bytes = cache_size_bytes; - m_coins_views->InitCache(); + m_coins_views->InitCache(cache_size_bytes, max_load_factor, connect_block_view_reserve_entries); } // Note that though this is marked const, we may end up modifying `m_cached_finished_ibd`, which @@ -2710,12 +2714,12 @@ CoinsCacheSizeState Chainstate::GetCoinsCacheSizeState( { AssertLockHeld(::cs_main); const int64_t nMempoolUsage = m_mempool ? m_mempool->DynamicMemoryUsage() : 0; - int64_t cacheSize = CoinsTip().DynamicMemoryUsage(); + int64_t cacheSize = CoinsTip().ActiveMemoryUsage(); int64_t nTotalSpace = max_coins_cache_size_bytes + std::max(int64_t(max_mempool_size_bytes) - nMempoolUsage, 0); if (cacheSize > nTotalSpace) { - LogInfo("Cache size (%s) exceeds total space (%s)\n", cacheSize, nTotalSpace); + LogInfo("Coins cache active (%s) exceeds total space (%s)\n", cacheSize, nTotalSpace); return CoinsCacheSizeState::CRITICAL; } else if (cacheSize > LargeCoinsCacheThreshold(nTotalSpace)) { return CoinsCacheSizeState::LARGE; @@ -2790,8 +2794,10 @@ bool Chainstate::FlushStateToDisk( bool fCacheCritical = mode == FlushStateMode::IF_NEEDED && cache_state >= CoinsCacheSizeState::CRITICAL; // It's been a while since we wrote the block index and chain state to disk. Do this frequently, so we don't need to redownload or reindex after a crash. bool fPeriodicWrite = mode == FlushStateMode::PERIODIC && nNow >= m_next_write; + // Flush the chainstate (which may refer to block index entries). + bool should_empty{(mode == FlushStateMode::ALWAYS) || fCacheLarge || fCacheCritical}; // Combine all conditions that result in a write to disk. - bool should_write = (mode == FlushStateMode::ALWAYS) || fCacheLarge || fCacheCritical || fPeriodicWrite || fFlushForPrune; + bool should_write{should_empty || fPeriodicWrite || fFlushForPrune}; // Write blocks, block index and best chain related state to disk. if (should_write) { LogDebug(BCLog::COINDB, "Writing chainstate to disk: flush mode=%s, prune=%d, large=%d, critical=%d, periodic=%d", @@ -2838,9 +2844,11 @@ bool Chainstate::FlushStateToDisk( if (!CheckDiskSpace(m_chainman.m_options.datadir, 48 * 2 * 2 * CoinsTip().GetCacheSize())) { return FatalError(m_chainman.GetNotifications(), state, _("Disk space is too low!")); } - // Flush the chainstate (which may refer to block index entries). - const auto empty_cache{(mode == FlushStateMode::ALWAYS) || fCacheLarge || fCacheCritical}; - empty_cache ? CoinsTip().Flush() : CoinsTip().Sync(); + if (should_empty) { + CoinsTip().Flush(); + } else { + CoinsTip().Sync(); + } full_flush_completed = true; TRACEPOINT(utxocache, flush, int64_t{Ticks(NodeClock::now() - nNow)}, @@ -2971,13 +2979,16 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra // Apply the block atomically to the chain state. const auto time_start{SteadyClock::now()}; { - CCoinsViewCache view(&CoinsTip()); + auto& view{ConnectBlockView()}; + view.Start(); assert(view.GetBestBlock() == pindexDelete->GetBlockHash()); if (DisconnectBlock(block, pindexDelete, view) != DISCONNECT_OK) { LogError("DisconnectTip(): DisconnectBlock %s failed\n", pindexDelete->GetBlockHash().ToString()); + view.Reset(); return false; } - view.Flush(/*will_reuse_cache=*/false); // local CCoinsViewCache goes out of scope + view.Flush(); + view.Reset(); } LogDebug(BCLog::BENCH, "- Disconnect block: %.2fms\n", Ticks(SteadyClock::now() - time_start)); @@ -3093,7 +3104,9 @@ bool Chainstate::ConnectTip( LogDebug(BCLog::BENCH, " - Load block from disk: %.2fms\n", Ticks(time_2 - time_1)); { - CCoinsViewCache view(&CoinsTip()); + auto& view{ConnectBlockView()}; + view.Start(); + assert(view.GetBestBlock() == (pindexNew->pprev ? pindexNew->pprev->GetBlockHash() : uint256::ZERO)); bool rv = ConnectBlock(*block_to_connect, state, pindexNew, view); if (m_chainman.m_options.signals) { m_chainman.m_options.signals->BlockChecked(block_to_connect, state); @@ -3102,6 +3115,7 @@ bool Chainstate::ConnectTip( if (state.IsInvalid()) InvalidBlockFound(pindexNew, state); LogError("%s: ConnectBlock %s failed, %s\n", __func__, pindexNew->GetBlockHash().ToString(), state.ToString()); + view.Reset(); return false; } time_3 = SteadyClock::now(); @@ -3111,7 +3125,8 @@ bool Chainstate::ConnectTip( Ticks(time_3 - time_2), Ticks(m_chainman.time_connect_total), Ticks(m_chainman.time_connect_total) / m_chainman.num_blocks_total); - view.Flush(/*will_reuse_cache=*/false); // local CCoinsViewCache goes out of scope + view.Flush(); + view.Reset(); } const auto time_4{SteadyClock::now()}; m_chainman.time_flush += time_4 - time_3; @@ -4580,13 +4595,17 @@ BlockValidationState TestBlockValidity( index_dummy.pprev = tip; index_dummy.nHeight = tip->nHeight + 1; index_dummy.phashBlock = &block_hash; - CCoinsViewCache view_dummy(&chainstate.CoinsTip()); + auto& view_dummy{chainstate.ConnectBlockView()}; + view_dummy.Start(); + assert(view_dummy.GetBestBlock() == tip->GetBlockHash()); // Set fJustCheck to true in order to update, and not clear, validation caches. if(!chainstate.ConnectBlock(block, state, &index_dummy, view_dummy, /*fJustCheck=*/true)) { if (state.IsValid()) NONFATAL_UNREACHABLE(); + view_dummy.Reset(); return state; } + view_dummy.Reset(); // Ensure no check returned successfully while also setting an invalid state. if (!state.IsValid()) NONFATAL_UNREACHABLE(); @@ -4740,7 +4759,7 @@ VerifyDBResult CVerifyDB::VerifyDB( } } // check level 3: check for inconsistencies during memory-only disconnect of tip blocks - size_t curr_coins_usage = coins.DynamicMemoryUsage() + chainstate.CoinsTip().DynamicMemoryUsage(); + size_t curr_coins_usage = coins.ActiveMemoryUsage() + chainstate.CoinsTip().ActiveMemoryUsage(); if (nCheckLevel >= 3) { if (curr_coins_usage <= chainstate.m_coinstip_cache_size_bytes) { @@ -4913,7 +4932,7 @@ bool Chainstate::ReplayBlocks() } cache.SetBestBlock(pindexNew->GetBlockHash()); - cache.Flush(/*will_reuse_cache=*/false); // local CCoinsViewCache goes out of scope + cache.Flush(); // local CCoinsViewCache goes out of scope m_chainman.GetNotifications().progress(bilingual_str{}, 100, false); return true; } @@ -5517,6 +5536,7 @@ std::string Chainstate::ToString() bool Chainstate::ResizeCoinsCaches(size_t coinstip_size, size_t coinsdb_size) { AssertLockHeld(::cs_main); + const float max_load_factor{CoinsTip().GetMaxLoadFactor()}; if (coinstip_size == m_coinstip_cache_size_bytes && coinsdb_size == m_coinsdb_cache_size_bytes) { // Cache sizes are unchanged, no need to continue. @@ -5536,11 +5556,16 @@ bool Chainstate::ResizeCoinsCaches(size_t coinstip_size, size_t coinsdb_size) bool ret; if (coinstip_size > old_coinstip_size) { + CoinsTip().ReserveCache(coinstip_size, max_load_factor); // Likely no need to flush if cache sizes have grown. ret = FlushStateToDisk(state, FlushStateMode::IF_NEEDED); } else { // Otherwise, flush state to disk and deallocate the in-memory coins map. ret = FlushStateToDisk(state, FlushStateMode::ALWAYS); + if (ret) { + CoinsTip().ReallocateCache(); + CoinsTip().ReserveCache(coinstip_size, max_load_factor); + } } return ret; } @@ -5716,7 +5741,8 @@ util::Result ChainstateManager::ActivateSnapshot( static_cast(current_coinsdb_cache_size * SNAPSHOT_CACHE_PERC), in_memory, /*should_wipe=*/false); snapshot_chainstate->InitCoinsCache( - static_cast(current_coinstip_cache_size * SNAPSHOT_CACHE_PERC)); + static_cast(current_coinstip_cache_size * SNAPSHOT_CACHE_PERC), + CCoinsViewCache::CONNECT_BLOCK_VIEW_RESERVE_ENTRIES); } auto cleanup_bad_snapshot = [&](bilingual_str reason) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { diff --git a/src/validation.h b/src/validation.h index 28ae96a6f458..f16fed08d6f3 100644 --- a/src/validation.h +++ b/src/validation.h @@ -488,6 +488,9 @@ class CoinsViews { //! can fit per the dbcache setting. std::unique_ptr m_cacheview GUARDED_BY(cs_main); + //! A per-block cache used for ConnectBlock to not pollute the underlying cache with newly created coins in case the block is invalid. + std::unique_ptr m_connect_block_view GUARDED_BY(cs_main); + //! This constructor initializes CCoinsViewDB and CCoinsViewErrorCatcher instances, but it //! *does not* create a CCoinsViewCache instance by default. This is done separately because the //! presence of the cache has implications on whether or not we're allowed to flush the cache's @@ -497,7 +500,7 @@ class CoinsViews { CoinsViews(DBParams db_params, CoinsViewOptions options); //! Initialize the CCoinsViewCache member. - void InitCache() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + void InitCache(size_t cache_size_bytes, float max_load_factor, size_t connect_block_view_reserve_entries) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); }; enum class CoinsCacheSizeState @@ -604,7 +607,7 @@ class Chainstate //! Initialize the in-memory coins cache (to be done after the health of the on-disk database //! is verified). - void InitCoinsCache(size_t cache_size_bytes) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + void InitCoinsCache(size_t cache_size_bytes, size_t connect_block_view_reserve_entries = 0, float max_load_factor = CCoinsViewCache::DEFAULT_MAX_LOAD_FACTOR) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); //! @returns whether or not the CoinsViews object has been fully initialized and we can //! safely flush this object to disk. @@ -684,6 +687,13 @@ class Chainstate return *Assert(m_coins_views->m_cacheview); } + CCoinsViewCache& ConnectBlockView() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) + { + AssertLockHeld(::cs_main); + Assert(m_coins_views); + return *Assert(m_coins_views->m_connect_block_view); + } + //! @returns A reference to the on-disk UTXO set database. CCoinsViewDB& CoinsDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {