Merge bitcoin/bitcoin#34164: validation: add reusable coins view for ConnectBlock
3e0fd0e4ddrefactor: rename will_reuse_cache to reallocate_cache (Andrew Toth)44b4ee194dvalidation: reuse same CCoinsViewCache for every ConnectBlock call (Andrew Toth)8fb6043231coins: introduce CCoinsViewCache::ResetGuard (Andrew Toth)041758f5edcoins: use hashBlock setter internally for CCoinsViewCache methods (Andrew Toth)8dd9200fc9coins: add Reset on CCoinsViewCache (Andrew Toth) Pull request description: This is the first commit of #31132, which can be merged as an independent change. It has a small benefit on its own, but will help in moving the parent PR forward. Add a `Reset()` method to `CCoinsViewCache` that clears `cacheCoins`, `cachedCoinsUsage`, and `hashBlock` without flushing to the `base` view. This allows efficiently reusing a cache instance across multiple blocks. Add `CCoinsViewCache::CreateResetGuard` method to return a `CCoinsViewCache::ResetGuard`. The `ResetGuard` automatically calls `Reset()` on destruction. This RAII pattern ensures the cache is always properly reset between blocks. Add `m_connect_block_view` as a persistent `CCoinsViewCache` for `ConnectBlock`, avoiding repeated memory allocations. ACKs for top commit: l0rinc: ACK3e0fd0e4ddachow101: ACK3e0fd0e4ddsedited: ACK3e0fd0e4ddTree-SHA512: a95feaa062a9eb7cf7514425a7e7adffd347cd1f7b32b4c1fefcde30002141757c184174702b3104a029dcd33194f8bd734159deebb2e668716089305b42cb00
This commit is contained in:
7 files changed
+116
-11
No files matched your search
+11
-3
@@ -7,6 +7,7 @@
|
|||||||
#include <consensus/consensus.h>
|
#include <consensus/consensus.h>
|
||||||
#include <logging.h>
|
#include <logging.h>
|
||||||
#include <random.h>
|
#include <random.h>
|
||||||
|
#include <uint256.h>
|
||||||
#include <util/trace.h>
|
#include <util/trace.h>
|
||||||
|
|
||||||
TRACEPOINT_SEMAPHORE(utxocache, add);
|
TRACEPOINT_SEMAPHORE(utxocache, add);
|
||||||
@@ -247,15 +248,15 @@ void CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& ha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hashBlock = hashBlockIn;
|
SetBestBlock(hashBlockIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CCoinsViewCache::Flush(bool will_reuse_cache)
|
void CCoinsViewCache::Flush(bool reallocate_cache)
|
||||||
{
|
{
|
||||||
auto cursor{CoinsViewCacheCursor(m_sentinel, cacheCoins, /*will_erase=*/true)};
|
auto cursor{CoinsViewCacheCursor(m_sentinel, cacheCoins, /*will_erase=*/true)};
|
||||||
base->BatchWrite(cursor, hashBlock);
|
base->BatchWrite(cursor, hashBlock);
|
||||||
cacheCoins.clear();
|
cacheCoins.clear();
|
||||||
if (will_reuse_cache) {
|
if (reallocate_cache) {
|
||||||
ReallocateCache();
|
ReallocateCache();
|
||||||
}
|
}
|
||||||
cachedCoinsUsage = 0;
|
cachedCoinsUsage = 0;
|
||||||
@@ -271,6 +272,13 @@ void CCoinsViewCache::Sync()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CCoinsViewCache::Reset() noexcept
|
||||||
|
{
|
||||||
|
cacheCoins.clear();
|
||||||
|
cachedCoinsUsage = 0;
|
||||||
|
SetBestBlock(uint256::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
void CCoinsViewCache::Uncache(const COutPoint& hash)
|
void CCoinsViewCache::Uncache(const COutPoint& hash)
|
||||||
{
|
{
|
||||||
CCoinsMap::iterator it = cacheCoins.find(hash);
|
CCoinsMap::iterator it = cacheCoins.find(hash);
|
||||||
|
|||||||
+28
-2
@@ -6,6 +6,7 @@
|
|||||||
#ifndef BITCOIN_COINS_H
|
#ifndef BITCOIN_COINS_H
|
||||||
#define BITCOIN_COINS_H
|
#define BITCOIN_COINS_H
|
||||||
|
|
||||||
|
#include <attributes.h>
|
||||||
#include <compressor.h>
|
#include <compressor.h>
|
||||||
#include <core_memusage.h>
|
#include <core_memusage.h>
|
||||||
#include <memusage.h>
|
#include <memusage.h>
|
||||||
@@ -369,6 +370,12 @@ protected:
|
|||||||
/* Cached dynamic memory usage for the inner Coin objects. */
|
/* Cached dynamic memory usage for the inner Coin objects. */
|
||||||
mutable size_t cachedCoinsUsage{0};
|
mutable size_t cachedCoinsUsage{0};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discard all modifications made to this cache without flushing to the base view.
|
||||||
|
* This can be used to efficiently reuse a cache instance across multiple operations.
|
||||||
|
*/
|
||||||
|
void Reset() noexcept;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
CCoinsViewCache(CCoinsView *baseIn, bool deterministic = false);
|
CCoinsViewCache(CCoinsView *baseIn, bool deterministic = false);
|
||||||
|
|
||||||
@@ -432,10 +439,10 @@ public:
|
|||||||
* Push the modifications applied to this cache to its base and wipe local state.
|
* 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
|
* Failure to call this method or Sync() before destruction will cause the changes
|
||||||
* to be forgotten.
|
* to be forgotten.
|
||||||
* If will_reuse_cache is false, the cache will retain the same memory footprint
|
* If reallocate_cache is false, the cache will retain the same memory footprint
|
||||||
* after flushing and should be destroyed to deallocate.
|
* after flushing and should be destroyed to deallocate.
|
||||||
*/
|
*/
|
||||||
void Flush(bool will_reuse_cache = true);
|
void Flush(bool reallocate_cache = true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push the modifications applied to this cache to its base while retaining
|
* Push the modifications applied to this cache to its base while retaining
|
||||||
@@ -470,6 +477,25 @@ public:
|
|||||||
//! Run an internal sanity check on the cache data structure. */
|
//! Run an internal sanity check on the cache data structure. */
|
||||||
void SanityCheck() const;
|
void SanityCheck() const;
|
||||||
|
|
||||||
|
class ResetGuard
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
friend CCoinsViewCache;
|
||||||
|
CCoinsViewCache& m_cache;
|
||||||
|
explicit ResetGuard(CCoinsViewCache& cache LIFETIMEBOUND) noexcept : m_cache{cache} {}
|
||||||
|
|
||||||
|
public:
|
||||||
|
ResetGuard(const ResetGuard&) = delete;
|
||||||
|
ResetGuard& operator=(const ResetGuard&) = delete;
|
||||||
|
ResetGuard(ResetGuard&&) = delete;
|
||||||
|
ResetGuard& operator=(ResetGuard&&) = delete;
|
||||||
|
|
||||||
|
~ResetGuard() { m_cache.Reset(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
//! Create a scoped guard that will call `Reset()` on this cache when it goes out of scope.
|
||||||
|
[[nodiscard]] ResetGuard CreateResetGuard() noexcept { return ResetGuard{*this}; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* @note this is marked const, but may actually append to `cacheCoins`, increasing
|
* @note this is marked const, but may actually append to `cacheCoins`, increasing
|
||||||
|
|||||||
@@ -1116,4 +1116,47 @@ BOOST_AUTO_TEST_CASE(ccoins_emplace_duplicate_keeps_usage_balanced)
|
|||||||
BOOST_CHECK(cache.AccessCoin(outpoint) == coin1);
|
BOOST_CHECK(cache.AccessCoin(outpoint) == coin1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BOOST_AUTO_TEST_CASE(ccoins_reset_guard)
|
||||||
|
{
|
||||||
|
CCoinsViewTest root{m_rng};
|
||||||
|
CCoinsViewCache root_cache{&root};
|
||||||
|
uint256 base_best_block{m_rng.rand256()};
|
||||||
|
root_cache.SetBestBlock(base_best_block);
|
||||||
|
root_cache.Flush();
|
||||||
|
|
||||||
|
CCoinsViewCache 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});
|
||||||
|
|
||||||
|
uint256 cache_best_block{m_rng.rand256()};
|
||||||
|
cache.SetBestBlock(cache_best_block);
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto reset_guard{cache.CreateResetGuard()};
|
||||||
|
BOOST_CHECK(cache.AccessCoin(outpoint) == coin);
|
||||||
|
BOOST_CHECK(!cache.AccessCoin(outpoint).IsSpent());
|
||||||
|
BOOST_CHECK_EQUAL(cache.GetCacheSize(), 1);
|
||||||
|
BOOST_CHECK_EQUAL(cache.GetBestBlock(), cache_best_block);
|
||||||
|
BOOST_CHECK(!root_cache.HaveCoinInCache(outpoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_CHECK(cache.AccessCoin(outpoint).IsSpent());
|
||||||
|
BOOST_CHECK_EQUAL(cache.GetCacheSize(), 0);
|
||||||
|
BOOST_CHECK_EQUAL(cache.GetBestBlock(), base_best_block);
|
||||||
|
BOOST_CHECK(!root_cache.HaveCoinInCache(outpoint));
|
||||||
|
|
||||||
|
// Using a reset guard again is idempotent
|
||||||
|
{
|
||||||
|
const auto reset_guard{cache.CreateResetGuard()};
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_CHECK(cache.AccessCoin(outpoint).IsSpent());
|
||||||
|
BOOST_CHECK_EQUAL(cache.GetCacheSize(), 0);
|
||||||
|
BOOST_CHECK_EQUAL(cache.GetBestBlock(), base_best_block);
|
||||||
|
BOOST_CHECK(!root_cache.HaveCoinInCache(outpoint));
|
||||||
|
}
|
||||||
|
|
||||||
BOOST_AUTO_TEST_SUITE_END()
|
BOOST_AUTO_TEST_SUITE_END()
|
||||||
@@ -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(/*reallocate_cache=*/fuzzed_data_provider.ConsumeBool());
|
||||||
},
|
},
|
||||||
[&] {
|
[&] {
|
||||||
coins_view_cache.Sync();
|
coins_view_cache.Sync();
|
||||||
@@ -85,6 +85,20 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
|
|||||||
if (is_db && best_block.IsNull()) best_block = uint256::ONE;
|
if (is_db && best_block.IsNull()) best_block = uint256::ONE;
|
||||||
coins_view_cache.SetBestBlock(best_block);
|
coins_view_cache.SetBestBlock(best_block);
|
||||||
},
|
},
|
||||||
|
[&] {
|
||||||
|
{
|
||||||
|
const auto reset_guard{coins_view_cache.CreateResetGuard()};
|
||||||
|
}
|
||||||
|
// Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite().
|
||||||
|
if (is_db) {
|
||||||
|
const uint256 best_block{ConsumeUInt256(fuzzed_data_provider)};
|
||||||
|
if (best_block.IsNull()) {
|
||||||
|
good_data = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
coins_view_cache.SetBestBlock(best_block);
|
||||||
|
}
|
||||||
|
},
|
||||||
[&] {
|
[&] {
|
||||||
Coin move_to;
|
Coin move_to;
|
||||||
(void)coins_view_cache.SpendCoin(random_out_point, fuzzed_data_provider.ConsumeBool() ? &move_to : nullptr);
|
(void)coins_view_cache.SpendCoin(random_out_point, fuzzed_data_provider.ConsumeBool() ? &move_to : nullptr);
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ FUZZ_TARGET(coinscache_sim)
|
|||||||
// Apply to simulation data.
|
// Apply to simulation data.
|
||||||
flush();
|
flush();
|
||||||
// Apply to real caches.
|
// Apply to real caches.
|
||||||
caches.back()->Flush(/*will_reuse_cache=*/provider.ConsumeBool());
|
caches.back()->Flush(/*reallocate_cache=*/provider.ConsumeBool());
|
||||||
},
|
},
|
||||||
|
|
||||||
[&]() { // Sync.
|
[&]() { // Sync.
|
||||||
@@ -398,6 +398,14 @@ FUZZ_TARGET(coinscache_sim)
|
|||||||
caches.back()->Sync();
|
caches.back()->Sync();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[&]() { // Reset.
|
||||||
|
sim_caches[caches.size()].Wipe();
|
||||||
|
// Apply to real caches.
|
||||||
|
{
|
||||||
|
const auto reset_guard{caches.back()->CreateResetGuard()};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
[&]() { // GetCacheSize
|
[&]() { // GetCacheSize
|
||||||
(void)caches.back()->GetCacheSize();
|
(void)caches.back()->GetCacheSize();
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-4
@@ -1856,6 +1856,7 @@ void CoinsViews::InitCache()
|
|||||||
{
|
{
|
||||||
AssertLockHeld(::cs_main);
|
AssertLockHeld(::cs_main);
|
||||||
m_cacheview = std::make_unique<CCoinsViewCache>(&m_catcherview);
|
m_cacheview = std::make_unique<CCoinsViewCache>(&m_catcherview);
|
||||||
|
m_connect_block_view = std::make_unique<CCoinsViewCache>(&*m_cacheview);
|
||||||
}
|
}
|
||||||
|
|
||||||
Chainstate::Chainstate(
|
Chainstate::Chainstate(
|
||||||
@@ -2956,7 +2957,7 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra
|
|||||||
LogError("DisconnectTip(): DisconnectBlock %s failed\n", pindexDelete->GetBlockHash().ToString());
|
LogError("DisconnectTip(): DisconnectBlock %s failed\n", pindexDelete->GetBlockHash().ToString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
view.Flush(/*will_reuse_cache=*/false); // local CCoinsViewCache goes out of scope
|
view.Flush(/*reallocate_cache=*/false); // local CCoinsViewCache goes out of scope
|
||||||
}
|
}
|
||||||
LogDebug(BCLog::BENCH, "- Disconnect block: %.2fms\n",
|
LogDebug(BCLog::BENCH, "- Disconnect block: %.2fms\n",
|
||||||
Ticks<MillisecondsDouble>(SteadyClock::now() - time_start));
|
Ticks<MillisecondsDouble>(SteadyClock::now() - time_start));
|
||||||
@@ -3073,7 +3074,8 @@ bool Chainstate::ConnectTip(
|
|||||||
LogDebug(BCLog::BENCH, " - Load block from disk: %.2fms\n",
|
LogDebug(BCLog::BENCH, " - Load block from disk: %.2fms\n",
|
||||||
Ticks<MillisecondsDouble>(time_2 - time_1));
|
Ticks<MillisecondsDouble>(time_2 - time_1));
|
||||||
{
|
{
|
||||||
CCoinsViewCache view(&CoinsTip());
|
CCoinsViewCache& view{*m_coins_views->m_connect_block_view};
|
||||||
|
const auto reset_guard{view.CreateResetGuard()};
|
||||||
bool rv = ConnectBlock(*block_to_connect, state, pindexNew, view);
|
bool rv = ConnectBlock(*block_to_connect, state, pindexNew, view);
|
||||||
if (m_chainman.m_options.signals) {
|
if (m_chainman.m_options.signals) {
|
||||||
m_chainman.m_options.signals->BlockChecked(block_to_connect, state);
|
m_chainman.m_options.signals->BlockChecked(block_to_connect, state);
|
||||||
@@ -3091,7 +3093,7 @@ bool Chainstate::ConnectTip(
|
|||||||
Ticks<MillisecondsDouble>(time_3 - time_2),
|
Ticks<MillisecondsDouble>(time_3 - time_2),
|
||||||
Ticks<SecondsDouble>(m_chainman.time_connect_total),
|
Ticks<SecondsDouble>(m_chainman.time_connect_total),
|
||||||
Ticks<MillisecondsDouble>(m_chainman.time_connect_total) / m_chainman.num_blocks_total);
|
Ticks<MillisecondsDouble>(m_chainman.time_connect_total) / m_chainman.num_blocks_total);
|
||||||
view.Flush(/*will_reuse_cache=*/false); // local CCoinsViewCache goes out of scope
|
view.Flush(/*reallocate_cache=*/false); // No need to reallocate since it only has capacity for 1 block
|
||||||
}
|
}
|
||||||
const auto time_4{SteadyClock::now()};
|
const auto time_4{SteadyClock::now()};
|
||||||
m_chainman.time_flush += time_4 - time_3;
|
m_chainman.time_flush += time_4 - time_3;
|
||||||
@@ -4904,7 +4906,7 @@ bool Chainstate::ReplayBlocks()
|
|||||||
}
|
}
|
||||||
|
|
||||||
cache.SetBestBlock(pindexNew->GetBlockHash());
|
cache.SetBestBlock(pindexNew->GetBlockHash());
|
||||||
cache.Flush(/*will_reuse_cache=*/false); // local CCoinsViewCache goes out of scope
|
cache.Flush(/*reallocate_cache=*/false); // local CCoinsViewCache goes out of scope
|
||||||
m_chainman.GetNotifications().progress(bilingual_str{}, 100, false);
|
m_chainman.GetNotifications().progress(bilingual_str{}, 100, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -489,6 +489,10 @@ public:
|
|||||||
//! can fit per the dbcache setting.
|
//! can fit per the dbcache setting.
|
||||||
std::unique_ptr<CCoinsViewCache> m_cacheview GUARDED_BY(cs_main);
|
std::unique_ptr<CCoinsViewCache> m_cacheview GUARDED_BY(cs_main);
|
||||||
|
|
||||||
|
//! Temporary CCoinsViewCache layered on top of m_cacheview and passed to ConnectBlock().
|
||||||
|
//! Reset between calls and flushed only on success, so invalid blocks don't pollute the underlying cache.
|
||||||
|
std::unique_ptr<CCoinsViewCache> m_connect_block_view GUARDED_BY(cs_main);
|
||||||
|
|
||||||
//! This constructor initializes CCoinsViewDB and CCoinsViewErrorCatcher instances, but it
|
//! This constructor initializes CCoinsViewDB and CCoinsViewErrorCatcher instances, but it
|
||||||
//! *does not* create a CCoinsViewCache instance by default. This is done separately because the
|
//! *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
|
//! presence of the cache has implications on whether or not we're allowed to flush the cache's
|
||||||
|
|||||||
Reference in new issue
Block a user