Skip to content

Commit

Permalink
Merge #1531: [Backport] Implement accurate UTXO cache size accounting
Browse files Browse the repository at this point in the history
9880245 Relocate calls to CheckDiskSpace (furszy)
0b69282 Write block index more frequently than cache flushes (furszy)
4338b08 Cache tweak and logging improvements (furszy)
5e505df Use accurate memory for flushing decisions (furszy)
e59840a Keep track of memory usage in CCoinsViewCache. (furszy)
34e48a1 Add memusage.h (Pieter Wuille)

Pull request description:

  This adds a basic foundation for accurate memory counting (memusage.h, with some implementation-specific assumptions, but at least the tree node and hashtable node implementations used are very generic and straightforward, so likely accurate for several systems). On the tested system at least, they are exact, ignoring memory fragmentation (tested using a single binary creating and modifying large amounts of different configurations of these data structures, and observing total resident set size afterwards). I expect this to be useful for other resource-limiting subsystems later on.

  Then, this is used to implement accurate memory usage counting for CCoins objects, and efficiently computed memory usage counting for CCoinsViewCache (using cached totals, and increments/decrements on updates through CCoinsModifier). The existing CCoinsViewCache randomized simultation unit test is extended to also verify the correctness of the cached memory usage totals. Changing any of the cached total update statements in coins.cpp breaks the unit test.

  Finally, the internal flush triggering mechanism is changed to use the memory usage mechanism rather than transaction count of pcoinsTip.

  Coming from upstream@[6102](bitcoin#6102) .

  --- Needs more testing ---

ACKs for top commit:
  random-zebra:
    ACK 9880245
  Fuzzbawls:
    ACK 9880245

Tree-SHA512: ca9abd18cbcf94825cd216b4a31e38092cba1107be32b94ef8a38bf82fda9ee679591126066bcca50e56f8b727376149f41d4109183c0127b5908f2cf2921003
  • Loading branch information
furszy committed May 15, 2020
2 parents 077ab3d + 9880245 commit 1f9e7e4
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 40 deletions.
1 change: 1 addition & 0 deletions src/Makefile.am
Expand Up @@ -131,6 +131,7 @@ BITCOIN_CORE_H = \
limitedmap.h \
logging.h \
main.h \
memusage.h \
masternode.h \
masternode-payments.h \
masternode-budget.h \
Expand Down
24 changes: 21 additions & 3 deletions src/coins.cpp
Expand Up @@ -5,6 +5,7 @@

#include "coins.h"

#include "memusage.h"
#include "random.h"

#include <assert.h>
Expand Down Expand Up @@ -76,13 +77,17 @@ bool CCoinsViewBacked::GetStats(CCoinsStats& stats) const { return base->GetStat

CCoinsKeyHasher::CCoinsKeyHasher() : salt(GetRandHash()) {}

CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn) : CCoinsViewBacked(baseIn), hasModifier(false) {}
CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn) : CCoinsViewBacked(baseIn), hasModifier(false), cachedCoinsUsage(0) {}

CCoinsViewCache::~CCoinsViewCache()
{
assert(!hasModifier);
}

size_t CCoinsViewCache::DynamicMemoryUsage() const {
return memusage::DynamicUsage(cacheCoins) + cachedCoinsUsage;
}

CCoinsMap::const_iterator CCoinsViewCache::FetchCoins(const uint256& txid) const
{
CCoinsMap::iterator it = cacheCoins.find(txid);
Expand All @@ -98,6 +103,7 @@ CCoinsMap::const_iterator CCoinsViewCache::FetchCoins(const uint256& txid) const
// version as fresh.
ret->second.flags = CCoinsCacheEntry::FRESH;
}
cachedCoinsUsage += memusage::DynamicUsage(ret->second.coins);
return ret;
}

Expand All @@ -115,6 +121,7 @@ CCoinsModifier CCoinsViewCache::ModifyCoins(const uint256& txid)
{
assert(!hasModifier);
std::pair<CCoinsMap::iterator, bool> ret = cacheCoins.insert(std::make_pair(txid, CCoinsCacheEntry()));
size_t cachedCoinUsage = 0;
if (ret.second) {
if (!base->GetCoins(txid, ret.first->second.coins)) {
// The parent view does not have this entry; mark it as fresh.
Expand All @@ -124,10 +131,12 @@ CCoinsModifier CCoinsViewCache::ModifyCoins(const uint256& txid)
// The parent view only has a pruned entry for this; mark it as fresh.
ret.first->second.flags = CCoinsCacheEntry::FRESH;
}
} else {
cachedCoinUsage = memusage::DynamicUsage(ret.first->second.coins);
}
// Assume that whenever ModifyCoins is called, the entry will be modified.
ret.first->second.flags |= CCoinsCacheEntry::DIRTY;
return CCoinsModifier(*this, ret.first);
return CCoinsModifier(*this, ret.first, cachedCoinUsage);
}

const CCoins* CCoinsViewCache::AccessCoins(const uint256& txid) const
Expand Down Expand Up @@ -177,17 +186,21 @@ bool CCoinsViewCache::BatchWrite(CCoinsMap& mapCoins, const uint256& hashBlockIn
assert(it->second.flags & CCoinsCacheEntry::FRESH);
CCoinsCacheEntry& entry = cacheCoins[it->first];
entry.coins.swap(it->second.coins);
cachedCoinsUsage += memusage::DynamicUsage(entry.coins);
entry.flags = CCoinsCacheEntry::DIRTY | CCoinsCacheEntry::FRESH;
}
} else {
if ((itUs->second.flags & CCoinsCacheEntry::FRESH) && it->second.coins.IsPruned()) {
// The grandparent does not have an entry, and the child is
// modified and being pruned. This means we can just delete
// it from the parent.
cachedCoinsUsage -= memusage::DynamicUsage(itUs->second.coins);
cacheCoins.erase(itUs);
} else {
// A normal modification.
cachedCoinsUsage -= memusage::DynamicUsage(itUs->second.coins);
itUs->second.coins.swap(it->second.coins);
cachedCoinsUsage += memusage::DynamicUsage(itUs->second.coins);
itUs->second.flags |= CCoinsCacheEntry::DIRTY;
}
}
Expand All @@ -203,6 +216,7 @@ bool CCoinsViewCache::Flush()
{
bool fOk = base->BatchWrite(cacheCoins, hashBlock);
cacheCoins.clear();
cachedCoinsUsage = 0;
return fOk;
}

Expand Down Expand Up @@ -269,7 +283,7 @@ double CCoinsViewCache::GetPriority(const CTransaction& tx, int nHeight) const
return tx.ComputePriority(dResult);
}

CCoinsModifier::CCoinsModifier(CCoinsViewCache& cache_, CCoinsMap::iterator it_) : cache(cache_), it(it_)
CCoinsModifier::CCoinsModifier(CCoinsViewCache& cache_, CCoinsMap::iterator it_, size_t usage) : cache(cache_), it(it_), cachedCoinUsage(usage)
{
assert(!cache.hasModifier);
cache.hasModifier = true;
Expand All @@ -280,7 +294,11 @@ CCoinsModifier::~CCoinsModifier()
assert(cache.hasModifier);
cache.hasModifier = false;
it->second.coins.Cleanup();
cache.cachedCoinsUsage -= cachedCoinUsage; // Subtract the old usage
if ((it->second.flags & CCoinsCacheEntry::FRESH) && it->second.coins.IsPruned()) {
cache.cacheCoins.erase(it);
} else {
// If the coin still exists after the modification, add the new usage
cache.cachedCoinsUsage += memusage::DynamicUsage(it->second.coins);
}
}
19 changes: 18 additions & 1 deletion src/coins.h
Expand Up @@ -8,6 +8,7 @@
#define BITCOIN_COINS_H

#include "compressor.h"
#include "memusage.h"
#include "consensus/consensus.h" // can be removed once policy/ established
#include "script/standard.h"
#include "serialize.h"
Expand Down Expand Up @@ -284,6 +285,15 @@ class CCoins
return false;
return true;
}

size_t DynamicMemoryUsage() const {
size_t ret = memusage::DynamicUsage(vout);
for(const CTxOut &out : vout) {
const std::vector<unsigned char> *script = &out.scriptPubKey;
ret += memusage::DynamicUsage(*script);
}
return ret;
}
};

class CCoinsKeyHasher
Expand Down Expand Up @@ -390,7 +400,8 @@ class CCoinsModifier
private:
CCoinsViewCache& cache;
CCoinsMap::iterator it;
CCoinsModifier(CCoinsViewCache& cache_, CCoinsMap::iterator it_);
size_t cachedCoinUsage; // Cached memory usage of the CCoins object before modification
CCoinsModifier(CCoinsViewCache& cache_, CCoinsMap::iterator it_, size_t usage);

public:
CCoins* operator->() { return &it->second.coins; }
Expand All @@ -413,6 +424,9 @@ class CCoinsViewCache : public CCoinsViewBacked
mutable uint256 hashBlock;
mutable CCoinsMap cacheCoins;

/* Cached dynamic memory usage for the inner CCoins objects. */
mutable size_t cachedCoinsUsage;

public:
CCoinsViewCache(CCoinsView* baseIn);
~CCoinsViewCache();
Expand Down Expand Up @@ -448,6 +462,9 @@ class CCoinsViewCache : public CCoinsViewBacked
//! Calculate the size of the cache (in number of transactions)
unsigned int GetCacheSize() const;

//! Calculate the size of the cache (in bytes)
size_t DynamicMemoryUsage() const;

/**
* Amount of pivx coming in to a transaction
* Note that lightweight clients may not know anything besides the hash of previous transactions,
Expand Down
18 changes: 10 additions & 8 deletions src/init.cpp
Expand Up @@ -1420,18 +1420,20 @@ bool AppInit2()
boost::filesystem::create_directories(GetDataDir() / "blocks");

// cache size calculations
size_t nTotalCache = (GetArg("-dbcache", nDefaultDbCache) << 20);
if (nTotalCache < (nMinDbCache << 20))
nTotalCache = (nMinDbCache << 20); // total cache cannot be less than nMinDbCache
else if (nTotalCache > (nMaxDbCache << 20))
nTotalCache = (nMaxDbCache << 20); // total cache cannot be greater than nMaxDbCache
size_t nBlockTreeDBCache = nTotalCache / 8;
int64_t nTotalCache = (GetArg("-dbcache", nDefaultDbCache) << 20);
nTotalCache = std::max(nTotalCache, nMinDbCache << 20); // total cache cannot be less than nMinDbCache
nTotalCache = std::min(nTotalCache, nMaxDbCache << 20); // total cache cannot be greater than nMaxDbcache
int64_t nBlockTreeDBCache = nTotalCache / 8;
if (nBlockTreeDBCache > (1 << 21) && !GetBoolArg("-txindex", true))
nBlockTreeDBCache = (1 << 21); // block tree db cache shouldn't be larger than 2 MiB
nTotalCache -= nBlockTreeDBCache;
size_t nCoinDBCache = nTotalCache / 2; // use half of the remaining cache for coindb cache
int64_t nCoinDBCache = std::min(nTotalCache / 2, (nTotalCache / 4) + (1 << 23)); // use 25%-50% of the remainder for disk cache
nTotalCache -= nCoinDBCache;
nCoinCacheSize = nTotalCache / 300; // coins in memory require around 300 bytes
nCoinCacheUsage = nTotalCache; // the rest goes to in-memory cache
LogPrintf("Cache configuration:\n");
LogPrintf("* Using %.1fMiB for block index database\n", nBlockTreeDBCache * (1.0 / 1024 / 1024));
LogPrintf("* Using %.1fMiB for chain state database\n", nCoinDBCache * (1.0 / 1024 / 1024));
LogPrintf("* Using %.1fMiB for in-memory UTXO set\n", nCoinCacheUsage * (1.0 / 1024 / 1024));

bool fLoaded = false;
while (!fLoaded && !ShutdownRequested()) {
Expand Down
73 changes: 52 additions & 21 deletions src/main.cpp
Expand Up @@ -94,7 +94,7 @@ bool fTxIndex = true;
bool fIsBareMultisigStd = true;
bool fCheckBlockIndex = false;
bool fVerifyingBlocks = false;
unsigned int nCoinCacheSize = 5000;
size_t nCoinCacheUsage = 5000 * 300;

/* If the tip is older than this (in seconds), the node is considered to be in initial block download. */
int64_t nMaxTipAge = DEFAULT_MAX_TIP_AGE;
Expand Down Expand Up @@ -2586,16 +2586,35 @@ bool static FlushStateToDisk(CValidationState& state, FlushStateMode mode)
{
LOCK(cs_main);
static int64_t nLastWrite = 0;
static int64_t nLastFlush = 0;
static int64_t nLastSetChain = 0;
try {
if ((mode == FLUSH_STATE_ALWAYS) ||
((mode == FLUSH_STATE_PERIODIC || mode == FLUSH_STATE_IF_NEEDED) && pcoinsTip->GetCacheSize() > nCoinCacheSize) ||
(mode == FLUSH_STATE_PERIODIC && GetTimeMicros() > nLastWrite + DATABASE_WRITE_INTERVAL * 1000000)) {
// Typical CCoins structures on disk are around 100 bytes in size.
// Pushing a new one to the database can cause it to be written
// twice (once in the log, and once in the tables). This is already
// an overestimation, as most will delete an existing entry or
// overwrite one. Still, use a conservative safety factor of 2.
if (!CheckDiskSpace(100 * 2 * 2 * pcoinsTip->GetCacheSize()))
int64_t nNow = GetTimeMicros();
// Avoid writing/flushing immediately after startup.
if (nLastWrite == 0) {
nLastWrite = nNow;
}
if (nLastFlush == 0) {
nLastFlush = nNow;
}
if (nLastSetChain == 0) {
nLastSetChain = nNow;
}
size_t cacheSize = pcoinsTip->DynamicMemoryUsage();
// The cache is large and close to the limit, but we have time now (not in the middle of a block processing).
bool fCacheLarge = mode == FLUSH_STATE_PERIODIC && cacheSize * (10.0/9) > nCoinCacheUsage;
// The cache is over the limit, we have to write now.
bool fCacheCritical = mode == FLUSH_STATE_IF_NEEDED && cacheSize > nCoinCacheUsage;
// It's been a while since we wrote the block index to disk. Do this frequently, so we don't need to redownload after a crash.
bool fPeriodicWrite = mode == FLUSH_STATE_PERIODIC && nNow > nLastWrite + (int64_t)DATABASE_WRITE_INTERVAL * 1000000;
// It's been very long since we flushed the cache. Do this infrequently, to optimize cache usage.
bool fPeriodicFlush = mode == FLUSH_STATE_PERIODIC && nNow > nLastFlush + (int64_t)DATABASE_FLUSH_INTERVAL * 1000000;
// Combine all conditions that result in a full cache flush.
bool fDoFullFlush = (mode == FLUSH_STATE_ALWAYS) || fCacheLarge || fCacheCritical || fPeriodicFlush;
// Write blocks and block index to disk.
if (fDoFullFlush || fPeriodicWrite) {
// Depend on nMinDiskSpace to ensure we can write block index
if (!CheckDiskSpace(0))
return state.Error("out of disk space");
// First make sure all block and undo data is flushed to disk.
FlushBlockFile();
Expand Down Expand Up @@ -2625,17 +2644,29 @@ bool static FlushStateToDisk(CValidationState& state, FlushStateMode mode)
return AbortNode(state, "Failed to write money supply to DB");
}
}
// Finally flush the chainstate (which may refer to block index entries).
if (!pcoinsTip->Flush()) {
return AbortNode(state, "Failed to write to coin database");
}
nLastWrite = nNow;
}

// Flush best chain related state. This can only be done if the blocks / block index write was also done.
if (fDoFullFlush) {
// Typical CCoins structures on disk are around 128 bytes in size.
// Pushing a new one to the database can cause it to be written
// twice (once in the log, and once in the tables). This is already
// an overestimation, as most will delete an existing entry or
// overwrite one. Still, use a conservative safety factor of 2.
if (!CheckDiskSpace(128 * 2 * 2 * pcoinsTip->GetCacheSize()))
return state.Error("out of disk space");
// Flush the chainstate (which may refer to block index entries).
if (!pcoinsTip->Flush())
return AbortNode(state, "Failed to write to coin database");
nLastFlush = nNow;
}
if ((mode == FLUSH_STATE_ALWAYS || mode == FLUSH_STATE_PERIODIC) && nNow > nLastSetChain + (int64_t)DATABASE_WRITE_INTERVAL * 1000000) {
// Update best block in wallet (so we can detect restored wallets).
if (mode != FLUSH_STATE_IF_NEEDED) {
GetMainSignals().SetBestChain(chainActive.GetLocator());
}
nLastWrite = GetTimeMicros();
GetMainSignals().SetBestChain(chainActive.GetLocator());
nLastSetChain = nNow;
}

} catch (const std::runtime_error& e) {
return AbortNode(state, std::string("System error while flushing: ") + e.what());
}
Expand Down Expand Up @@ -2664,10 +2695,10 @@ void static UpdateTip(CBlockIndex* pindexNew)
}

const CBlockIndex* pChainTip = chainActive.Tip();
LogPrintf("UpdateTip: new best=%s height=%d version=%d log2_work=%.16f tx=%lu date=%s progress=%f cache=%u\n",
LogPrintf("UpdateTip: new best=%s height=%d version=%d log2_work=%.16f tx=%lu date=%s progress=%f cache=%.1fMiB(%utx)\n",
pChainTip->GetBlockHash().GetHex(), pChainTip->nHeight, pChainTip->nVersion, log(pChainTip->nChainWork.getdouble()) / log(2.0), (unsigned long)pChainTip->nChainTx,
DateTimeStrFormat("%Y-%m-%d %H:%M:%S", pChainTip->GetBlockTime()),
Checkpoints::GuessVerificationProgress(pChainTip), (unsigned int)pcoinsTip->GetCacheSize());
Checkpoints::GuessVerificationProgress(pChainTip), pcoinsTip->DynamicMemoryUsage() * (1.0 / (1<<20)), pcoinsTip->GetCacheSize());

// Check the version of the last 100 blocks to see if we need to upgrade:
static bool fWarned = false;
Expand Down Expand Up @@ -4534,7 +4565,7 @@ bool CVerifyDB::VerifyDB(CCoinsView* coinsview, int nCheckLevel, int nCheckDepth
}
}
// check level 3: check for inconsistencies during memory-only disconnect of tip blocks
if (nCheckLevel >= 3 && pindex == pindexState && (coins.GetCacheSize() + pcoinsTip->GetCacheSize()) <= nCoinCacheSize) {
if (nCheckLevel >= 3 && pindex == pindexState && (coins.DynamicMemoryUsage() + pcoinsTip->DynamicMemoryUsage()) <= nCoinCacheUsage) {
bool fClean = true;
if (!DisconnectBlock(block, state, pindex, coins, &fClean))
return error("VerifyDB() : *** irrecoverable inconsistency in block data at %d, hash=%s", pindex->nHeight, pindex->GetBlockHash().ToString());
Expand Down
8 changes: 5 additions & 3 deletions src/main.h
Expand Up @@ -96,8 +96,10 @@ static const unsigned int MAX_HEADERS_RESULTS = 2000;
* degree of disordering of blocks on disk (which make reindexing and in the future perhaps pruning
* harder). We'll probably want to make this a per-peer adaptive value at some point. */
static const unsigned int BLOCK_DOWNLOAD_WINDOW = 1024;
/** Time to wait (in seconds) between writing blockchain state to disk. */
static const unsigned int DATABASE_WRITE_INTERVAL = 3600;
/** Time to wait (in seconds) between writing blocks/block index to disk. */
static const unsigned int DATABASE_WRITE_INTERVAL = 60 * 60;
/** Time to wait (in seconds) between flushing chainstate to disk. */
static const unsigned int DATABASE_FLUSH_INTERVAL = 24 * 60 * 60;
/** Maximum length of reject messages. */
static const unsigned int MAX_REJECT_MESSAGE_LENGTH = 111;

Expand Down Expand Up @@ -140,7 +142,7 @@ extern int nScriptCheckThreads;
extern bool fTxIndex;
extern bool fIsBareMultisigStd;
extern bool fCheckBlockIndex;
extern unsigned int nCoinCacheSize;
extern size_t nCoinCacheUsage;
extern CFeeRate minRelayTxFee;
extern int64_t nMaxTipAge;
extern bool fVerifyingBlocks;
Expand Down

0 comments on commit 1f9e7e4

Please sign in to comment.