Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 81 additions & 6 deletions src/coins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Coin> CCoinsView::GetCoin(const COutPoint& outpoint) const { return std::nullopt; }
uint256 CCoinsView::GetBestBlock() const { return uint256(); }
std::vector<uint256> CCoinsView::GetHeadBlocks() const { return std::vector<uint256>(); }
Expand All @@ -38,19 +47,76 @@ void CCoinsViewBacked::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& h
std::unique_ptr<CCoinsViewCursor> 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);
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
{
Expand Down
22 changes: 18 additions & 4 deletions src/coins.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Coin> GetCoin(const COutPoint& outpoint) const override;
bool HaveCoin(const COutPoint &outpoint) const override;
Expand Down Expand Up @@ -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
Expand All @@ -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;

Expand All @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion src/node/chainstate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
19 changes: 14 additions & 5 deletions src/support/allocators/pool.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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});
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -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));
Expand All @@ -279,6 +286,8 @@ class PoolResource final
{
return m_chunk_size_bytes;
}

size_t BytesInUse() const noexcept { return m_bytes_in_use; }
};


Expand Down
46 changes: 46 additions & 0 deletions src/test/coins_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
7 changes: 6 additions & 1 deletion src/test/fuzz/coins_view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down
8 changes: 7 additions & 1 deletion src/test/fuzz/coinscache_sim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
},
Expand Down
Loading
Loading