From 2c2ec6aebc13d9af6c52925cf8c832d2b90d3b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Tue, 2 Dec 2025 09:45:46 +0100 Subject: [PATCH 1/7] refactor: reuse `should_empty` for chainstate flush condition --- src/validation.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index e919552ad230..e67d6aa5fb3b 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2790,8 +2790,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 +2840,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)}, From 7c0bf91e3d8f18b6ec0ad09255a0cbaa2796de55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Fri, 26 Dec 2025 21:48:54 +0200 Subject: [PATCH 2/7] validation: reuse connect block coins view --- src/coins.cpp | 18 +++++++++++++ src/coins.h | 3 +++ src/test/coins_tests.cpp | 46 ++++++++++++++++++++++++++++++++ src/test/fuzz/coins_view.cpp | 5 ++++ src/test/fuzz/coinscache_sim.cpp | 6 +++++ src/validation.cpp | 22 +++++++++++---- src/validation.h | 10 +++++++ 7 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/coins.cpp b/src/coins.cpp index 7f2ffc38efa0..6c148d71f776 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -42,6 +42,24 @@ CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic) : CCoinsViewBacked(baseIn), m_deterministic(deterministic), cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource) { + 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); } diff --git a/src/coins.h b/src/coins.h index 6da53829996d..1c0397bf36cc 100644 --- a/src/coins.h +++ b/src/coins.h @@ -384,6 +384,9 @@ class CCoinsViewCache : public CCoinsViewBacked */ 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; 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..a33f43475a2b 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -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..835df2bf9598 100644 --- a/src/test/fuzz/coinscache_sim.cpp +++ b/src/test/fuzz/coinscache_sim.cpp @@ -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/validation.cpp b/src/validation.cpp index e67d6aa5fb3b..38f327924fcd 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1856,6 +1856,7 @@ void CoinsViews::InitCache() { AssertLockHeld(::cs_main); m_cacheview = std::make_unique(&m_catcherview); + m_connect_block_view = std::make_unique(&*m_cacheview); } Chainstate::Chainstate( @@ -2975,13 +2976,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(/*will_reuse_cache=*/false); + view.Reset(); } LogDebug(BCLog::BENCH, "- Disconnect block: %.2fms\n", Ticks(SteadyClock::now() - time_start)); @@ -3097,7 +3101,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); @@ -3106,6 +3112,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(); @@ -3115,7 +3122,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(/*will_reuse_cache=*/false); + view.Reset(); } const auto time_4{SteadyClock::now()}; m_chainman.time_flush += time_4 - time_3; @@ -4584,13 +4592,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(); diff --git a/src/validation.h b/src/validation.h index 28ae96a6f458..63fe5a437dca 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 @@ -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) { From 0b6cbfee580d46406bd226db6a8ba632eba2be12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Fri, 26 Dec 2025 22:18:12 +0200 Subject: [PATCH 3/7] coins: warn on coins cache rehash --- src/coins.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/coins.cpp b/src/coins.cpp index 6c148d71f776..dbe9030ac0fa 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(); } @@ -68,7 +77,9 @@ size_t CCoinsViewCache::DynamicMemoryUsage() const { } 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); @@ -96,7 +107,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()) { @@ -134,8 +147,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; } @@ -211,8 +226,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 { From e45dccb4e5157ce696ee7d6054424b48fabfcf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Fri, 26 Dec 2025 22:15:59 +0200 Subject: [PATCH 4/7] coins: track active cache memory usage --- src/coins.cpp | 10 +++++++++- src/coins.h | 3 +++ src/support/allocators/pool.h | 7 +++++++ src/test/pool_tests.cpp | 6 ++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/coins.cpp b/src/coins.cpp index dbe9030ac0fa..07a16f7df405 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -72,10 +72,18 @@ void CCoinsViewCache::Reset() noexcept m_sentinel.second.SelfRef(m_sentinel); } -size_t CCoinsViewCache::DynamicMemoryUsage() const { +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); diff --git a/src/coins.h b/src/coins.h index 1c0397bf36cc..26e2a2bdbafe 100644 --- a/src/coins.h +++ b/src/coins.h @@ -467,6 +467,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; diff --git a/src/support/allocators/pool.h b/src/support/allocators/pool.h index abca09ad9018..664e4c44728f 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. */ @@ -227,6 +229,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 +242,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 +257,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 +284,8 @@ class PoolResource final { return m_chunk_size_bytes; } + + size_t BytesInUse() const noexcept { return m_bytes_in_use; } }; 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); From 188b7d13145fbe340d94d7e82cc955ba06d30134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Fri, 26 Dec 2025 22:27:04 +0200 Subject: [PATCH 5/7] coins: reserve coins cache based on dbcache --- src/coins.cpp | 28 +++++++++++++++++++++++++++- src/coins.h | 12 +++++++++++- src/node/chainstate.cpp | 3 ++- src/test/validation_flush_tests.cpp | 15 ++++++++++----- src/validation.cpp | 25 ++++++++++++++++--------- src/validation.h | 4 ++-- 6 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/coins.cpp b/src/coins.cpp index 07a16f7df405..9f2d85e37405 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -47,10 +47,12 @@ 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(); } @@ -72,6 +74,29 @@ void CCoinsViewCache::Reset() noexcept m_sentinel.second.SelfRef(m_sentinel); } +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}; + return max_coins_cache_size_bytes / bytes_per_entry_lower_bound; +} + +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; @@ -393,6 +418,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 26e2a2bdbafe..5f9d31d61750 100644 --- a/src/coins.h +++ b/src/coins.h @@ -377,7 +377,13 @@ 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. @@ -480,6 +486,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/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 38f327924fcd..f66c8eee06e2 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1852,11 +1852,11 @@ 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); - m_connect_block_view = std::make_unique(&*m_cacheview); + m_cacheview = std::make_unique(&m_catcherview, /*deterministic=*/false, CCoinsViewCache::ReservedEntries(cache_size_bytes, max_load_factor), max_load_factor); + m_connect_block_view = std::make_unique(&*m_cacheview, /*deterministic=*/false, connect_block_view_reserve_entries, max_load_factor); } Chainstate::Chainstate( @@ -1927,12 +1927,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 @@ -2711,12 +2711,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; @@ -4756,7 +4756,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) { @@ -5533,6 +5533,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. @@ -5552,11 +5553,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; } @@ -5732,7 +5738,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 63fe5a437dca..f16fed08d6f3 100644 --- a/src/validation.h +++ b/src/validation.h @@ -500,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 @@ -607,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. From 0f67b68f3c5a054c73dce62012f94e5c24a8ea88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Fri, 26 Dec 2025 22:28:08 +0200 Subject: [PATCH 6/7] coins: drop will_reuse_cache from Flush --- src/coins.cpp | 10 ++++------ src/coins.h | 2 +- src/test/fuzz/coins_view.cpp | 2 +- src/test/fuzz/coinscache_sim.cpp | 2 +- src/validation.cpp | 6 +++--- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/coins.cpp b/src/coins.cpp index 9f2d85e37405..55d771bb7a52 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -321,15 +321,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; + cacheCoins.clear(); + cachedCoinsUsage = 0; + } void CCoinsViewCache::Sync() diff --git a/src/coins.h b/src/coins.h index 5f9d31d61750..cbaa559f7e3d 100644 --- a/src/coins.h +++ b/src/coins.h @@ -451,7 +451,7 @@ class CCoinsViewCache : public CCoinsViewBacked * 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 diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index a33f43475a2b..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(); diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp index 835df2bf9598..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. diff --git a/src/validation.cpp b/src/validation.cpp index f66c8eee06e2..f2caef8aa128 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2984,7 +2984,7 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra view.Reset(); return false; } - view.Flush(/*will_reuse_cache=*/false); + view.Flush(); view.Reset(); } LogDebug(BCLog::BENCH, "- Disconnect block: %.2fms\n", @@ -3122,7 +3122,7 @@ 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); + view.Flush(); view.Reset(); } const auto time_4{SteadyClock::now()}; @@ -4929,7 +4929,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; } From dfb0b61fc602f68bfcb9fdab96d05463ab62b4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Fri, 26 Dec 2025 22:19:20 +0200 Subject: [PATCH 7/7] coins: log and preserve cache load factor on reallocate --- src/coins.cpp | 18 +++++++++++++----- src/coins.h | 2 -- src/support/allocators/pool.h | 12 +++++++----- src/validation.cpp | 5 ++++- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/coins.cpp b/src/coins.cpp index 55d771bb7a52..9f2a27c96846 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -83,7 +83,11 @@ size_t CCoinsViewCache::ReservedEntries(size_t max_coins_cache_size_bytes, float 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}; - return max_coins_cache_size_bytes / 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) @@ -94,7 +98,7 @@ void CCoinsViewCache::ReserveCache(size_t max_coins_cache_size_bytes, float max_ 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); + cacheCoins.reserve(reserve_entries); } size_t CCoinsViewCache::DynamicMemoryUsage() const @@ -325,9 +329,9 @@ void CCoinsViewCache::Flush() { auto cursor{CoinsViewCacheCursor(m_sentinel, cacheCoins, /*will_erase=*/true)}; base->BatchWrite(cursor, hashBlock); - cacheCoins.clear(); - cachedCoinsUsage = 0; - + cacheCoins.clear(); + cachedCoinsUsage = 0; + m_sentinel.second.SelfRef(m_sentinel); } void CCoinsViewCache::Sync() @@ -375,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 diff --git a/src/coins.h b/src/coins.h index cbaa559f7e3d..8e9005d2a553 100644 --- a/src/coins.h +++ b/src/coins.h @@ -448,8 +448,6 @@ 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(); diff --git a/src/support/allocators/pool.h b/src/support/allocators/pool.h index 664e4c44728f..0fb3f819ef55 100644 --- a/src/support/allocators/pool.h +++ b/src/support/allocators/pool.h @@ -157,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}); diff --git a/src/validation.cpp b/src/validation.cpp index f2caef8aa128..a1b9e466eabd 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1855,7 +1855,10 @@ CoinsViews::CoinsViews(DBParams db_params, CoinsViewOptions options) 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, /*deterministic=*/false, CCoinsViewCache::ReservedEntries(cache_size_bytes, max_load_factor), max_load_factor); + + 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); }