Merge bitcoin/bitcoin#33157: cluster mempool: control/optimize TxGraph memory usage

023cd5a546 txgraph: add SingletonClusterImpl (mem optimization) (Pieter Wuille)
e346250732 txgraph: give Clusters a range of intended tx counts (preparation) (Pieter Wuille)
e93b0f09cc txgraph: abstract out creation of empty Clusters (refactor) (Pieter Wuille)
6baf12621f txgraph: comment fixes (doc fix) (Pieter Wuille)
726b995739 txgraph: make Cluster an abstract class (refactor) (Pieter Wuille)
2602d89edd txgraph: avoid accessing other Cluster internals (refactor) (Pieter Wuille)
04c808ac4c txgraph: expose memory usage estimate function (feature) (Pieter Wuille)
7680bb8fd4 txgraph: keep track of Cluster memory usage (preparation) (Pieter Wuille)
4ba562e5f4 txgraph: keep data structures compact (mem optimization) (Pieter Wuille)
bb5cb222ae depgraph: add memory usage control (feature) (Pieter Wuille)
b1637a90de txgraph: avoid holes in DepGraph positions (mem optimization) (Pieter Wuille)
2b1d302508 txgraph: move some sanity checks from Cluster to TxGraphImpl (refactor) (Pieter Wuille)
d40302fbaf txgraph: Make level of Cluster implicit (optimization) (Pieter Wuille)

Pull request description:

  Part of #30289.

  This adds a few optimizations to reduce `TxGraph`'s memory usage, and makes sure that dynamic memory it uses doesn't linger after shrinking clusters. Finally, it exposes a function `GetMainMemoryUsage()` to compute `TxGraph`'s approximate memory usage.

  It makes the `Cluster` type abstract, with two instances (`SingletonClusterImpl` for 1-transaction clusters, and `GenericClusterImpl` for others).

  On my 64-bit system, I obtain the following numbers:
  * `SingletonClusterImpl`: 48 bytes, plus 16 bytes malloc overhead in its `unique_ptr`, plus 8-byte pointer in `m_clusters`
  * `GenericClusterImpl`: 104 bytes, plus 16 bytes malloc overhead in its `unique_ptr`, plus 8-byte pointer in `m_clusters`, plus 72 bytes malloc overhead inside its vectors and `DepGraph`, plus 40 bytes per transaction in those.
  * `TxGraphImpl::Entry`: 72 bytes per transaction
  * `TxGraphImpl::ChunkData`: 8 bytes, plus 56 bytes in `std::set` overhead + malloc overhead, all per chunk.
  * `TxGraph::Ref`: 16 bytes per transaction

  This overall amounts to 200 bytes per cluster, plus 64 bytes per chunk, plus 128 bytes per transaction, but only 224 bytes overall per singleton cluster.

ACKs for top commit:
  l0rinc:
    code review reACK 023cd5a546
  instagibbs:
    reACK 023cd5a546
  ismaelsadeeq:
    reACK 023cd5a546  🚢
  glozow:
    reACK 023cd5a546

Tree-SHA512: c957b27f47318be7c25d71453df2ae9d4e7bf21dab13b6e5e975cca122a221a99b15c584872491225785d276a9165f090675ee0f4460a2775bd3271933e3b246
This commit is contained in:
merge-script committed 2025-10-14 11:56:52 -04:00
commit fdcf67de80
5 files changed
+848 -262

No files matched your search

+12
View File
@@ -12,6 +12,7 @@
#include <utility>
#include <vector>
#include <memusage.h>
#include <random.h>
#include <span.h>
#include <util/feefrac.h>
@@ -332,6 +333,17 @@ public:
}
return true;
}
/** Reduce memory usage if possible. No observable effect. */
void Compact() noexcept
{
entries.shrink_to_fit();
}
size_t DynamicMemoryUsage() const noexcept
{
return memusage::DynamicUsage(entries);
}
};
/** A set of transactions together with their aggregate feerate. */
+61 -52
View File
@@ -452,63 +452,72 @@ FUZZ_TARGET(clusterlin_depgraph_sim)
}
};
auto last_compaction_pos{real.PositionRange()};
LIMITED_WHILE(provider.remaining_bytes() > 0, 1000) {
uint8_t command = provider.ConsumeIntegral<uint8_t>();
if (num_tx_sim == 0 || ((command % 3) <= 0 && num_tx_sim < TestBitSet::Size())) {
// AddTransaction.
auto fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
auto size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff);
FeeFrac feerate{fee, size};
// Apply to DepGraph.
auto idx = real.AddTransaction(feerate);
// Verify that the returned index is correct.
assert(!sim[idx].has_value());
for (DepGraphIndex i = 0; i < TestBitSet::Size(); ++i) {
if (!sim[i].has_value()) {
assert(idx == i);
break;
}
}
// Update sim.
sim[idx] = {feerate, TestBitSet::Singleton(idx)};
++num_tx_sim;
continue;
}
if ((command % 3) <= 1 && num_tx_sim > 0) {
// AddDependencies.
DepGraphIndex child = idx_fn();
auto parents = subset_fn();
// Apply to DepGraph.
real.AddDependencies(parents, child);
// Apply to sim.
sim[child]->second |= parents;
continue;
}
if (num_tx_sim > 0) {
// Remove transactions.
auto del = set_fn();
// Propagate all ancestry information before deleting anything in the simulation (as
// intermediary transactions may be deleted which impact connectivity).
anc_update_fn();
// Compare the state of the transactions being deleted.
for (auto i : del) check_fn(i);
// Apply to DepGraph.
real.RemoveTransactions(del);
// Apply to sim.
for (DepGraphIndex i = 0; i < sim.size(); ++i) {
if (sim[i].has_value()) {
if (del[i]) {
--num_tx_sim;
sim[i] = std::nullopt;
} else {
sim[i]->second -= del;
int command = provider.ConsumeIntegral<uint8_t>() % 4;
while (true) {
// Iterate decreasing command until an applicable branch is found.
if (num_tx_sim < TestBitSet::Size() && command-- == 0) {
// AddTransaction.
auto fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
auto size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff);
FeeFrac feerate{fee, size};
// Apply to DepGraph.
auto idx = real.AddTransaction(feerate);
// Verify that the returned index is correct.
assert(!sim[idx].has_value());
for (DepGraphIndex i = 0; i < TestBitSet::Size(); ++i) {
if (!sim[i].has_value()) {
assert(idx == i);
break;
}
}
// Update sim.
sim[idx] = {feerate, TestBitSet::Singleton(idx)};
++num_tx_sim;
break;
} else if (num_tx_sim > 0 && command-- == 0) {
// AddDependencies.
DepGraphIndex child = idx_fn();
auto parents = subset_fn();
// Apply to DepGraph.
real.AddDependencies(parents, child);
// Apply to sim.
sim[child]->second |= parents;
break;
} else if (num_tx_sim > 0 && command-- == 0) {
// Remove transactions.
auto del = set_fn();
// Propagate all ancestry information before deleting anything in the simulation (as
// intermediary transactions may be deleted which impact connectivity).
anc_update_fn();
// Compare the state of the transactions being deleted.
for (auto i : del) check_fn(i);
// Apply to DepGraph.
real.RemoveTransactions(del);
// Apply to sim.
for (DepGraphIndex i = 0; i < sim.size(); ++i) {
if (sim[i].has_value()) {
if (del[i]) {
--num_tx_sim;
sim[i] = std::nullopt;
} else {
sim[i]->second -= del;
}
}
}
break;
} else if (command-- == 0) {
// Compact.
const size_t mem_before{real.DynamicMemoryUsage()};
real.Compact();
const size_t mem_after{real.DynamicMemoryUsage()};
assert(real.PositionRange() < last_compaction_pos ? mem_after < mem_before : mem_after <= mem_before);
last_compaction_pos = real.PositionRange();
break;
}
continue;
}
// This should be unreachable (one of the 3 above actions should always be possible).
assert(false);
}
// Compare the real obtained depgraph against the simulation.
+15
View File
@@ -1012,6 +1012,21 @@ FUZZ_TARGET(txgraph)
}
assert(!top_sim.IsOversized());
break;
} else if (command-- == 0) {
// GetMainMemoryUsage().
auto usage = real->GetMainMemoryUsage();
// Test stability.
if (alt) {
auto usage2 = real->GetMainMemoryUsage();
assert(usage == usage2);
}
// Only empty graphs have 0 memory usage.
if (main_sim.GetTransactionCount() == 0) {
assert(usage == 0);
} else {
assert(usage > 0);
}
break;
}
}
}
+753 -210
View File
File diff suppressed because it is too large. Load diff
+7
View File
@@ -202,6 +202,13 @@ public:
* graph must not be oversized. If the graph is empty, {{}, FeePerWeight{}} is returned. */
virtual std::pair<std::vector<Ref*>, FeePerWeight> GetWorstMainChunk() noexcept = 0;
/** Get the approximate memory usage for this object, just counting the main graph. If a
* staging graph is present, return a number corresponding to memory usage after
* AbortStaging() would be called. BlockBuilders' memory usage, memory usage of internally
* queued operations, and memory due to temporary caches, is not included here. Can always be
* called. */
virtual size_t GetMainMemoryUsage() noexcept = 0;
/** Perform an internal consistency check on this object. */
virtual void SanityCheck() const = 0;