Skip to content

Commit f3c2fc8

Browse files
committed
txgraph: add work limit to DoWork(), try optimal (feature)
This adds an `iters` parameter to DoWork(), which controls how much work it is allowed to do right now. Additionally, DoWork() won't stop at just getting everything ACCEPTABLE, but if there is work budget left, will also attempt to get every cluster linearized optimally.
1 parent e96b00d commit f3c2fc8

File tree

3 files changed

+91
-17
lines changed

3 files changed

+91
-17
lines changed

src/test/fuzz/txgraph.cpp

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ struct SimTxGraph
5858
SetType modified;
5959
/** The configured maximum total size of transactions per cluster. */
6060
uint64_t max_cluster_size;
61+
/** Whether the corresponding real graph is known to be optimally linearized. */
62+
bool real_is_optimal{false};
6163

6264
/** Construct a new SimTxGraph with the specified maximum cluster count and size. */
6365
explicit SimTxGraph(DepGraphIndex cluster_count, uint64_t cluster_size) :
@@ -139,6 +141,7 @@ struct SimTxGraph
139141
{
140142
assert(graph.TxCount() < MAX_TRANSACTIONS);
141143
auto simpos = graph.AddTransaction(feerate);
144+
real_is_optimal = false;
142145
MakeModified(simpos);
143146
assert(graph.Positions()[simpos]);
144147
simmap[simpos] = std::make_shared<TxGraph::Ref>();
@@ -158,6 +161,7 @@ struct SimTxGraph
158161
if (chl_pos == MISSING) return;
159162
graph.AddDependencies(SetType::Singleton(par_pos), chl_pos);
160163
MakeModified(par_pos);
164+
real_is_optimal = false;
161165
// This may invalidate our cached oversized value.
162166
if (oversized.has_value() && !*oversized) oversized = std::nullopt;
163167
}
@@ -168,6 +172,7 @@ struct SimTxGraph
168172
auto pos = Find(ref);
169173
if (pos == MISSING) return;
170174
// No need to invoke MakeModified, because this equally affects main and staging.
175+
real_is_optimal = false;
171176
graph.FeeRate(pos).fee = fee;
172177
}
173178

@@ -177,6 +182,7 @@ struct SimTxGraph
177182
auto pos = Find(ref);
178183
if (pos == MISSING) return;
179184
MakeModified(pos);
185+
real_is_optimal = false;
180186
graph.RemoveTransactions(SetType::Singleton(pos));
181187
simrevmap.erase(simmap[pos].get());
182188
// Retain the TxGraph::Ref corresponding to this position, so the Ref destruction isn't
@@ -203,6 +209,7 @@ struct SimTxGraph
203209
} else {
204210
MakeModified(pos);
205211
graph.RemoveTransactions(SetType::Singleton(pos));
212+
real_is_optimal = false;
206213
simrevmap.erase(simmap[pos].get());
207214
simmap[pos].reset();
208215
// This may invalidate our cached oversized value.
@@ -467,6 +474,7 @@ FUZZ_TARGET(txgraph)
467474
if (top_sim.graph.Ancestors(pos_par)[pos_chl]) break;
468475
}
469476
top_sim.AddDependency(par, chl);
477+
top_sim.real_is_optimal = false;
470478
real->AddDependency(*par, *chl);
471479
break;
472480
} else if ((block_builders.empty() || sims.size() > 1) && top_sim.removed.size() < 100 && command-- == 0) {
@@ -721,7 +729,18 @@ FUZZ_TARGET(txgraph)
721729
break;
722730
} else if (command-- == 0) {
723731
// DoWork.
724-
real->DoWork();
732+
uint64_t iters = provider.ConsumeIntegralInRange<uint64_t>(0, alt ? 10000 : 255);
733+
if (real->DoWork(iters)) {
734+
for (unsigned level = 0; level < sims.size(); ++level) {
735+
// DoWork() will not optimize oversized levels.
736+
if (sims[level].IsOversized()) continue;
737+
// DoWork() will not touch the main level if a builder is present.
738+
if (level == 0 && !block_builders.empty()) continue;
739+
// If neither of the two above conditions holds, and DoWork() returned
740+
// then the level is optimal.
741+
sims[level].real_is_optimal = true;
742+
}
743+
}
725744
break;
726745
} else if (sims.size() == 2 && !sims[0].IsOversized() && !sims[1].IsOversized() && command-- == 0) {
727746
// GetMainStagingDiagrams()
@@ -1005,6 +1024,16 @@ FUZZ_TARGET(txgraph)
10051024
}
10061025
assert(todo.None());
10071026

1027+
// If the real graph claims to be optimal (the last DoWork() call returned true), verify
1028+
// that calling Linearize on it does not improve it further.
1029+
if (sims[0].real_is_optimal) {
1030+
auto real_diagram = ChunkLinearization(sims[0].graph, vec1);
1031+
auto [sim_lin, _optimal, _cost] = Linearize(sims[0].graph, 300000, rng.rand64(), vec1);
1032+
auto sim_diagram = ChunkLinearization(sims[0].graph, sim_lin);
1033+
auto cmp = CompareChunks(real_diagram, sim_diagram);
1034+
assert(cmp == 0);
1035+
}
1036+
10081037
// For every transaction in the total ordering, find a random one before it and after it,
10091038
// and compare their chunk feerates, which must be consistent with the ordering.
10101039
for (size_t pos = 0; pos < vec1.size(); ++pos) {

src/txgraph.cpp

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,9 @@ class Cluster
189189
void Merge(TxGraphImpl& graph, Cluster& cluster) noexcept;
190190
/** Given a span of (parent, child) pairs that all belong to this Cluster, apply them. */
191191
void ApplyDependencies(TxGraphImpl& graph, std::span<std::pair<GraphIndex, GraphIndex>> to_apply) noexcept;
192-
/** Improve the linearization of this Cluster. Returns how much work was performed. */
193-
uint64_t Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept;
192+
/** Improve the linearization of this Cluster. Returns how much work was performed and whether
193+
* the Cluster's QualityLevel improved as a result. */
194+
std::pair<uint64_t, bool> Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept;
194195
/** For every chunk in the cluster, append its FeeFrac to ret. */
195196
void AppendChunkFeerates(std::vector<FeeFrac>& ret) const noexcept;
196197
/** Add a TrimTxData entry (filling m_chunk_feerate, m_index, m_tx_size) for every
@@ -592,7 +593,7 @@ class TxGraphImpl final : public TxGraph
592593
void AddDependency(const Ref& parent, const Ref& child) noexcept final;
593594
void SetTransactionFee(const Ref&, int64_t fee) noexcept final;
594595

595-
void DoWork() noexcept final;
596+
bool DoWork(uint64_t iters) noexcept final;
596597

597598
void StartStaging() noexcept final;
598599
void CommitStaging() noexcept final;
@@ -1655,12 +1656,12 @@ void TxGraphImpl::ApplyDependencies(int level) noexcept
16551656
clusterset.m_group_data = GroupData{};
16561657
}
16571658

1658-
uint64_t Cluster::Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept
1659+
std::pair<uint64_t, bool> Cluster::Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept
16591660
{
16601661
// We can only relinearize Clusters that do not need splitting.
16611662
Assume(!NeedsSplitting());
16621663
// No work is required for Clusters which are already optimally linearized.
1663-
if (IsOptimal()) return 0;
1664+
if (IsOptimal()) return {0, false};
16641665
// Invoke the actual linearization algorithm (passing in the existing one).
16651666
uint64_t rng_seed = graph.m_rng.rand64();
16661667
auto [linearization, optimal, cost] = Linearize(m_depgraph, max_iters, rng_seed, m_linearization);
@@ -1670,11 +1671,17 @@ uint64_t Cluster::Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept
16701671
// Update the linearization.
16711672
m_linearization = std::move(linearization);
16721673
// Update the Cluster's quality.
1673-
auto new_quality = optimal ? QualityLevel::OPTIMAL : QualityLevel::ACCEPTABLE;
1674-
graph.SetClusterQuality(m_level, m_quality, m_setindex, new_quality);
1674+
bool improved = false;
1675+
if (optimal) {
1676+
graph.SetClusterQuality(m_level, m_quality, m_setindex, QualityLevel::OPTIMAL);
1677+
improved = true;
1678+
} else if (max_iters >= graph.m_acceptable_iters && !IsAcceptable()) {
1679+
graph.SetClusterQuality(m_level, m_quality, m_setindex, QualityLevel::ACCEPTABLE);
1680+
improved = true;
1681+
}
16751682
// Update the Entry objects.
16761683
Updated(graph);
1677-
return cost;
1684+
return {cost, improved};
16781685
}
16791686

16801687
void TxGraphImpl::MakeAcceptable(Cluster& cluster) noexcept
@@ -2478,13 +2485,50 @@ void TxGraphImpl::SanityCheck() const
24782485
assert(actual_chunkindex == expected_chunkindex);
24792486
}
24802487

2481-
void TxGraphImpl::DoWork() noexcept
2482-
{
2483-
for (int level = 0; level <= GetTopLevel(); ++level) {
2484-
if (level > 0 || m_main_chunkindex_observers == 0) {
2485-
MakeAllAcceptable(level);
2488+
bool TxGraphImpl::DoWork(uint64_t iters) noexcept
2489+
{
2490+
uint64_t iters_done{0};
2491+
// First linearize everything in NEEDS_RELINEARIZE to an acceptable level. If more budget
2492+
// remains after that, try to make everything optimal.
2493+
for (QualityLevel quality : {QualityLevel::NEEDS_RELINEARIZE, QualityLevel::ACCEPTABLE}) {
2494+
// First linearize staging, if it exists, then main.
2495+
for (int level = GetTopLevel(); level >= 0; --level) {
2496+
// Do not modify main if it has any observers.
2497+
if (level == 0 && m_main_chunkindex_observers != 0) continue;
2498+
ApplyDependencies(level);
2499+
auto& clusterset = GetClusterSet(level);
2500+
// Do not modify oversized levels.
2501+
if (clusterset.m_oversized == true) continue;
2502+
auto& queue = clusterset.m_clusters[int(quality)];
2503+
while (!queue.empty()) {
2504+
if (iters_done >= iters) return false;
2505+
// Randomize the order in which we process, so that if the first cluster somehow
2506+
// needs more work than what iters allows, we don't keep spending it on the same
2507+
// one.
2508+
auto pos = m_rng.randrange<size_t>(queue.size());
2509+
auto iters_now = iters - iters_done;
2510+
if (quality == QualityLevel::NEEDS_RELINEARIZE) {
2511+
// If we're working with clusters that need relinearization still, only perform
2512+
// up to m_acceptable_iters iterations. If they become ACCEPTABLE, and we still
2513+
// have budget after all other clusters are ACCEPTABLE too, we'll spend the
2514+
// remaining budget on trying to make them OPTIMAL.
2515+
iters_now = std::min(iters_now, m_acceptable_iters);
2516+
}
2517+
auto [cost, improved] = queue[pos].get()->Relinearize(*this, iters_now);
2518+
iters_done += cost;
2519+
// If no improvement was made to the Cluster, it means we've essentially run out of
2520+
// budget. Even though it may be the case that iters_done < iters still, the
2521+
// linearizer decided there wasn't enough budget left to attempt anything with.
2522+
// To avoid an infinite loop that keeps trying clusters with minuscule budgets,
2523+
// stop here too.
2524+
if (!improved) return false;
2525+
}
24862526
}
24872527
}
2528+
// All possible work has been performed, so we can return true. Note that this does *not* mean
2529+
// that all clusters are optimally linearized now. It may be that there is nothing to do left
2530+
// because all non-optimal clusters are in oversized and/or observer-bearing levels.
2531+
return true;
24882532
}
24892533

24902534
void BlockBuilderImpl::Next() noexcept

src/txgraph.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,10 @@ class TxGraph
9494
virtual void SetTransactionFee(const Ref& arg, int64_t fee) noexcept = 0;
9595

9696
/** TxGraph is internally lazy, and will not compute many things until they are needed.
97-
* Calling DoWork will compute everything now, so that future operations are fast. This can be
98-
* invoked while oversized. */
99-
virtual void DoWork() noexcept = 0;
97+
* Calling DoWork will perform some work now (controlled by iters) so that future operations
98+
* are fast, if there is any. Returns whether all currently-available work is done. This can
99+
* be invoked while oversized, but oversized graphs will be skipped by this call. */
100+
virtual bool DoWork(uint64_t iters) noexcept = 0;
100101

101102
/** Create a staging graph (which cannot exist already). This acts as if a full copy of
102103
* the transaction graph is made, upon which further modifications are made. This copy can

0 commit comments

Comments
 (0)