diff --git a/src/memusage.h b/src/memusage.h index 9d9e549ef22..8119bc0f8b7 100644 --- a/src/memusage.h +++ b/src/memusage.h @@ -24,9 +24,6 @@ namespace memusage { -/** Compute the total memory used by allocating alloc bytes. */ -static size_t MallocUsage(size_t alloc); - /** Dynamic memory usage for built-in types is zero. */ static inline size_t DynamicUsage(const int8_t& v) { return 0; } static inline size_t DynamicUsage(const uint8_t& v) { return 0; } @@ -48,19 +45,21 @@ template static inline size_t DynamicUsage(const X * const &v) { ret * application data structures require more accurate inner accounting, they should * iterate themselves, or use more efficient caching + updating on modification. */ - -static inline size_t MallocUsage(size_t alloc) +static constexpr size_t MallocUsage(size_t alloc) { - // Measured on libc6 2.19 on Linux. - if (alloc == 0) { - return 0; - } else if (sizeof(void*) == 8) { - return ((alloc + 31) >> 4) << 4; - } else if (sizeof(void*) == 4) { - return ((alloc + 15) >> 3) << 3; - } else { - assert(0); - } + if (alloc == 0) return 0; + +#if defined(__arm__) || SIZE_MAX == UINT64_MAX + constexpr size_t min_alloc{9}; +#else + constexpr size_t min_alloc{0}; +#endif + constexpr size_t overhead{sizeof(size_t)}; + constexpr size_t step{alignof(std::max_align_t)}; + // step should be a nonzero power of 2 (exactly one bit set) + static_assert(step > 0 && (step & (step - 1)) == 0); + + return (std::max(min_alloc, alloc) + overhead + (step - 1)) & ~(step - 1); } // STL data structures @@ -177,23 +176,33 @@ static inline size_t DynamicUsage(const std::list& l) return MallocUsage(sizeof(list_node)) * l.size(); } +// Empirically, an std::unordered_map node has two pointers (likely +// forward and backward pointers) on some platforms (Windows and macOS), +// so be conservative in estimating memory usage by assuming this is +// the case for all platforms. template struct unordered_node : private X { private: void* ptr; + void* ptr2; }; +// The memory used by an unordered_set or unordered map is the sum of the +// sizes of the individual nodes (which are separately allocated) plus +// the size of the bucket array (which is a single allocation). +// Empirically, each element of the bucket array consists of two pointers +// on some platforms (Windows and macOS), so be conservative. template static inline size_t DynamicUsage(const std::unordered_set& s) { - return MallocUsage(sizeof(unordered_node)) * s.size() + MallocUsage(sizeof(void*) * s.bucket_count()); + return MallocUsage(sizeof(unordered_node)) * s.size() + MallocUsage(2 * sizeof(void*) * s.bucket_count()); } template static inline size_t DynamicUsage(const std::unordered_map& m) { - return MallocUsage(sizeof(unordered_node >)) * m.size() + MallocUsage(sizeof(void*) * m.bucket_count()); + return MallocUsage(sizeof(unordered_node >)) * m.size() + MallocUsage(2 * sizeof(void*) * m.bucket_count()); } template @@ -212,7 +221,7 @@ static inline size_t DynamicUsage(const std::unordered_mapNumAllocatedChunks(); size_t usage_chunks = MallocUsage(pool_resource->ChunkSizeBytes()) * pool_resource->NumAllocatedChunks(); - return usage_resource + usage_chunks + MallocUsage(sizeof(void*) * m.bucket_count()); + return usage_resource + usage_chunks + MallocUsage(2 * sizeof(void*) * m.bucket_count()); } } // namespace memusage diff --git a/src/test/validation_flush_tests.cpp b/src/test/validation_flush_tests.cpp index c325f7deb2b..bab0b782d45 100644 --- a/src/test/validation_flush_tests.cpp +++ b/src/test/validation_flush_tests.cpp @@ -30,7 +30,7 @@ BOOST_AUTO_TEST_CASE(getcoinscachesizestate) // (prevector<28, unsigned char>) when assigned 56 bytes of data per above. // // See also: Coin::DynamicMemoryUsage(). - constexpr unsigned int COIN_SIZE = is_64_bit ? 80 : 64; + constexpr size_t COIN_SIZE{64}; auto print_view_mem_usage = [](CCoinsViewCache& view) { BOOST_TEST_MESSAGE("CCoinsViewCache memory usage: " << view.DynamicMemoryUsage()); diff --git a/test/functional/test_framework/mempool_util.py b/test/functional/test_framework/mempool_util.py index 0e9c821e2ea..441686f4f22 100644 --- a/test/functional/test_framework/mempool_util.py +++ b/test/functional/test_framework/mempool_util.py @@ -57,16 +57,17 @@ def fill_mempool(test_framework, node, *, tx_sync_fun=None): # Generate UTXOs to flood the mempool # 1 to create a tx initially that will be evicted from the mempool later # 75 transactions each with a fee rate higher than the previous one + # 1 to create a final middle-sized tx to evict a filler tx and create room (if needed) ephemeral_miniwallet = MiniWallet(node, tag_name="fill_mempool_ephemeral_wallet") - test_framework.generate(ephemeral_miniwallet, 1 + num_of_batches * tx_batch_size) + test_framework.generate(ephemeral_miniwallet, 2 + num_of_batches * tx_batch_size) # Mine enough blocks so that the UTXOs are allowed to be spent test_framework.generate(node, COINBASE_MATURITY - 1) # Get all UTXOs up front to ensure none of the transactions spend from each other, as that may # change their effective feerate and thus the order in which they are selected for eviction. - confirmed_utxos = [ephemeral_miniwallet.get_utxo(confirmed_only=True) for _ in range(num_of_batches * tx_batch_size + 1)] - assert_equal(len(confirmed_utxos), num_of_batches * tx_batch_size + 1) + confirmed_utxos = [ephemeral_miniwallet.get_utxo(confirmed_only=True) for _ in range(num_of_batches * tx_batch_size + 2)] + assert_equal(len(confirmed_utxos), num_of_batches * tx_batch_size + 2) test_framework.log.debug("Create a mempool tx that will be evicted") tx_to_be_evicted_id = ephemeral_miniwallet.send_self_transfer( @@ -92,6 +93,14 @@ def fill_mempool(test_framework, node, *, tx_sync_fun=None): send_batch(fee) tx_sync_fun() if tx_sync_fun else test_framework.sync_mempools() # sync after all evictions + # If the mempool is almost full (<10k usage bytes left), submit one extra middle-sized tx, + # in order to evict a large filler tx and leave some room; this will enable tests to submit + # txs with just the mempoolminfee without immediate eviction ("mempool full" error). + if node.getmempoolinfo()['usage'] >= 4_990_000: + ephemeral_miniwallet.send_self_transfer(from_node=node, fee=num_of_batches * (base_fee / 2), + utxo_to_spend=confirmed_utxos.pop(0), target_vsize = 32500) + assert_greater_than(4_990_000, node.getmempoolinfo()['usage']) + test_framework.log.debug("The tx should be evicted by now") # The number of transactions created should be greater than the ones present in the mempool assert_greater_than(tx_batch_size * num_of_batches, len(node.getrawmempool())) @@ -101,6 +110,7 @@ def fill_mempool(test_framework, node, *, tx_sync_fun=None): test_framework.log.debug("Check that mempoolminfee is larger than minrelaytxfee") assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) assert_greater_than(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) + test_framework.sync_mempools() def tx_in_orphanage(node, tx: CTransaction) -> bool: """Returns true if the transaction is in the orphanage."""