Merge bitcoin/bitcoin#32263: cluster mempool: add TxGraph work controls

62ed1f92ef txgraph: check that DoWork finds optimal if given high budget (tests) (Pieter Wuille)
f3c2fc867f txgraph: add work limit to DoWork(), try optimal (feature) (Pieter Wuille)
e96b00d99e txgraph: make number of acceptable iterations configurable (feature) (Pieter Wuille)
cfe9958852 txgraph: track amount of work done in linearization (preparation) (Pieter Wuille)
6ba316eaa0 txgraph: 1-or-2-tx split-off clusters are optimal (optimization) (Pieter Wuille)
fad0eb091e txgraph: reset quality when merging clusters (bugfix) (Pieter Wuille)

Pull request description:

  Part of #30289. Builds on top of #31553.

  So far, the `TxGraph::DoWork()` function took no parameters, and just made all clusters reach the "acceptable" internal quality level by performing a minimum number of improvement iterations on it, but:
  * Did not attempt to go beyond that.
  * Was broken, as the QualityLevel of optimal clusters that merge together was not being reset.

  Fix this by adding an argument to `DoWork()` to control how much work it is allowed to do right now, which will first be used to get all clusters to the acceptable level, and if more budget remains, use it to try to get some or all clusters optimal. The function will now return `true` if all clusters are known to be optimal (and thus no further work remains). This is verified in the tests, by remembering whether the graph is optimal, and if it is at the end of the simulation run, verify that the overall linearization cannot be improved further.

ACKs for top commit:
  instagibbs:
    ACK 62ed1f92ef
  ismaelsadeeq:
    Code review ACK 62ed1f92ef
  glozow:
    ACK 62ed1f92ef

Tree-SHA512: 5f57d4052e369f3444e72e724f04c02004e0f66e365faa59c9f145323e606508380fc97bb038b68783a62ae9c10757f1b628b3b00b2ce9a46161fca2d4336d73
This commit is contained in:
merge-script
2025-07-29 09:07:10 -04:00
9 changed files with 182 additions and 54 deletions

View File

@@ -229,8 +229,8 @@ void BenchLinearizeOptimally(benchmark::Bench& bench, const std::array<uint8_t,
reader >> Using<DepGraphFormatter>(depgraph);
uint64_t rng_seed = 0;
bench.run([&] {
auto res = Linearize(depgraph, /*max_iterations=*/10000000, rng_seed++);
assert(res.second);
auto [_lin, optimal, _cost] = Linearize(depgraph, /*max_iterations=*/10000000, rng_seed++);
assert(optimal);
});
};

View File

@@ -46,6 +46,9 @@ void BenchTxGraphTrim(benchmark::Bench& bench)
static constexpr int NUM_DEPS_PER_BOTTOM_TX = 100;
/** Set a very large cluster size limit so that only the count limit is triggered. */
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000 * 100;
/** Set a very high number for acceptable iterations, so that we certainly benchmark optimal
* linearization. */
static constexpr uint64_t NUM_ACCEPTABLE_ITERS = 100'000'000;
/** Refs to all top transactions. */
std::vector<TxGraph::Ref> top_refs;
@@ -57,7 +60,7 @@ void BenchTxGraphTrim(benchmark::Bench& bench)
std::vector<size_t> top_components;
InsecureRandomContext rng(11);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS);
// Construct the top chains.
for (int chain = 0; chain < NUM_TOP_CHAINS; ++chain) {

View File

@@ -1030,19 +1030,20 @@ public:
* linearize.
* @param[in] old_linearization An existing linearization for the cluster (which must be
* topologically valid), or empty.
* @return A pair of:
* @return A tuple of:
* - The resulting linearization. It is guaranteed to be at least as
* good (in the feerate diagram sense) as old_linearization.
* - A boolean indicating whether the result is guaranteed to be
* optimal.
* - How many optimization steps were actually performed.
*
* Complexity: possibly O(N * min(max_iterations + N, sqrt(2^N))) where N=depgraph.TxCount().
*/
template<typename SetType>
std::pair<std::vector<DepGraphIndex>, bool> Linearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations, uint64_t rng_seed, std::span<const DepGraphIndex> old_linearization = {}) noexcept
std::tuple<std::vector<DepGraphIndex>, bool, uint64_t> Linearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations, uint64_t rng_seed, std::span<const DepGraphIndex> old_linearization = {}) noexcept
{
Assume(old_linearization.empty() || old_linearization.size() == depgraph.TxCount());
if (depgraph.TxCount() == 0) return {{}, true};
if (depgraph.TxCount() == 0) return {{}, true, 0};
uint64_t iterations_left = max_iterations;
std::vector<DepGraphIndex> linearization;
@@ -1113,7 +1114,7 @@ std::pair<std::vector<DepGraphIndex>, bool> Linearize(const DepGraph<SetType>& d
}
}
return {std::move(linearization), optimal};
return {std::move(linearization), optimal, max_iterations - iterations_left};
}
/** Improve a given linearization.

View File

@@ -1154,7 +1154,8 @@ FUZZ_TARGET(clusterlin_linearize)
// Invoke Linearize().
iter_count &= 0x7ffff;
auto [linearization, optimal] = Linearize(depgraph, iter_count, rng_seed, old_linearization);
auto [linearization, optimal, cost] = Linearize(depgraph, iter_count, rng_seed, old_linearization);
assert(cost <= iter_count);
SanityCheck(depgraph, linearization);
auto chunking = ChunkLinearization(depgraph, linearization);
@@ -1166,24 +1167,9 @@ FUZZ_TARGET(clusterlin_linearize)
}
// If the iteration count is sufficiently high, an optimal linearization must be found.
// Each linearization step can use up to 2^(k-1) iterations, with steps k=1..n. That sum is
// 2^n - 1.
const uint64_t n = depgraph.TxCount();
if (n <= 19 && iter_count > (uint64_t{1} << n)) {
if (iter_count >= MaxOptimalLinearizationIters(depgraph.TxCount())) {
assert(optimal);
}
// Additionally, if the assumption of sqrt(2^k)+1 iterations per step holds, plus ceil(k/4)
// start-up cost per step, plus ceil(n^2/64) start-up cost overall, we can compute the upper
// bound for a whole linearization (summing for k=1..n) using the Python expression
// [sum((k+3)//4 + int(math.sqrt(2**k)) + 1 for k in range(1, n + 1)) + (n**2 + 63) // 64 for n in range(0, 35)]:
static constexpr uint64_t MAX_OPTIMAL_ITERS[] = {
0, 4, 8, 12, 18, 26, 37, 51, 70, 97, 133, 182, 251, 346, 480, 666, 927, 1296, 1815, 2545,
3576, 5031, 7087, 9991, 14094, 19895, 28096, 39690, 56083, 79263, 112041, 158391, 223936,
316629, 447712
};
if (n < std::size(MAX_OPTIMAL_ITERS) && iter_count >= MAX_OPTIMAL_ITERS[n]) {
Assume(optimal);
}
// If Linearize claims optimal result, run quality tests.
if (optimal) {
@@ -1322,7 +1308,7 @@ FUZZ_TARGET(clusterlin_postlinearize_tree)
// Try to find an even better linearization directly. This must not change the diagram for the
// same reason.
auto [opt_linearization, _optimal] = Linearize(depgraph_tree, 100000, rng_seed, post_linearization);
auto [opt_linearization, _optimal, _cost] = Linearize(depgraph_tree, 100000, rng_seed, post_linearization);
auto opt_chunking = ChunkLinearization(depgraph_tree, opt_linearization);
auto cmp_opt = CompareChunks(opt_chunking, post_chunking);
assert(cmp_opt == 0);

View File

@@ -5,6 +5,7 @@
#include <cluster_linearize.h>
#include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h>
#include <test/util/cluster_linearize.h>
#include <test/util/random.h>
#include <txgraph.h>
#include <util/bitset.h>
@@ -58,6 +59,8 @@ struct SimTxGraph
SetType modified;
/** The configured maximum total size of transactions per cluster. */
uint64_t max_cluster_size;
/** Whether the corresponding real graph is known to be optimally linearized. */
bool real_is_optimal{false};
/** Construct a new SimTxGraph with the specified maximum cluster count and size. */
explicit SimTxGraph(DepGraphIndex cluster_count, uint64_t cluster_size) :
@@ -139,6 +142,7 @@ struct SimTxGraph
{
assert(graph.TxCount() < MAX_TRANSACTIONS);
auto simpos = graph.AddTransaction(feerate);
real_is_optimal = false;
MakeModified(simpos);
assert(graph.Positions()[simpos]);
simmap[simpos] = std::make_shared<TxGraph::Ref>();
@@ -158,6 +162,7 @@ struct SimTxGraph
if (chl_pos == MISSING) return;
graph.AddDependencies(SetType::Singleton(par_pos), chl_pos);
MakeModified(par_pos);
real_is_optimal = false;
// This may invalidate our cached oversized value.
if (oversized.has_value() && !*oversized) oversized = std::nullopt;
}
@@ -168,6 +173,7 @@ struct SimTxGraph
auto pos = Find(ref);
if (pos == MISSING) return;
// No need to invoke MakeModified, because this equally affects main and staging.
real_is_optimal = false;
graph.FeeRate(pos).fee = fee;
}
@@ -177,6 +183,7 @@ struct SimTxGraph
auto pos = Find(ref);
if (pos == MISSING) return;
MakeModified(pos);
real_is_optimal = false;
graph.RemoveTransactions(SetType::Singleton(pos));
simrevmap.erase(simmap[pos].get());
// Retain the TxGraph::Ref corresponding to this position, so the Ref destruction isn't
@@ -203,6 +210,7 @@ struct SimTxGraph
} else {
MakeModified(pos);
graph.RemoveTransactions(SetType::Singleton(pos));
real_is_optimal = false;
simrevmap.erase(simmap[pos].get());
simmap[pos].reset();
// This may invalidate our cached oversized value.
@@ -309,9 +317,11 @@ FUZZ_TARGET(txgraph)
auto max_cluster_count = provider.ConsumeIntegralInRange<DepGraphIndex>(1, MAX_CLUSTER_COUNT_LIMIT);
/** The maximum total size of transactions in a (non-oversized) cluster. */
auto max_cluster_size = provider.ConsumeIntegralInRange<uint64_t>(1, 0x3fffff * MAX_CLUSTER_COUNT_LIMIT);
/** The number of iterations to consider a cluster acceptably linearized. */
auto acceptable_iters = provider.ConsumeIntegralInRange<uint64_t>(0, 10000);
// Construct a real graph, and a vector of simulated graphs (main, and possibly staging).
auto real = MakeTxGraph(max_cluster_count, max_cluster_size);
auto real = MakeTxGraph(max_cluster_count, max_cluster_size, acceptable_iters);
std::vector<SimTxGraph> sims;
sims.reserve(2);
sims.emplace_back(max_cluster_count, max_cluster_size);
@@ -465,6 +475,7 @@ FUZZ_TARGET(txgraph)
if (top_sim.graph.Ancestors(pos_par)[pos_chl]) break;
}
top_sim.AddDependency(par, chl);
top_sim.real_is_optimal = false;
real->AddDependency(*par, *chl);
break;
} else if ((block_builders.empty() || sims.size() > 1) && top_sim.removed.size() < 100 && command-- == 0) {
@@ -719,7 +730,40 @@ FUZZ_TARGET(txgraph)
break;
} else if (command-- == 0) {
// DoWork.
real->DoWork();
uint64_t iters = provider.ConsumeIntegralInRange<uint64_t>(0, alt ? 10000 : 255);
bool ret = real->DoWork(iters);
uint64_t iters_for_optimal{0};
for (unsigned level = 0; level < sims.size(); ++level) {
// DoWork() will not optimize oversized levels, or the main level if a builder
// is present. Note that this impacts the DoWork() return value, as true means
// that non-optimal clusters may remain within such oversized or builder-having
// levels.
if (sims[level].IsOversized()) continue;
if (level == 0 && !block_builders.empty()) continue;
// If neither of the two above conditions holds, and DoWork() returned true,
// then the level is optimal.
if (ret) {
sims[level].real_is_optimal = true;
}
// Compute how many iterations would be needed to make everything optimal.
for (auto component : sims[level].GetComponents()) {
auto iters_opt_this_cluster = MaxOptimalLinearizationIters(component.Count());
if (iters_opt_this_cluster > acceptable_iters) {
// If the number of iterations required to linearize this cluster
// optimally exceeds acceptable_iters, DoWork() may process it in two
// stages: once to acceptable, and once to optimal.
iters_for_optimal += iters_opt_this_cluster + acceptable_iters;
} else {
iters_for_optimal += iters_opt_this_cluster;
}
}
}
if (!ret) {
// DoWork can only have more work left if the requested number of iterations
// was insufficient to linearize everything optimally within the levels it is
// allowed to touch.
assert(iters <= iters_for_optimal);
}
break;
} else if (sims.size() == 2 && !sims[0].IsOversized() && !sims[1].IsOversized() && command-- == 0) {
// GetMainStagingDiagrams()
@@ -1003,6 +1047,16 @@ FUZZ_TARGET(txgraph)
}
assert(todo.None());
// If the real graph claims to be optimal (the last DoWork() call returned true), verify
// that calling Linearize on it does not improve it further.
if (sims[0].real_is_optimal) {
auto real_diagram = ChunkLinearization(sims[0].graph, vec1);
auto [sim_lin, _optimal, _cost] = Linearize(sims[0].graph, 300000, rng.rand64(), vec1);
auto sim_diagram = ChunkLinearization(sims[0].graph, sim_lin);
auto cmp = CompareChunks(real_diagram, sim_diagram);
assert(cmp == 0);
}
// For every transaction in the total ordering, find a random one before it and after it,
// and compare their chunk feerates, which must be consistent with the ordering.
for (size_t pos = 0; pos < vec1.size(); ++pos) {

View File

@@ -13,6 +13,10 @@
BOOST_AUTO_TEST_SUITE(txgraph_tests)
/** The number used as acceptable_iters argument in these tests. High enough that everything
* should be optimal, always. */
static constexpr uint64_t NUM_ACCEPTABLE_ITERS = 100'000'000;
BOOST_AUTO_TEST_CASE(txgraph_trim_zigzag)
{
// T T T T T T T T T T T T T T (50 T's)
@@ -35,7 +39,7 @@ BOOST_AUTO_TEST_CASE(txgraph_trim_zigzag)
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000 * 100;
// Create a new graph for the test.
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS);
// Add all transactions and store their Refs.
std::vector<TxGraph::Ref> refs;
@@ -98,7 +102,7 @@ BOOST_AUTO_TEST_CASE(txgraph_trim_flower)
/** Set a very large cluster size limit so that only the count limit is triggered. */
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000 * 100;
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS);
// Add all transactions and store their Refs.
std::vector<TxGraph::Ref> refs;
@@ -184,7 +188,7 @@ BOOST_AUTO_TEST_CASE(txgraph_trim_huge)
std::vector<size_t> top_components;
FastRandomContext rng;
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS);
// Construct the top chains.
for (int chain = 0; chain < NUM_TOP_CHAINS; ++chain) {
@@ -256,7 +260,7 @@ BOOST_AUTO_TEST_CASE(txgraph_trim_big_singletons)
static constexpr int NUM_TOTAL_TX = 100;
// Create a new graph for the test.
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS);
// Add all transactions and store their Refs.
std::vector<TxGraph::Ref> refs;

View File

@@ -394,6 +394,29 @@ void SanityCheck(const DepGraph<SetType>& depgraph, std::span<const DepGraphInde
}
}
inline uint64_t MaxOptimalLinearizationIters(DepGraphIndex cluster_count)
{
// We assume sqrt(2^k)+1 candidate-finding iterations per candidate to be found, plus ceil(k/4)
// startup cost when up to k unlinearization transactions remain, plus ceil(n^2/64) overall
// startup cost in Linearize. Thus, we can compute the upper bound for a whole linearization
// (summing for k=1..n) using the Python expression:
//
// [sum((k+3)//4 + math.isqrt(2**k) + 1 for k in range(1, n + 1)) + (n**2 + 63) // 64 for n in range(0, 65)]
//
// Note that these are just assumptions, as the proven upper bound grows with 2^k, not
// sqrt(2^k).
static constexpr uint64_t MAX_OPTIMAL_ITERS[65] = {
0, 4, 8, 12, 18, 26, 37, 51, 70, 97, 133, 182, 251, 346, 480, 666, 927, 1296, 1815, 2545,
3576, 5031, 7087, 9991, 14094, 19895, 28096, 39690, 56083, 79263, 112041, 158391, 223936,
316629, 447712, 633086, 895241, 1265980, 1790280, 2531747, 3580335, 5063259, 7160424,
10126257, 14320575, 20252230, 28640853, 40504150, 57281380, 81007962, 114562410, 162015557,
229124437, 324030718, 458248463, 648061011, 916496483, 1296121563, 1832992493, 2592242635,
3665984477, 5184484745, 7331968412, 10368968930, 14663936244
};
assert(cluster_count < sizeof(MAX_OPTIMAL_ITERS) / sizeof(MAX_OPTIMAL_ITERS[0]));
return MAX_OPTIMAL_ITERS[cluster_count];
}
} // namespace
#endif // BITCOIN_TEST_UTIL_CLUSTER_LINEARIZE_H

View File

@@ -189,8 +189,9 @@ public:
void Merge(TxGraphImpl& graph, Cluster& cluster) noexcept;
/** Given a span of (parent, child) pairs that all belong to this Cluster, apply them. */
void ApplyDependencies(TxGraphImpl& graph, std::span<std::pair<GraphIndex, GraphIndex>> to_apply) noexcept;
/** Improve the linearization of this Cluster. */
void Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept;
/** Improve the linearization of this Cluster. Returns how much work was performed and whether
* the Cluster's QualityLevel improved as a result. */
std::pair<uint64_t, bool> Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept;
/** For every chunk in the cluster, append its FeeFrac to ret. */
void AppendChunkFeerates(std::vector<FeeFrac>& ret) const noexcept;
/** Add a TrimTxData entry (filling m_chunk_feerate, m_index, m_tx_size) for every
@@ -255,6 +256,9 @@ private:
const DepGraphIndex m_max_cluster_count;
/** This TxGraphImpl's maximum cluster size limit. */
const uint64_t m_max_cluster_size;
/** The number of linearization improvement steps needed per cluster to be considered
* acceptable. */
const uint64_t m_acceptable_iters;
/** Information about one group of Clusters to be merged. */
struct GroupEntry
@@ -456,9 +460,10 @@ private:
public:
/** Construct a new TxGraphImpl with the specified limits. */
explicit TxGraphImpl(DepGraphIndex max_cluster_count, uint64_t max_cluster_size) noexcept :
explicit TxGraphImpl(DepGraphIndex max_cluster_count, uint64_t max_cluster_size, uint64_t acceptable_iters) noexcept :
m_max_cluster_count(max_cluster_count),
m_max_cluster_size(max_cluster_size),
m_acceptable_iters(acceptable_iters),
m_main_chunkindex(ChunkOrder(this))
{
Assume(max_cluster_count >= 1);
@@ -588,7 +593,7 @@ public:
void AddDependency(const Ref& parent, const Ref& child) noexcept final;
void SetTransactionFee(const Ref&, int64_t fee) noexcept final;
void DoWork() noexcept final;
bool DoWork(uint64_t iters) noexcept final;
void StartStaging() noexcept final;
void CommitStaging() noexcept final;
@@ -978,10 +983,11 @@ bool Cluster::Split(TxGraphImpl& graph) noexcept
// Iterate over the connected components of this Cluster's m_depgraph.
while (todo.Any()) {
auto component = m_depgraph.FindConnectedComponent(todo);
auto split_quality = component.Count() <= 2 ? QualityLevel::OPTIMAL : new_quality;
if (first && component == todo) {
// The existing Cluster is an entire component. Leave it be, but update its quality.
Assume(todo == m_depgraph.Positions());
graph.SetClusterQuality(m_level, m_quality, m_setindex, new_quality);
graph.SetClusterQuality(m_level, m_quality, m_setindex, split_quality);
// If this made the quality ACCEPTABLE or OPTIMAL, we need to compute and cache its
// chunking.
Updated(graph);
@@ -996,7 +1002,7 @@ bool Cluster::Split(TxGraphImpl& graph) noexcept
for (auto i : component) {
remap[i] = {new_cluster.get(), DepGraphIndex(-1)};
}
graph.InsertCluster(m_level, std::move(new_cluster), new_quality);
graph.InsertCluster(m_level, std::move(new_cluster), split_quality);
todo -= component;
}
// Redistribute the transactions.
@@ -1108,6 +1114,11 @@ void Cluster::ApplyDependencies(TxGraphImpl& graph, std::span<std::pair<GraphInd
// linearization, and post-linearize it to fix up the worst problems with it.
FixLinearization(m_depgraph, m_linearization);
PostLinearize(m_depgraph, m_linearization);
Assume(!NeedsSplitting());
Assume(!IsOversized());
if (IsAcceptable()) {
graph.SetClusterQuality(m_level, m_quality, m_setindex, QualityLevel::NEEDS_RELINEARIZE);
}
// Finally push the changes to graph.m_entries.
Updated(graph);
@@ -1645,32 +1656,39 @@ void TxGraphImpl::ApplyDependencies(int level) noexcept
clusterset.m_group_data = GroupData{};
}
void Cluster::Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept
std::pair<uint64_t, bool> Cluster::Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept
{
// We can only relinearize Clusters that do not need splitting.
Assume(!NeedsSplitting());
// No work is required for Clusters which are already optimally linearized.
if (IsOptimal()) return;
if (IsOptimal()) return {0, false};
// Invoke the actual linearization algorithm (passing in the existing one).
uint64_t rng_seed = graph.m_rng.rand64();
auto [linearization, optimal] = Linearize(m_depgraph, max_iters, rng_seed, m_linearization);
auto [linearization, optimal, cost] = Linearize(m_depgraph, max_iters, rng_seed, m_linearization);
// Postlinearize if the result isn't optimal already. This guarantees (among other things)
// that the chunks of the resulting linearization are all connected.
if (!optimal) PostLinearize(m_depgraph, linearization);
// Update the linearization.
m_linearization = std::move(linearization);
// Update the Cluster's quality.
auto new_quality = optimal ? QualityLevel::OPTIMAL : QualityLevel::ACCEPTABLE;
graph.SetClusterQuality(m_level, m_quality, m_setindex, new_quality);
bool improved = false;
if (optimal) {
graph.SetClusterQuality(m_level, m_quality, m_setindex, QualityLevel::OPTIMAL);
improved = true;
} else if (max_iters >= graph.m_acceptable_iters && !IsAcceptable()) {
graph.SetClusterQuality(m_level, m_quality, m_setindex, QualityLevel::ACCEPTABLE);
improved = true;
}
// Update the Entry objects.
Updated(graph);
return {cost, improved};
}
void TxGraphImpl::MakeAcceptable(Cluster& cluster) noexcept
{
// Relinearize the Cluster if needed.
if (!cluster.NeedsSplitting() && !cluster.IsAcceptable() && !cluster.IsOversized()) {
cluster.Relinearize(*this, 10000);
cluster.Relinearize(*this, m_acceptable_iters);
}
}
@@ -2467,13 +2485,50 @@ void TxGraphImpl::SanityCheck() const
assert(actual_chunkindex == expected_chunkindex);
}
void TxGraphImpl::DoWork() noexcept
bool TxGraphImpl::DoWork(uint64_t iters) noexcept
{
for (int level = 0; level <= GetTopLevel(); ++level) {
if (level > 0 || m_main_chunkindex_observers == 0) {
MakeAllAcceptable(level);
uint64_t iters_done{0};
// First linearize everything in NEEDS_RELINEARIZE to an acceptable level. If more budget
// remains after that, try to make everything optimal.
for (QualityLevel quality : {QualityLevel::NEEDS_RELINEARIZE, QualityLevel::ACCEPTABLE}) {
// First linearize staging, if it exists, then main.
for (int level = GetTopLevel(); level >= 0; --level) {
// Do not modify main if it has any observers.
if (level == 0 && m_main_chunkindex_observers != 0) continue;
ApplyDependencies(level);
auto& clusterset = GetClusterSet(level);
// Do not modify oversized levels.
if (clusterset.m_oversized == true) continue;
auto& queue = clusterset.m_clusters[int(quality)];
while (!queue.empty()) {
if (iters_done >= iters) return false;
// Randomize the order in which we process, so that if the first cluster somehow
// needs more work than what iters allows, we don't keep spending it on the same
// one.
auto pos = m_rng.randrange<size_t>(queue.size());
auto iters_now = iters - iters_done;
if (quality == QualityLevel::NEEDS_RELINEARIZE) {
// If we're working with clusters that need relinearization still, only perform
// up to m_acceptable_iters iterations. If they become ACCEPTABLE, and we still
// have budget after all other clusters are ACCEPTABLE too, we'll spend the
// remaining budget on trying to make them OPTIMAL.
iters_now = std::min(iters_now, m_acceptable_iters);
}
auto [cost, improved] = queue[pos].get()->Relinearize(*this, iters_now);
iters_done += cost;
// If no improvement was made to the Cluster, it means we've essentially run out of
// budget. Even though it may be the case that iters_done < iters still, the
// linearizer decided there wasn't enough budget left to attempt anything with.
// To avoid an infinite loop that keeps trying clusters with minuscule budgets,
// stop here too.
if (!improved) return false;
}
}
}
// All possible work has been performed, so we can return true. Note that this does *not* mean
// that all clusters are optimally linearized now. It may be that there is nothing to do left
// because all non-optimal clusters are in oversized and/or observer-bearing levels.
return true;
}
void BlockBuilderImpl::Next() noexcept
@@ -2885,7 +2940,7 @@ TxGraph::Ref::Ref(Ref&& other) noexcept
std::swap(m_index, other.m_index);
}
std::unique_ptr<TxGraph> MakeTxGraph(unsigned max_cluster_count, uint64_t max_cluster_size) noexcept
std::unique_ptr<TxGraph> MakeTxGraph(unsigned max_cluster_count, uint64_t max_cluster_size, uint64_t acceptable_iters) noexcept
{
return std::make_unique<TxGraphImpl>(max_cluster_count, max_cluster_size);
return std::make_unique<TxGraphImpl>(max_cluster_count, max_cluster_size, acceptable_iters);
}

View File

@@ -94,9 +94,10 @@ public:
virtual void SetTransactionFee(const Ref& arg, int64_t fee) noexcept = 0;
/** TxGraph is internally lazy, and will not compute many things until they are needed.
* Calling DoWork will compute everything now, so that future operations are fast. This can be
* invoked while oversized. */
virtual void DoWork() noexcept = 0;
* Calling DoWork will perform some work now (controlled by iters) so that future operations
* are fast, if there is any. Returns whether all currently-available work is done. This can
* be invoked while oversized, but oversized graphs will be skipped by this call. */
virtual bool DoWork(uint64_t iters) noexcept = 0;
/** Create a staging graph (which cannot exist already). This acts as if a full copy of
* the transaction graph is made, upon which further modifications are made. This copy can
@@ -247,7 +248,8 @@ public:
/** Construct a new TxGraph with the specified limit on the number of transactions within a cluster,
* and on the sum of transaction sizes within a cluster. max_cluster_count cannot exceed
* MAX_CLUSTER_COUNT_LIMIT. */
std::unique_ptr<TxGraph> MakeTxGraph(unsigned max_cluster_count, uint64_t max_cluster_size) noexcept;
* MAX_CLUSTER_COUNT_LIMIT. acceptable_iters controls how many linearization optimization
* steps will be performed per cluster before they are considered to be of acceptable quality. */
std::unique_ptr<TxGraph> MakeTxGraph(unsigned max_cluster_count, uint64_t max_cluster_size, uint64_t acceptable_iters) noexcept;
#endif // BITCOIN_TXGRAPH_H