clusterlin: use 'cost' terminology instead of 'iters' (refactor)

This commit is contained in:
Pieter Wuille 2026-02-21 10:11:54 -05:00 committed by Pieter Wuille
parent 9e7129df29
commit ecc9a84f85
12 changed files with 111 additions and 91 deletions

View File

@ -55,7 +55,7 @@ void BenchLinearizeOptimallyTotal(benchmark::Bench& bench, const std::string& na
// Benchmark the total time to optimal.
uint64_t rng_seed = 0;
bench.name(bench_name).run([&] {
auto [_lin, optimal, _cost] = Linearize(depgraph, /*max_iterations=*/10000000, rng_seed++, IndexTxOrder{});
auto [_lin, optimal, _cost] = Linearize(depgraph, /*max_cost=*/10000000, rng_seed++, IndexTxOrder{});
assert(optimal);
});
}
@ -72,7 +72,7 @@ void BenchLinearizeOptimallyPerCost(benchmark::Bench& bench, const std::string&
// Determine the cost of 100 rng_seeds.
uint64_t total_cost = 0;
for (uint64_t iter = 0; iter < 100; ++iter) {
auto [_lin, optimal, cost] = Linearize(depgraph, /*max_iterations=*/10000000, /*rng_seed=*/iter, IndexTxOrder{});
auto [_lin, optimal, cost] = Linearize(depgraph, /*max_cost=*/10000000, /*rng_seed=*/iter, IndexTxOrder{});
total_cost += cost;
}
@ -80,7 +80,7 @@ void BenchLinearizeOptimallyPerCost(benchmark::Bench& bench, const std::string&
bench.name(bench_name).unit("cost").batch(total_cost).run([&] {
uint64_t recompute_cost = 0;
for (uint64_t iter = 0; iter < 100; ++iter) {
auto [_lin, optimal, cost] = Linearize(depgraph, /*max_iterations=*/10000000, /*rng_seed=*/iter, IndexTxOrder{});
auto [_lin, optimal, cost] = Linearize(depgraph, /*max_cost=*/10000000, /*rng_seed=*/iter, IndexTxOrder{});
assert(optimal);
recompute_cost += cost;
}

View File

@ -51,9 +51,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
/** Set a very high number for acceptable cost, so that we certainly benchmark optimal
* linearization. */
static constexpr uint64_t NUM_ACCEPTABLE_ITERS = 100'000'000;
static constexpr uint64_t HIGH_ACCEPTABLE_COST = 100'000'000;
/** Refs to all top transactions. */
std::vector<TxGraph::Ref> top_refs;
@ -65,7 +65,7 @@ void BenchTxGraphTrim(benchmark::Bench& bench)
std::vector<size_t> top_components;
InsecureRandomContext rng(11);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, NUM_ACCEPTABLE_ITERS, PointerComparator);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, HIGH_ACCEPTABLE_COST, PointerComparator);
// Construct the top chains.
for (int chain = 0; chain < NUM_TOP_CHAINS; ++chain) {

View File

@ -1742,7 +1742,7 @@ public:
/** Find or improve a linearization for a cluster.
*
* @param[in] depgraph Dependency graph of the cluster to be linearized.
* @param[in] max_iterations Upper bound on the amount of work that will be done.
* @param[in] max_cost Upper bound on the amount of work that will be done.
* @param[in] rng_seed A random number seed to control search order. This prevents peers
* from predicting exactly which clusters would be hard for us to
* linearize.
@ -1762,7 +1762,7 @@ public:
template<typename SetType>
std::tuple<std::vector<DepGraphIndex>, bool, uint64_t> Linearize(
const DepGraph<SetType>& depgraph,
uint64_t max_iterations,
uint64_t max_cost,
uint64_t rng_seed,
const StrongComparator<DepGraphIndex> auto& fallback_order,
std::span<const DepGraphIndex> old_linearization = {},
@ -1778,23 +1778,23 @@ std::tuple<std::vector<DepGraphIndex>, bool, uint64_t> Linearize(
}
// Make improvement steps to it until we hit the max_iterations limit, or an optimal result
// is found.
if (forest.GetCost() < max_iterations) {
if (forest.GetCost() < max_cost) {
forest.StartOptimizing();
do {
if (!forest.OptimizeStep()) break;
} while (forest.GetCost() < max_iterations);
} while (forest.GetCost() < max_cost);
}
// Make chunk minimization steps until we hit the max_iterations limit, or all chunks are
// minimal.
bool optimal = false;
if (forest.GetCost() < max_iterations) {
if (forest.GetCost() < max_cost) {
forest.StartMinimizing();
do {
if (!forest.MinimizeStep()) {
optimal = true;
break;
}
} while (forest.GetCost() < max_iterations);
} while (forest.GetCost() < max_cost);
}
return {forest.GetLinearization(fallback_order), optimal, forest.GetCost()};
}

View File

@ -88,9 +88,15 @@ void TestOptimalLinearization(std::span<const uint8_t> enc, std::initializer_lis
is_topological = false;
break;
}
std::tie(lin, opt, cost) = Linearize(depgraph, 1000000000000, rng.rand64(), IndexTxOrder{}, lin, is_topological);
std::tie(lin, opt, cost) = Linearize(
/*depgraph=*/depgraph,
/*max_cost=*/1000000000000,
/*rng_seed=*/rng.rand64(),
/*fallback_order=*/IndexTxOrder{},
/*old_linearization=*/lin,
/*is_topological=*/is_topological);
BOOST_CHECK(opt);
BOOST_CHECK(cost <= MaxOptimalLinearizationIters(depgraph.TxCount()));
BOOST_CHECK(cost <= MaxOptimalLinearizationCost(depgraph.TxCount()));
SanityCheck(depgraph, lin);
BOOST_CHECK(std::ranges::equal(lin, optimal_linearization));
}

View File

@ -984,7 +984,7 @@ FUZZ_TARGET(clusterlin_sfl)
// Verify that optimality is reached within an expected amount of work. This protects against
// hypothetical bugs that hugely increase the amount of work needed to reach optimality.
assert(sfl.GetCost() <= MaxOptimalLinearizationIters(depgraph.TxCount()));
assert(sfl.GetCost() <= MaxOptimalLinearizationCost(depgraph.TxCount()));
// The result must be as good as SimpleLinearize.
auto [simple_linearization, simple_optimal] = SimpleLinearize(depgraph, MAX_SIMPLE_ITERATIONS / 10);
@ -1011,15 +1011,15 @@ FUZZ_TARGET(clusterlin_linearize)
{
// Verify the behavior of Linearize().
// Retrieve an RNG seed, an iteration count, a depgraph, and whether to make it connected from
// the fuzz input.
// Retrieve an RNG seed, a maximum amount of work, a depgraph, and whether to make it connected
// from the fuzz input.
SpanReader reader(buffer);
DepGraph<TestBitSet> depgraph;
uint64_t rng_seed{0};
uint64_t iter_count{0};
uint64_t max_cost{0};
uint8_t flags{7};
try {
reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph) >> rng_seed >> flags;
reader >> VARINT(max_cost) >> Using<DepGraphFormatter>(depgraph) >> rng_seed >> flags;
} catch (const std::ios_base::failure&) {}
bool make_connected = flags & 1;
// The following 3 booleans have 4 combinations:
@ -1043,8 +1043,14 @@ FUZZ_TARGET(clusterlin_linearize)
}
// Invoke Linearize().
iter_count &= 0x7ffff;
auto [linearization, optimal, cost] = Linearize(depgraph, iter_count, rng_seed, IndexTxOrder{}, old_linearization, /*is_topological=*/claim_topological_input);
max_cost &= 0x7ffff;
auto [linearization, optimal, cost] = Linearize(
/*depgraph=*/depgraph,
/*max_cost=*/max_cost,
/*rng_seed=*/rng_seed,
/*fallback_order=*/IndexTxOrder{},
/*old_linearization=*/old_linearization,
/*is_topological=*/claim_topological_input);
SanityCheck(depgraph, linearization);
auto chunking = ChunkLinearization(depgraph, linearization);
@ -1056,8 +1062,8 @@ FUZZ_TARGET(clusterlin_linearize)
assert(cmp >= 0);
}
// If the iteration count is sufficiently high, an optimal linearization must be found.
if (iter_count > MaxOptimalLinearizationIters(depgraph.TxCount())) {
// If the maximum amount of work is sufficiently high, an optimal linearization must be found.
if (max_cost > MaxOptimalLinearizationCost(depgraph.TxCount())) {
assert(optimal);
}
@ -1145,7 +1151,7 @@ FUZZ_TARGET(clusterlin_linearize)
// Redo from scratch with a different rng_seed. The resulting linearization should be
// deterministic, if both are optimal.
auto [linearization2, optimal2, cost2] = Linearize(depgraph, MaxOptimalLinearizationIters(depgraph.TxCount()) + 1, rng_seed ^ 0x1337, IndexTxOrder{});
auto [linearization2, optimal2, cost2] = Linearize(depgraph, MaxOptimalLinearizationCost(depgraph.TxCount()) + 1, rng_seed ^ 0x1337, IndexTxOrder{});
assert(optimal2);
assert(linearization2 == linearization);
}

View File

@ -325,8 +325,8 @@ 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);
/** The amount of work to consider a cluster acceptably linearized. */
auto acceptable_cost = provider.ConsumeIntegralInRange<uint64_t>(0, 10000);
/** The set of uint64_t "txid"s that have been assigned before. */
std::set<uint64_t> assigned_txids;
@ -342,7 +342,7 @@ FUZZ_TARGET(txgraph)
auto real = MakeTxGraph(
/*max_cluster_count=*/max_cluster_count,
/*max_cluster_size=*/max_cluster_size,
/*acceptable_iters=*/acceptable_iters,
/*acceptable_cost=*/acceptable_cost,
/*fallback_order=*/fallback_order);
std::vector<SimTxGraph> sims;
@ -758,9 +758,9 @@ FUZZ_TARGET(txgraph)
break;
} else if (command-- == 0) {
// DoWork.
uint64_t iters = provider.ConsumeIntegralInRange<uint64_t>(0, alt ? 10000 : 255);
bool ret = real->DoWork(iters);
uint64_t iters_for_optimal{0};
uint64_t max_cost = provider.ConsumeIntegralInRange<uint64_t>(0, alt ? 10000 : 255);
bool ret = real->DoWork(max_cost);
uint64_t cost_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
@ -773,24 +773,24 @@ FUZZ_TARGET(txgraph)
if (ret) {
sims[level].real_is_optimal = true;
}
// Compute how many iterations would be needed to make everything optimal.
// Compute how much work 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
auto cost_opt_this_cluster = MaxOptimalLinearizationCost(component.Count());
if (cost_opt_this_cluster > acceptable_cost) {
// If the amount of work required to linearize this cluster
// optimally exceeds acceptable_cost, DoWork() may process it in two
// stages: once to acceptable, and once to optimal.
iters_for_optimal += iters_opt_this_cluster + acceptable_iters;
cost_for_optimal += cost_opt_this_cluster + acceptable_cost;
} else {
iters_for_optimal += iters_opt_this_cluster;
cost_for_optimal += cost_opt_this_cluster;
}
}
}
if (!ret) {
// DoWork can only have more work left if the requested number of iterations
// DoWork can only have more work left if the requested amount of work
// was insufficient to linearize everything optimally within the levels it is
// allowed to touch.
assert(iters <= iters_for_optimal);
assert(max_cost <= cost_for_optimal);
}
break;
} else if (sims.size() == 2 && !sims[0].IsOversized() && !sims[1].IsOversized() && command-- == 0) {
@ -1165,7 +1165,7 @@ FUZZ_TARGET(txgraph)
auto real_redo = MakeTxGraph(
/*max_cluster_count=*/max_cluster_count,
/*max_cluster_size=*/max_cluster_size,
/*acceptable_iters=*/acceptable_iters,
/*acceptable_cost=*/acceptable_cost,
/*fallback_order=*/fallback_order);
/** Vector (indexed by SimTxGraph::Pos) of TxObjects in real_redo). */
std::vector<std::optional<SimTxObject>> txobjects_redo;

View File

@ -15,9 +15,9 @@ BOOST_AUTO_TEST_SUITE(txgraph_tests)
namespace {
/** The number used as acceptable_iters argument in these tests. High enough that everything
/** The number used as acceptable_cost argument in these tests. High enough that everything
* should be optimal, always. */
constexpr uint64_t NUM_ACCEPTABLE_ITERS = 100'000'000;
constexpr uint64_t HIGH_ACCEPTABLE_COST = 100'000'000;
std::strong_ordering PointerComparator(const TxGraph::Ref& a, const TxGraph::Ref& b) noexcept
{
@ -48,7 +48,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, NUM_ACCEPTABLE_ITERS, PointerComparator);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, HIGH_ACCEPTABLE_COST, PointerComparator);
// Add all transactions and store their Refs.
std::vector<TxGraph::Ref> refs;
@ -111,7 +111,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, NUM_ACCEPTABLE_ITERS, PointerComparator);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, HIGH_ACCEPTABLE_COST, PointerComparator);
// Add all transactions and store their Refs.
std::vector<TxGraph::Ref> refs;
@ -197,7 +197,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, NUM_ACCEPTABLE_ITERS, PointerComparator);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, HIGH_ACCEPTABLE_COST, PointerComparator);
// Construct the top chains.
for (int chain = 0; chain < NUM_TOP_CHAINS; ++chain) {
@ -270,7 +270,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, NUM_ACCEPTABLE_ITERS, PointerComparator);
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE, HIGH_ACCEPTABLE_COST, PointerComparator);
// Add all transactions and store their Refs.
std::vector<TxGraph::Ref> refs;
@ -304,7 +304,7 @@ BOOST_AUTO_TEST_CASE(txgraph_trim_big_singletons)
BOOST_AUTO_TEST_CASE(txgraph_chunk_chain)
{
// Create a new graph for the test.
auto graph = MakeTxGraph(50, 1000, NUM_ACCEPTABLE_ITERS, PointerComparator);
auto graph = MakeTxGraph(50, 1000, HIGH_ACCEPTABLE_COST, PointerComparator);
auto block_builder_checker = [&graph](std::vector<std::vector<TxGraph::Ref*>> expected_chunks) {
std::vector<std::vector<TxGraph::Ref*>> chunks;
@ -383,7 +383,7 @@ BOOST_AUTO_TEST_CASE(txgraph_staging)
/* Create a new graph for the test.
* The parameters are max_cluster_count, max_cluster_size, acceptable_iters
*/
auto graph = MakeTxGraph(10, 1000, NUM_ACCEPTABLE_ITERS, PointerComparator);
auto graph = MakeTxGraph(10, 1000, HIGH_ACCEPTABLE_COST, PointerComparator);
std::vector<TxGraph::Ref> refs;
refs.reserve(2);

View File

@ -394,13 +394,13 @@ void SanityCheck(const DepGraph<SetType>& depgraph, std::span<const DepGraphInde
}
}
inline uint64_t MaxOptimalLinearizationIters(DepGraphIndex cluster_count)
inline uint64_t MaxOptimalLinearizationCost(DepGraphIndex cluster_count)
{
// These are the largest numbers seen returned as cost by Linearize(), in a large randomized
// trial. There exist almost certainly far worse cases, but they are unlikely to be
// encountered in randomized tests. The purpose of these numbers is guaranteeing that for
// *some* reasonable cost bound, optimal linearizations are always found.
static constexpr uint64_t ITERS[65] = {
static constexpr uint64_t COSTS[65] = {
0,
0, 4, 10, 34, 76, 156, 229, 380,
441, 517, 678, 933, 1037, 1366, 1464, 1711,
@ -411,9 +411,9 @@ inline uint64_t MaxOptimalLinearizationIters(DepGraphIndex cluster_count)
34046, 26227, 34662, 38019, 40814, 31113, 41448, 33968,
35024, 59207, 42872, 41277, 42365, 51833, 63410, 67035
};
assert(cluster_count < std::size(ITERS));
assert(cluster_count < std::size(COSTS));
// Multiply the table number by two, to account for the fact that they are not absolutes.
return ITERS[cluster_count] * 2;
return COSTS[cluster_count] * 2;
}
} // namespace

View File

@ -217,7 +217,7 @@ public:
virtual void ApplyDependencies(TxGraphImpl& graph, int level, std::span<std::pair<GraphIndex, GraphIndex>> to_apply) noexcept = 0;
/** Improve the linearization of this Cluster. Returns how much work was performed and whether
* the Cluster's QualityLevel improved as a result. */
virtual std::pair<uint64_t, bool> Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept = 0;
virtual std::pair<uint64_t, bool> Relinearize(TxGraphImpl& graph, int level, uint64_t max_cost) noexcept = 0;
/** For every chunk in the cluster, append its FeeFrac to ret. */
virtual void AppendChunkFeerates(std::vector<FeeFrac>& ret) const noexcept = 0;
/** Add a TrimTxData entry (filling m_chunk_feerate, m_index, m_tx_size) for every
@ -296,7 +296,7 @@ public:
[[nodiscard]] bool Split(TxGraphImpl& graph, int level) noexcept final;
void Merge(TxGraphImpl& graph, int level, Cluster& cluster) noexcept final;
void ApplyDependencies(TxGraphImpl& graph, int level, std::span<std::pair<GraphIndex, GraphIndex>> to_apply) noexcept final;
std::pair<uint64_t, bool> Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept final;
std::pair<uint64_t, bool> Relinearize(TxGraphImpl& graph, int level, uint64_t max_cost) noexcept final;
void AppendChunkFeerates(std::vector<FeeFrac>& ret) const noexcept final;
uint64_t AppendTrimData(std::vector<TrimTxData>& ret, std::vector<std::pair<GraphIndex, GraphIndex>>& deps) const noexcept final;
void GetAncestorRefs(const TxGraphImpl& graph, std::span<std::pair<Cluster*, DepGraphIndex>>& args, std::vector<TxGraph::Ref*>& output) noexcept final;
@ -353,7 +353,7 @@ public:
[[nodiscard]] bool Split(TxGraphImpl& graph, int level) noexcept final;
void Merge(TxGraphImpl& graph, int level, Cluster& cluster) noexcept final;
void ApplyDependencies(TxGraphImpl& graph, int level, std::span<std::pair<GraphIndex, GraphIndex>> to_apply) noexcept final;
std::pair<uint64_t, bool> Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept final;
std::pair<uint64_t, bool> Relinearize(TxGraphImpl& graph, int level, uint64_t max_cost) noexcept final;
void AppendChunkFeerates(std::vector<FeeFrac>& ret) const noexcept final;
uint64_t AppendTrimData(std::vector<TrimTxData>& ret, std::vector<std::pair<GraphIndex, GraphIndex>>& deps) const noexcept final;
void GetAncestorRefs(const TxGraphImpl& graph, std::span<std::pair<Cluster*, DepGraphIndex>>& args, std::vector<TxGraph::Ref*>& output) noexcept final;
@ -400,9 +400,8 @@ 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;
/** The amount of linearization work needed per cluster to be considered acceptable. */
const uint64_t m_acceptable_cost;
/** Fallback ordering for transactions. */
const std::function<std::strong_ordering(const TxGraph::Ref&, const TxGraph::Ref&)> m_fallback_order;
@ -636,12 +635,12 @@ public:
explicit TxGraphImpl(
DepGraphIndex max_cluster_count,
uint64_t max_cluster_size,
uint64_t acceptable_iters,
uint64_t acceptable_cost,
const std::function<std::strong_ordering(const TxGraph::Ref&, const TxGraph::Ref&)>& fallback_order
) noexcept :
m_max_cluster_count(max_cluster_count),
m_max_cluster_size(max_cluster_size),
m_acceptable_iters(acceptable_iters),
m_acceptable_cost(acceptable_cost),
m_fallback_order(fallback_order),
m_main_chunkindex(ChunkOrder(this))
{
@ -803,7 +802,7 @@ public:
void AddDependency(const Ref& parent, const Ref& child) noexcept final;
void SetTransactionFee(const Ref&, int64_t fee) noexcept final;
bool DoWork(uint64_t iters) noexcept final;
bool DoWork(uint64_t max_cost) noexcept final;
void StartStaging() noexcept final;
void CommitStaging() noexcept final;
@ -2155,7 +2154,7 @@ void TxGraphImpl::ApplyDependencies(int level) noexcept
clusterset.m_group_data = GroupData{};
}
std::pair<uint64_t, bool> GenericClusterImpl::Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept
std::pair<uint64_t, bool> GenericClusterImpl::Relinearize(TxGraphImpl& graph, int level, uint64_t max_cost) noexcept
{
// We can only relinearize Clusters that do not need splitting.
Assume(!NeedsSplitting());
@ -2168,7 +2167,13 @@ std::pair<uint64_t, bool> GenericClusterImpl::Relinearize(TxGraphImpl& graph, in
const auto ref_b = graph.m_entries[m_mapping[b]].m_ref;
return graph.m_fallback_order(*ref_a, *ref_b);
};
auto [linearization, optimal, cost] = Linearize(m_depgraph, max_iters, rng_seed, fallback_order, m_linearization, /*is_topological=*/IsTopological());
auto [linearization, optimal, cost] = Linearize(
/*depgraph=*/m_depgraph,
/*max_cost=*/max_cost,
/*rng_seed=*/rng_seed,
/*fallback_order=*/fallback_order,
/*old_linearization=*/m_linearization,
/*is_topological=*/IsTopological());
// Postlinearize to improve the linearization (if optimal, only the sub-chunk order).
// This also guarantees that all chunks are connected (even when non-optimal).
PostLinearize(m_depgraph, linearization);
@ -2179,7 +2184,7 @@ std::pair<uint64_t, bool> GenericClusterImpl::Relinearize(TxGraphImpl& graph, in
if (optimal) {
graph.SetClusterQuality(level, m_quality, m_setindex, QualityLevel::OPTIMAL);
improved = true;
} else if (max_iters >= graph.m_acceptable_iters && !IsAcceptable()) {
} else if (max_cost >= graph.m_acceptable_cost && !IsAcceptable()) {
graph.SetClusterQuality(level, m_quality, m_setindex, QualityLevel::ACCEPTABLE);
improved = true;
} else if (!IsTopological()) {
@ -2191,7 +2196,7 @@ std::pair<uint64_t, bool> GenericClusterImpl::Relinearize(TxGraphImpl& graph, in
return {cost, improved};
}
std::pair<uint64_t, bool> SingletonClusterImpl::Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept
std::pair<uint64_t, bool> SingletonClusterImpl::Relinearize(TxGraphImpl& graph, int level, uint64_t max_cost) noexcept
{
// All singletons are optimal, oversized, or need splitting. Each of these precludes
// Relinearize from being called.
@ -2203,7 +2208,7 @@ void TxGraphImpl::MakeAcceptable(Cluster& cluster, int level) noexcept
{
// Relinearize the Cluster if needed.
if (!cluster.NeedsSplitting() && !cluster.IsAcceptable() && !cluster.IsOversized()) {
cluster.Relinearize(*this, level, m_acceptable_iters);
cluster.Relinearize(*this, level, m_acceptable_cost);
}
}
@ -3105,9 +3110,9 @@ void TxGraphImpl::SanityCheck() const
assert(actual_chunkindex == expected_chunkindex);
}
bool TxGraphImpl::DoWork(uint64_t iters) noexcept
bool TxGraphImpl::DoWork(uint64_t max_cost) noexcept
{
uint64_t iters_done{0};
uint64_t cost_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_FIX, QualityLevel::NEEDS_RELINEARIZE, QualityLevel::ACCEPTABLE}) {
@ -3121,23 +3126,23 @@ bool TxGraphImpl::DoWork(uint64_t iters) noexcept
if (clusterset.m_oversized == true) continue;
auto& queue = clusterset.m_clusters[int(quality)];
while (!queue.empty()) {
if (iters_done >= iters) return false;
if (cost_done >= max_cost) 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
// needs more work than what max_cost 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;
auto cost_now = max_cost - cost_done;
if (quality == QualityLevel::NEEDS_FIX || 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
// up to m_acceptable_cost work. 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);
cost_now = std::min(cost_now, m_acceptable_cost);
}
auto [cost, improved] = queue[pos].get()->Relinearize(*this, level, iters_now);
iters_done += cost;
auto [cost, improved] = queue[pos].get()->Relinearize(*this, level, cost_now);
cost_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
// budget. Even though it may be the case that cost_done < max_cost 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.
@ -3565,8 +3570,12 @@ TxGraph::Ref::Ref(Ref&& other) noexcept
std::unique_ptr<TxGraph> MakeTxGraph(
unsigned max_cluster_count,
uint64_t max_cluster_size,
uint64_t acceptable_iters,
uint64_t acceptable_cost,
const std::function<std::strong_ordering(const TxGraph::Ref&, const TxGraph::Ref&)>& fallback_order) noexcept
{
return std::make_unique<TxGraphImpl>(max_cluster_count, max_cluster_size, acceptable_iters, fallback_order);
return std::make_unique<TxGraphImpl>(
/*max_cluster_count=*/max_cluster_count,
/*max_cluster_size=*/max_cluster_size,
/*acceptable_cost=*/acceptable_cost,
/*fallback_order=*/fallback_order);
}

View File

@ -102,10 +102,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 perform some work now (controlled by iters) so that future operations
* Calling DoWork will perform some work now (controlled by max_cost) 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;
virtual bool DoWork(uint64_t max_cost) 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
@ -257,7 +257,7 @@ public:
* and on the sum of transaction sizes within a cluster.
*
* - max_cluster_count cannot exceed MAX_CLUSTER_COUNT_LIMIT.
* - acceptable_iters controls how many linearization optimization steps will be performed per
* - acceptable_cost controls how much linearization optimization work will be performed per
* cluster before they are considered to be of acceptable quality.
* - fallback_order determines how to break tie-breaks between transactions:
* fallback_order(a, b) < 0 means a is "better" than b, and will (in case of ties) be placed
@ -266,7 +266,7 @@ public:
std::unique_ptr<TxGraph> MakeTxGraph(
unsigned max_cluster_count,
uint64_t max_cluster_size,
uint64_t acceptable_iters,
uint64_t acceptable_cost,
const std::function<std::strong_ordering(const TxGraph::Ref&, const TxGraph::Ref&)>& fallback_order
) noexcept;

View File

@ -179,7 +179,7 @@ CTxMemPool::CTxMemPool(Options opts, bilingual_str& error)
m_txgraph = MakeTxGraph(
/*max_cluster_count=*/m_opts.limits.cluster_count,
/*max_cluster_size=*/m_opts.limits.cluster_size_vbytes * WITNESS_SCALE_FACTOR,
/*acceptable_iters=*/ACCEPTABLE_ITERS,
/*acceptable_cost=*/ACCEPTABLE_COST,
/*fallback_order=*/[&](const TxGraph::Ref& a, const TxGraph::Ref& b) noexcept {
const Txid& txid_a = static_cast<const CTxMemPoolEntry&>(a).GetTx().GetHash();
const Txid& txid_b = static_cast<const CTxMemPoolEntry&>(b).GetTx().GetHash();
@ -221,7 +221,7 @@ void CTxMemPool::Apply(ChangeSet* changeset)
addNewTransaction(it);
}
if (!m_txgraph->DoWork(POST_CHANGE_WORK)) {
if (!m_txgraph->DoWork(/*max_cost=*/POST_CHANGE_COST)) {
LogDebug(BCLog::MEMPOOL, "Mempool in non-optimal ordering after addition(s).");
}
}
@ -380,7 +380,7 @@ void CTxMemPool::removeForReorg(CChain& chain, std::function<bool(txiter)> check
for (indexed_transaction_set::const_iterator it = mapTx.begin(); it != mapTx.end(); it++) {
assert(TestLockPointValidity(chain, it->GetLockPoints()));
}
if (!m_txgraph->DoWork(POST_CHANGE_WORK)) {
if (!m_txgraph->DoWork(/*max_cost=*/POST_CHANGE_COST)) {
LogDebug(BCLog::MEMPOOL, "Mempool in non-optimal ordering after reorg.");
}
}
@ -425,7 +425,7 @@ void CTxMemPool::removeForBlock(const std::vector<CTransactionRef>& vtx, unsigne
}
lastRollingFeeUpdate = GetTime();
blockSinceLastRollingFeeBump = true;
if (!m_txgraph->DoWork(POST_CHANGE_WORK)) {
if (!m_txgraph->DoWork(/*max_cost=*/POST_CHANGE_COST)) {
LogDebug(BCLog::MEMPOOL, "Mempool in non-optimal ordering after block.");
}
}

View File

@ -49,14 +49,13 @@ struct bilingual_str;
/** Fake height value used in Coin to signify they are only in the memory pool (since 0.8) */
static const uint32_t MEMPOOL_HEIGHT = 0x7FFFFFFF;
/** How many linearization iterations required for TxGraph clusters to have
* "acceptable" quality, if they cannot be optimally linearized with fewer
* iterations. */
static constexpr uint64_t ACCEPTABLE_ITERS = 1'700;
/** How much linearization cost required for TxGraph clusters to have
* "acceptable" quality, if they cannot be optimally linearized with less cost. */
static constexpr uint64_t ACCEPTABLE_COST = 1'700;
/** How much work we ask TxGraph to do after a mempool change occurs (either
* due to a changeset being applied, a new block being found, or a reorg). */
static constexpr uint64_t POST_CHANGE_WORK = 5 * ACCEPTABLE_ITERS;
static constexpr uint64_t POST_CHANGE_COST = 5 * ACCEPTABLE_COST;
/**
* Test whether the LockPoints height and time are still valid on the current chain