Skip to content

Commit 2f71a1e

Browse files
author
MarcoFalke
committed
Merge #18637: coins: allow cache resize after init
f19fdd4 test: add test for CChainState::ResizeCoinsCaches() (James O'Beirne) 8ac3ef4 add ChainstateManager::MaybeRebalanceCaches() (James O'Beirne) f36aaa6 Add CChainState::ResizeCoinsCaches (James O'Beirne) b223111 txdb: add CCoinsViewDB::ChangeCacheSize (James O'Beirne) Pull request description: This is part of the [assumeutxo project](https://github.com/bitcoin/bitcoin/projects/11): Parent PR: #15606 Issue: #15605 Specification: https://github.com/jamesob/assumeutxo-docs/tree/master/proposal --- In the assumeutxo implementation draft (#15056), once a UTXO snapshot is loaded, a new chainstate object is created after initialization. This means that we have to reclaim some of the cache that we've allocated to the original chainstate (per `dbcache=`) to repurpose for the snapshot chainstate. Furthermore, it makes sense to have different cache allocations depending on which chainstate is more active. While the snapshot chainstate is working to get to the network tip (and the background validation chainstate is idle), it makes sense that the snapshot chainstate should have the majority of cache allocation. And contrariwise once the snapshot has reached network tip, most of the cache should be given to the background validation chainstate. This set of changes (detailed in the commit messages) allows us to dynamically resize the various coins caches. None of the functionality introduced here is used at the moment, but will be in the next AU PR (which introduces `ActivateSnapshot`). `ChainstateManager::MaybeRebalanceCaches()` defines the (somewhat normative) cache allocations between the snapshot and background validation chainstates. I'd be interested in feedback if anyone has thoughts on the proportions I've set there. ACKs for top commit: ajtowns: weak utACK f19fdd4 -- didn't find any major problems, but not super confident that I didn't miss anything fjahr: Code review ACK f19fdd4 ryanofsky: Code review ACK f19fdd4. Only change since last review is constructor cleanup (no change in behavior). I think the suggestions here from ajtowns and others are good, but shouldn't delay merging the PR (and hold up assumeutxo) Tree-SHA512: fffb7847fb6993dd4a1a41cf11179b211b0b20b7eb5f7cf6266442136bfe9d43b830bbefcafd475bfd4af273f5573500594aa41fff03e0ed5c2a1e8562ff9269
2 parents a41ae68 + f19fdd4 commit 2f71a1e

12 files changed

+285
-33
lines changed

src/Makefile.test.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ BITCOIN_TESTS =\
275275
test/uint256_tests.cpp \
276276
test/util_tests.cpp \
277277
test/validation_block_tests.cpp \
278+
test/validation_chainstate_tests.cpp \
278279
test/validation_chainstatemanager_tests.cpp \
279280
test/validation_flush_tests.cpp \
280281
test/validationinterface_tests.cpp \

src/coins.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@ bool CCoinsViewCache::HaveInputs(const CTransaction& tx) const
245245
return true;
246246
}
247247

248+
void CCoinsViewCache::ReallocateCache()
249+
{
250+
// Cache should be empty when we're calling this.
251+
assert(cacheCoins.size() == 0);
252+
cacheCoins.~CCoinsMap();
253+
::new (&cacheCoins) CCoinsMap();
254+
}
255+
248256
static const size_t MIN_TRANSACTION_OUTPUT_WEIGHT = WITNESS_SCALE_FACTOR * ::GetSerializeSize(CTxOut(), PROTOCOL_VERSION);
249257
static const size_t MAX_OUTPUTS_PER_BLOCK = MAX_BLOCK_WEIGHT / MIN_TRANSACTION_OUTPUT_WEIGHT;
250258

src/coins.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,13 @@ class CCoinsViewCache : public CCoinsViewBacked
318318
//! Check whether all prevouts of the transaction are present in the UTXO set represented by this view
319319
bool HaveInputs(const CTransaction& tx) const;
320320

321+
//! Force a reallocation of the cache map. This is required when downsizing
322+
//! the cache because the map's allocator may be hanging onto a lot of
323+
//! memory despite having called .clear().
324+
//!
325+
//! See: https://stackoverflow.com/questions/42114044/how-to-release-unordered-map-memory
326+
void ReallocateCache();
327+
321328
private:
322329
/**
323330
* @note this is marked const, but may actually append to `cacheCoins`, increasing

src/init.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,7 +1534,7 @@ bool AppInitMain(const util::Ref& context, NodeContext& node)
15341534
int64_t nCoinDBCache = std::min(nTotalCache / 2, (nTotalCache / 4) + (1 << 23)); // use 25%-50% of the remainder for disk cache
15351535
nCoinDBCache = std::min(nCoinDBCache, nMaxCoinsDBCache << 20); // cap total coins db cache
15361536
nTotalCache -= nCoinDBCache;
1537-
nCoinCacheUsage = nTotalCache; // the rest goes to in-memory cache
1537+
int64_t nCoinCacheUsage = nTotalCache; // the rest goes to in-memory cache
15381538
int64_t nMempoolSizeMax = gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000;
15391539
LogPrintf("Cache configuration:\n");
15401540
LogPrintf("* Using %.1f MiB for block index database\n", nBlockTreeDBCache * (1.0 / 1024 / 1024));
@@ -1563,6 +1563,9 @@ bool AppInitMain(const util::Ref& context, NodeContext& node)
15631563
try {
15641564
LOCK(cs_main);
15651565
chainman.InitializeChainstate();
1566+
chainman.m_total_coinstip_cache = nCoinCacheUsage;
1567+
chainman.m_total_coinsdb_cache = nCoinDBCache;
1568+
15661569
UnloadBlockIndex();
15671570

15681571
// new CBlockTreeDB tries to delete the existing file, which
@@ -1646,7 +1649,7 @@ bool AppInitMain(const util::Ref& context, NodeContext& node)
16461649
}
16471650

16481651
// The on-disk coinsdb is now in a good state, create the cache
1649-
chainstate->InitCoinsCache();
1652+
chainstate->InitCoinsCache(nCoinCacheUsage);
16501653
assert(chainstate->CanFlushToDisk());
16511654

16521655
if (!is_coinsview_empty(chainstate)) {

src/test/util/setup_common.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ TestingSetup::TestingSetup(const std::string& chainName, const std::vector<const
142142
::ChainstateActive().InitCoinsDB(
143143
/* cache_size_bytes */ 1 << 23, /* in_memory */ true, /* should_wipe */ false);
144144
assert(!::ChainstateActive().CanFlushToDisk());
145-
::ChainstateActive().InitCoinsCache();
145+
::ChainstateActive().InitCoinsCache(1 << 23);
146146
assert(::ChainstateActive().CanFlushToDisk());
147147
if (!LoadGenesisBlock(chainparams)) {
148148
throw std::runtime_error("LoadGenesisBlock failed.");
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) 2020 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
//
5+
#include <random.h>
6+
#include <uint256.h>
7+
#include <consensus/validation.h>
8+
#include <sync.h>
9+
#include <test/util/setup_common.h>
10+
#include <validation.h>
11+
12+
#include <vector>
13+
14+
#include <boost/test/unit_test.hpp>
15+
16+
BOOST_FIXTURE_TEST_SUITE(validation_chainstate_tests, TestingSetup)
17+
18+
//! Test resizing coins-related CChainState caches during runtime.
19+
//!
20+
BOOST_AUTO_TEST_CASE(validation_chainstate_resize_caches)
21+
{
22+
ChainstateManager manager;
23+
24+
//! Create and add a Coin with DynamicMemoryUsage of 80 bytes to the given view.
25+
auto add_coin = [](CCoinsViewCache& coins_view) -> COutPoint {
26+
Coin newcoin;
27+
uint256 txid = InsecureRand256();
28+
COutPoint outp{txid, 0};
29+
newcoin.nHeight = 1;
30+
newcoin.out.nValue = InsecureRand32();
31+
newcoin.out.scriptPubKey.assign((uint32_t)56, 1);
32+
coins_view.AddCoin(outp, std::move(newcoin), false);
33+
34+
return outp;
35+
};
36+
37+
ENTER_CRITICAL_SECTION(cs_main);
38+
CChainState& c1 = manager.InitializeChainstate();
39+
LEAVE_CRITICAL_SECTION(cs_main);
40+
c1.InitCoinsDB(
41+
/* cache_size_bytes */ 1 << 23, /* in_memory */ true, /* should_wipe */ false);
42+
WITH_LOCK(::cs_main, c1.InitCoinsCache(1 << 23));
43+
44+
// Add a coin to the in-memory cache, upsize once, then downsize.
45+
{
46+
LOCK(::cs_main);
47+
auto outpoint = add_coin(c1.CoinsTip());
48+
49+
// Set a meaningless bestblock value in the coinsview cache - otherwise we won't
50+
// flush during ResizecoinsCaches() and will subsequently hit an assertion.
51+
c1.CoinsTip().SetBestBlock(InsecureRand256());
52+
53+
BOOST_CHECK(c1.CoinsTip().HaveCoinInCache(outpoint));
54+
55+
c1.ResizeCoinsCaches(
56+
1 << 24, // upsizing the coinsview cache
57+
1 << 22 // downsizing the coinsdb cache
58+
);
59+
60+
// View should still have the coin cached, since we haven't destructed the cache on upsize.
61+
BOOST_CHECK(c1.CoinsTip().HaveCoinInCache(outpoint));
62+
63+
c1.ResizeCoinsCaches(
64+
1 << 22, // downsizing the coinsview cache
65+
1 << 23 // upsizing the coinsdb cache
66+
);
67+
68+
// The view cache should be empty since we had to destruct to downsize.
69+
BOOST_CHECK(!c1.CoinsTip().HaveCoinInCache(outpoint));
70+
}
71+
72+
// Avoid triggering the address sanitizer.
73+
WITH_LOCK(::cs_main, manager.Unload());
74+
}
75+
76+
BOOST_AUTO_TEST_SUITE_END()

src/test/validation_chainstatemanager_tests.cpp

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,11 @@ BOOST_AUTO_TEST_CASE(chainstatemanager)
2828

2929
// Create a legacy (IBD) chainstate.
3030
//
31-
ENTER_CRITICAL_SECTION(cs_main);
32-
CChainState& c1 = manager.InitializeChainstate();
33-
LEAVE_CRITICAL_SECTION(cs_main);
31+
CChainState& c1 = *WITH_LOCK(::cs_main, return &manager.InitializeChainstate());
3432
chainstates.push_back(&c1);
3533
c1.InitCoinsDB(
3634
/* cache_size_bytes */ 1 << 23, /* in_memory */ true, /* should_wipe */ false);
37-
WITH_LOCK(::cs_main, c1.InitCoinsCache());
35+
WITH_LOCK(::cs_main, c1.InitCoinsCache(1 << 23));
3836

3937
BOOST_CHECK(!manager.IsSnapshotActive());
4038
BOOST_CHECK(!manager.IsSnapshotValidated());
@@ -57,12 +55,13 @@ BOOST_AUTO_TEST_CASE(chainstatemanager)
5755
// Create a snapshot-based chainstate.
5856
//
5957
ENTER_CRITICAL_SECTION(cs_main);
60-
CChainState& c2 = manager.InitializeChainstate(GetRandHash());
58+
CChainState& c2 = *WITH_LOCK(::cs_main,
59+
return &manager.InitializeChainstate(GetRandHash()));
6160
LEAVE_CRITICAL_SECTION(cs_main);
6261
chainstates.push_back(&c2);
6362
c2.InitCoinsDB(
6463
/* cache_size_bytes */ 1 << 23, /* in_memory */ true, /* should_wipe */ false);
65-
WITH_LOCK(::cs_main, c2.InitCoinsCache());
64+
WITH_LOCK(::cs_main, c2.InitCoinsCache(1 << 23));
6665
// Unlike c1, which doesn't have any blocks. Gets us different tip, height.
6766
c2.LoadGenesisBlock(chainparams);
6867
BlockValidationState _;
@@ -104,4 +103,58 @@ BOOST_AUTO_TEST_CASE(chainstatemanager)
104103
WITH_LOCK(::cs_main, manager.Unload());
105104
}
106105

106+
//! Test rebalancing the caches associated with each chainstate.
107+
BOOST_AUTO_TEST_CASE(chainstatemanager_rebalance_caches)
108+
{
109+
ChainstateManager manager;
110+
size_t max_cache = 10000;
111+
manager.m_total_coinsdb_cache = max_cache;
112+
manager.m_total_coinstip_cache = max_cache;
113+
114+
std::vector<CChainState*> chainstates;
115+
116+
// Create a legacy (IBD) chainstate.
117+
//
118+
ENTER_CRITICAL_SECTION(cs_main);
119+
CChainState& c1 = manager.InitializeChainstate();
120+
LEAVE_CRITICAL_SECTION(cs_main);
121+
chainstates.push_back(&c1);
122+
c1.InitCoinsDB(
123+
/* cache_size_bytes */ 1 << 23, /* in_memory */ true, /* should_wipe */ false);
124+
125+
{
126+
LOCK(::cs_main);
127+
c1.InitCoinsCache(1 << 23);
128+
c1.CoinsTip().SetBestBlock(InsecureRand256());
129+
manager.MaybeRebalanceCaches();
130+
}
131+
132+
BOOST_CHECK_EQUAL(c1.m_coinstip_cache_size_bytes, max_cache);
133+
BOOST_CHECK_EQUAL(c1.m_coinsdb_cache_size_bytes, max_cache);
134+
135+
// Create a snapshot-based chainstate.
136+
//
137+
ENTER_CRITICAL_SECTION(cs_main);
138+
CChainState& c2 = manager.InitializeChainstate(GetRandHash());
139+
LEAVE_CRITICAL_SECTION(cs_main);
140+
chainstates.push_back(&c2);
141+
c2.InitCoinsDB(
142+
/* cache_size_bytes */ 1 << 23, /* in_memory */ true, /* should_wipe */ false);
143+
144+
{
145+
LOCK(::cs_main);
146+
c2.InitCoinsCache(1 << 23);
147+
c2.CoinsTip().SetBestBlock(InsecureRand256());
148+
manager.MaybeRebalanceCaches();
149+
}
150+
151+
// Since both chainstates are considered to be in initial block download,
152+
// the snapshot chainstate should take priority.
153+
BOOST_CHECK_CLOSE(c1.m_coinstip_cache_size_bytes, max_cache * 0.05, 1);
154+
BOOST_CHECK_CLOSE(c1.m_coinsdb_cache_size_bytes, max_cache * 0.05, 1);
155+
BOOST_CHECK_CLOSE(c2.m_coinstip_cache_size_bytes, max_cache * 0.95, 1);
156+
BOOST_CHECK_CLOSE(c2.m_coinsdb_cache_size_bytes, max_cache * 0.95, 1);
157+
158+
}
159+
107160
BOOST_AUTO_TEST_SUITE_END()

src/test/validation_flush_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate)
2121
BlockManager blockman{};
2222
CChainState chainstate{blockman};
2323
chainstate.InitCoinsDB(/*cache_size_bytes*/ 1 << 10, /*in_memory*/ true, /*should_wipe*/ false);
24-
WITH_LOCK(::cs_main, chainstate.InitCoinsCache());
24+
WITH_LOCK(::cs_main, chainstate.InitCoinsCache(1 << 10));
2525
CTxMemPool tx_pool{};
2626

2727
constexpr bool is_64_bit = sizeof(void*) == 8;

src/txdb.cpp

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <random.h>
1111
#include <shutdown.h>
1212
#include <uint256.h>
13+
#include <util/memory.h>
1314
#include <util/system.h>
1415
#include <util/translation.h>
1516
#include <util/vector.h>
@@ -39,35 +40,45 @@ struct CoinEntry {
3940

4041
}
4142

42-
CCoinsViewDB::CCoinsViewDB(fs::path ldb_path, size_t nCacheSize, bool fMemory, bool fWipe) : db(ldb_path, nCacheSize, fMemory, fWipe, true)
43+
CCoinsViewDB::CCoinsViewDB(fs::path ldb_path, size_t nCacheSize, bool fMemory, bool fWipe) :
44+
m_db(MakeUnique<CDBWrapper>(ldb_path, nCacheSize, fMemory, fWipe, true)),
45+
m_ldb_path(ldb_path),
46+
m_is_memory(fMemory) { }
47+
48+
void CCoinsViewDB::ResizeCache(size_t new_cache_size)
4349
{
50+
// Have to do a reset first to get the original `m_db` state to release its
51+
// filesystem lock.
52+
m_db.reset();
53+
m_db = MakeUnique<CDBWrapper>(
54+
m_ldb_path, new_cache_size, m_is_memory, /*fWipe*/ false, /*obfuscate*/ true);
4455
}
4556

4657
bool CCoinsViewDB::GetCoin(const COutPoint &outpoint, Coin &coin) const {
47-
return db.Read(CoinEntry(&outpoint), coin);
58+
return m_db->Read(CoinEntry(&outpoint), coin);
4859
}
4960

5061
bool CCoinsViewDB::HaveCoin(const COutPoint &outpoint) const {
51-
return db.Exists(CoinEntry(&outpoint));
62+
return m_db->Exists(CoinEntry(&outpoint));
5263
}
5364

5465
uint256 CCoinsViewDB::GetBestBlock() const {
5566
uint256 hashBestChain;
56-
if (!db.Read(DB_BEST_BLOCK, hashBestChain))
67+
if (!m_db->Read(DB_BEST_BLOCK, hashBestChain))
5768
return uint256();
5869
return hashBestChain;
5970
}
6071

6172
std::vector<uint256> CCoinsViewDB::GetHeadBlocks() const {
6273
std::vector<uint256> vhashHeadBlocks;
63-
if (!db.Read(DB_HEAD_BLOCKS, vhashHeadBlocks)) {
74+
if (!m_db->Read(DB_HEAD_BLOCKS, vhashHeadBlocks)) {
6475
return std::vector<uint256>();
6576
}
6677
return vhashHeadBlocks;
6778
}
6879

6980
bool CCoinsViewDB::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) {
70-
CDBBatch batch(db);
81+
CDBBatch batch(*m_db);
7182
size_t count = 0;
7283
size_t changed = 0;
7384
size_t batch_size = (size_t)gArgs.GetArg("-dbbatchsize", nDefaultDbBatchSize);
@@ -105,7 +116,7 @@ bool CCoinsViewDB::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) {
105116
mapCoins.erase(itOld);
106117
if (batch.SizeEstimate() > batch_size) {
107118
LogPrint(BCLog::COINDB, "Writing partial batch of %.2f MiB\n", batch.SizeEstimate() * (1.0 / 1048576.0));
108-
db.WriteBatch(batch);
119+
m_db->WriteBatch(batch);
109120
batch.Clear();
110121
if (crash_simulate) {
111122
static FastRandomContext rng;
@@ -122,14 +133,14 @@ bool CCoinsViewDB::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock) {
122133
batch.Write(DB_BEST_BLOCK, hashBlock);
123134

124135
LogPrint(BCLog::COINDB, "Writing final batch of %.2f MiB\n", batch.SizeEstimate() * (1.0 / 1048576.0));
125-
bool ret = db.WriteBatch(batch);
136+
bool ret = m_db->WriteBatch(batch);
126137
LogPrint(BCLog::COINDB, "Committed %u changed transaction outputs (out of %u) to coin database...\n", (unsigned int)changed, (unsigned int)count);
127138
return ret;
128139
}
129140

130141
size_t CCoinsViewDB::EstimateSize() const
131142
{
132-
return db.EstimateSize(DB_COIN, (char)(DB_COIN+1));
143+
return m_db->EstimateSize(DB_COIN, (char)(DB_COIN+1));
133144
}
134145

135146
CBlockTreeDB::CBlockTreeDB(size_t nCacheSize, bool fMemory, bool fWipe) : CDBWrapper(GetDataDir() / "blocks" / "index", nCacheSize, fMemory, fWipe) {
@@ -156,7 +167,7 @@ bool CBlockTreeDB::ReadLastBlockFile(int &nFile) {
156167

157168
CCoinsViewCursor *CCoinsViewDB::Cursor() const
158169
{
159-
CCoinsViewDBCursor *i = new CCoinsViewDBCursor(const_cast<CDBWrapper&>(db).NewIterator(), GetBestBlock());
170+
CCoinsViewDBCursor *i = new CCoinsViewDBCursor(const_cast<CDBWrapper&>(*m_db).NewIterator(), GetBestBlock());
160171
/* It seems that there are no "const iterators" for LevelDB. Since we
161172
only need read operations on it, use a const-cast to get around
162173
that restriction. */
@@ -335,7 +346,7 @@ class CCoins
335346
* Currently implemented: from the per-tx utxo model (0.8..0.14.x) to per-txout.
336347
*/
337348
bool CCoinsViewDB::Upgrade() {
338-
std::unique_ptr<CDBIterator> pcursor(db.NewIterator());
349+
std::unique_ptr<CDBIterator> pcursor(m_db->NewIterator());
339350
pcursor->Seek(std::make_pair(DB_COINS, uint256()));
340351
if (!pcursor->Valid()) {
341352
return true;
@@ -346,7 +357,7 @@ bool CCoinsViewDB::Upgrade() {
346357
LogPrintf("[0%%]..."); /* Continued */
347358
uiInterface.ShowProgress(_("Upgrading UTXO database").translated, 0, true);
348359
size_t batch_size = 1 << 24;
349-
CDBBatch batch(db);
360+
CDBBatch batch(*m_db);
350361
int reportDone = 0;
351362
std::pair<unsigned char, uint256> key;
352363
std::pair<unsigned char, uint256> prev_key = {DB_COINS, uint256()};
@@ -380,18 +391,18 @@ bool CCoinsViewDB::Upgrade() {
380391
}
381392
batch.Erase(key);
382393
if (batch.SizeEstimate() > batch_size) {
383-
db.WriteBatch(batch);
394+
m_db->WriteBatch(batch);
384395
batch.Clear();
385-
db.CompactRange(prev_key, key);
396+
m_db->CompactRange(prev_key, key);
386397
prev_key = key;
387398
}
388399
pcursor->Next();
389400
} else {
390401
break;
391402
}
392403
}
393-
db.WriteBatch(batch);
394-
db.CompactRange({DB_COINS, uint256()}, key);
404+
m_db->WriteBatch(batch);
405+
m_db->CompactRange({DB_COINS, uint256()}, key);
395406
uiInterface.ShowProgress("", 100, false);
396407
LogPrintf("[%s].\n", ShutdownRequested() ? "CANCELLED" : "DONE");
397408
return !ShutdownRequested();

0 commit comments

Comments
 (0)