From d40302fbaf41bcb70939db534111f490d7eed5c1 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 15 Nov 2024 09:34:08 -0500 Subject: [PATCH 01/13] txgraph: Make level of Cluster implicit (optimization) This reduces per-Cluster memory usage by making Clusters not aware of their own level. Instead, track it either in calling code, or infer it based on the transactions in them. --- src/txgraph.cpp | 229 ++++++++++++++++++++++++++---------------------- 1 file changed, 122 insertions(+), 107 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index c8ff7537fe..41764ff89f 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -113,8 +113,6 @@ class Cluster QualityLevel m_quality{QualityLevel::NONE}; /** Which position this Cluster has in Graph::ClusterSet::m_clusters[m_quality]. */ ClusterSetIndex m_setindex{ClusterSetIndex(-1)}; - /** Which level this Cluster is at in the graph (-1=not inserted, 0=main, 1=staging). */ - int m_level{-1}; /** Sequence number for this Cluster (for tie-breaking comparison between equal-chunk-feerate transactions in distinct clusters). */ uint64_t m_sequence; @@ -161,10 +159,13 @@ public: uint64_t GetTotalTxSize() const noexcept; /** Given a DepGraphIndex into this Cluster, find the corresponding GraphIndex. */ GraphIndex GetClusterEntry(DepGraphIndex index) const noexcept { return m_mapping[index]; } + /** Figure out what level this Cluster exists at in the graph. In most cases this is known by + * the caller already (see all "int level" arguments below), but not always. */ + int GetLevel(const TxGraphImpl& graph) const noexcept; /** Only called by Graph::SwapIndexes. */ void UpdateMapping(DepGraphIndex cluster_idx, GraphIndex graph_idx) noexcept { m_mapping[cluster_idx] = graph_idx; } /** Push changes to Cluster and its linearization to the TxGraphImpl Entry objects. */ - void Updated(TxGraphImpl& graph) noexcept; + void Updated(TxGraphImpl& graph, int level) noexcept; /** Create a copy of this Cluster in staging, returning a pointer to it (used by PullIn). */ Cluster* CopyToStaging(TxGraphImpl& graph) const noexcept; /** Get the list of Clusters in main that conflict with this one (which is assumed to be in staging). */ @@ -173,7 +174,7 @@ public: * deleted immediately after. */ void MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept; /** Remove all transactions from a Cluster. */ - void Clear(TxGraphImpl& graph) noexcept; + void Clear(TxGraphImpl& graph, int level) noexcept; /** Change a Cluster's level from 1 (staging) to 0 (main). */ void MoveToMain(TxGraphImpl& graph) noexcept; @@ -181,17 +182,17 @@ public: /** Apply all removals from the front of to_remove that apply to this Cluster, popping them * off. These must be at least one such entry. */ - void ApplyRemovals(TxGraphImpl& graph, std::span& to_remove) noexcept; + void ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept; /** Split this cluster (must have a NEEDS_SPLIT* quality). Returns whether to delete this * Cluster afterwards. */ - [[nodiscard]] bool Split(TxGraphImpl& graph) noexcept; + [[nodiscard]] bool Split(TxGraphImpl& graph, int level) noexcept; /** Move all transactions from cluster to *this (as separate components). */ - void Merge(TxGraphImpl& graph, Cluster& cluster) noexcept; + void Merge(TxGraphImpl& graph, int level, Cluster& cluster) noexcept; /** Given a span of (parent, child) pairs that all belong to this Cluster, apply them. */ - void ApplyDependencies(TxGraphImpl& graph, std::span> to_apply) noexcept; + void ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) 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 Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept; + std::pair Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept; /** For every chunk in the cluster, append its FeeFrac to ret. */ void AppendChunkFeerates(std::vector& ret) const noexcept; /** Add a TrimTxData entry (filling m_chunk_feerate, m_index, m_tx_size) for every @@ -214,7 +215,7 @@ public: /** Get the individual transaction feerate of a Cluster element. */ FeePerWeight GetIndividualFeerate(DepGraphIndex idx) noexcept; /** Modify the fee of a Cluster element. */ - void SetFee(TxGraphImpl& graph, DepGraphIndex idx, int64_t fee) noexcept; + void SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept; // Debugging functions. @@ -485,11 +486,13 @@ public: void SwapIndexes(GraphIndex a, GraphIndex b) noexcept; /** If idx exists in the specified level ClusterSet (explicitly, or in the level below and not * removed), return the Cluster it is in. Otherwise, return nullptr. */ - Cluster* FindCluster(GraphIndex idx, int level) const noexcept; + Cluster* FindCluster(GraphIndex idx, int level) const noexcept { return FindClusterAndLevel(idx, level).first; } + /** Like FindCluster, but also return what level the match was found in (-1 if not found). */ + std::pair FindClusterAndLevel(GraphIndex idx, int level) const noexcept; /** Extract a Cluster from its ClusterSet. */ std::unique_ptr ExtractCluster(int level, QualityLevel quality, ClusterSetIndex setindex) noexcept; /** Delete a Cluster. */ - void DeleteCluster(Cluster& cluster) noexcept; + void DeleteCluster(Cluster& cluster, int level) noexcept; /** Insert a Cluster into its ClusterSet. */ ClusterSetIndex InsertCluster(int level, std::unique_ptr&& cluster, QualityLevel quality) noexcept; /** Change the QualityLevel of a Cluster (identified by old_quality and old_index). */ @@ -567,22 +570,22 @@ public: /** If cluster is not in staging, copy it there, and return a pointer to it. * Staging must exist, and this modifies the locators of its * transactions from inherited (P,M) to explicit (P,P). */ - Cluster* PullIn(Cluster* cluster) noexcept; + Cluster* PullIn(Cluster* cluster, int level) noexcept; /** Apply all removals queued up in m_to_remove to the relevant Clusters (which get a * NEEDS_SPLIT* QualityLevel) up to the specified level. */ void ApplyRemovals(int up_to_level) noexcept; /** Split an individual cluster. */ - void Split(Cluster& cluster) noexcept; + void Split(Cluster& cluster, int level) noexcept; /** Split all clusters that need splitting up to the specified level. */ void SplitAll(int up_to_level) noexcept; /** Populate m_group_data based on m_deps_to_add in the specified level. */ void GroupClusters(int level) noexcept; /** Merge the specified clusters. */ - void Merge(std::span to_merge) noexcept; + void Merge(std::span to_merge, int level) noexcept; /** Apply all m_deps_to_add to the relevant Clusters in the specified level. */ void ApplyDependencies(int level) noexcept; /** Make a specified Cluster have quality ACCEPTABLE or OPTIMAL. */ - void MakeAcceptable(Cluster& cluster) noexcept; + void MakeAcceptable(Cluster& cluster, int level) noexcept; /** Make all Clusters at the specified level have quality ACCEPTABLE or OPTIMAL. */ void MakeAllAcceptable(int level) noexcept; @@ -707,6 +710,23 @@ uint64_t Cluster::GetTotalTxSize() const noexcept return ret; } +int Cluster::GetLevel(const TxGraphImpl& graph) const noexcept +{ + // GetLevel() does not work for empty Clusters. + if (!Assume(!m_linearization.empty())) return -1; + + // Pick an arbitrary Entry that occurs in this Cluster. + const auto& entry = graph.m_entries[m_mapping[m_linearization.front()]]; + // See if there is a level whose Locator matches this Cluster, if so return that level. + for (int level = 0; level < MAX_LEVELS; ++level) { + if (entry.m_locator[level].cluster == this) return level; + } + // Given that we started with an Entry that occurs in this Cluster, one of its Locators must + // point back to it. + assert(false); + return -1; +} + void TxGraphImpl::ClearLocator(int level, GraphIndex idx, bool oversized_tx) noexcept { auto& entry = m_entries[idx]; @@ -734,22 +754,22 @@ void TxGraphImpl::ClearLocator(int level, GraphIndex idx, bool oversized_tx) noe if (level == 0) ClearChunkData(entry); } -void Cluster::Updated(TxGraphImpl& graph) noexcept +void Cluster::Updated(TxGraphImpl& graph, int level) noexcept { // Update all the Locators for this Cluster's Entry objects. for (DepGraphIndex idx : m_linearization) { auto& entry = graph.m_entries[m_mapping[idx]]; // Discard any potential ChunkData prior to modifying the Cluster (as that could // invalidate its ordering). - if (m_level == 0) graph.ClearChunkData(entry); - entry.m_locator[m_level].SetPresent(this, idx); + if (level == 0) graph.ClearChunkData(entry); + entry.m_locator[level].SetPresent(this, idx); } // If this is for the main graph (level = 0), and the Cluster's quality is ACCEPTABLE or // OPTIMAL, compute its chunking and store its information in the Entry's m_main_lin_index // and m_main_chunk_feerate. These fields are only accessed after making the entire graph // ACCEPTABLE, so it is pointless to compute these if we haven't reached that quality level // yet. - if (m_level == 0 && IsAcceptable()) { + if (level == 0 && IsAcceptable()) { const LinearizationChunking chunking(m_depgraph, m_linearization); LinearizationIndex lin_idx{0}; // Iterate over the chunks. @@ -784,7 +804,6 @@ void Cluster::Updated(TxGraphImpl& graph) noexcept void Cluster::GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept { - Assume(m_level == 1); for (auto i : m_linearization) { auto& entry = graph.m_entries[m_mapping[i]]; // For every transaction Entry in this Cluster, if it also exists in a lower-level Cluster, @@ -832,11 +851,11 @@ Cluster* Cluster::CopyToStaging(TxGraphImpl& graph) const noexcept // Insert the new Cluster into the graph. graph.InsertCluster(1, std::move(ret), m_quality); // Update its Locators. - ptr->Updated(graph); + ptr->Updated(graph, /*level=*/1); return ptr; } -void Cluster::ApplyRemovals(TxGraphImpl& graph, std::span& to_remove) noexcept +void Cluster::ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept { // Iterate over the prefix of to_remove that applies to this cluster. Assume(!to_remove.empty()); @@ -845,7 +864,7 @@ void Cluster::ApplyRemovals(TxGraphImpl& graph, std::span& to_remove GraphIndex idx = to_remove.front(); Assume(idx < graph.m_entries.size()); auto& entry = graph.m_entries[idx]; - auto& locator = entry.m_locator[m_level]; + auto& locator = entry.m_locator[level]; // Stop once we hit an entry that applies to another Cluster. if (locator.cluster != this) break; // - Remember it in a set of to-remove DepGraphIndexes. @@ -855,11 +874,11 @@ void Cluster::ApplyRemovals(TxGraphImpl& graph, std::span& to_remove // that causes it to be accessed regardless. m_mapping[locator.index] = GraphIndex(-1); // - Remove its linearization index from the Entry (if in main). - if (m_level == 0) { + if (level == 0) { entry.m_main_lin_index = LinearizationIndex(-1); } // - Mark it as missing/removed in the Entry's locator. - graph.ClearLocator(m_level, idx, m_quality == QualityLevel::OVERSIZED_SINGLETON); + graph.ClearLocator(level, idx, m_quality == QualityLevel::OVERSIZED_SINGLETON); to_remove = to_remove.subspan(1); } while(!to_remove.empty()); @@ -891,14 +910,14 @@ void Cluster::ApplyRemovals(TxGraphImpl& graph, std::span& to_remove [&](auto pos) { return todo[pos]; }), m_linearization.end()); quality = QualityLevel::NEEDS_SPLIT; } - graph.SetClusterQuality(m_level, m_quality, m_setindex, quality); - Updated(graph); + graph.SetClusterQuality(level, m_quality, m_setindex, quality); + Updated(graph, level); } -void Cluster::Clear(TxGraphImpl& graph) noexcept +void Cluster::Clear(TxGraphImpl& graph, int level) noexcept { for (auto i : m_linearization) { - graph.ClearLocator(m_level, m_mapping[i], m_quality == QualityLevel::OVERSIZED_SINGLETON); + graph.ClearLocator(level, m_mapping[i], m_quality == QualityLevel::OVERSIZED_SINGLETON); } m_depgraph = {}; m_linearization.clear(); @@ -907,7 +926,6 @@ void Cluster::Clear(TxGraphImpl& graph) noexcept void Cluster::MoveToMain(TxGraphImpl& graph) noexcept { - Assume(m_level == 1); for (auto i : m_linearization) { GraphIndex idx = m_mapping[i]; auto& entry = graph.m_entries[idx]; @@ -915,8 +933,8 @@ void Cluster::MoveToMain(TxGraphImpl& graph) noexcept } auto quality = m_quality; auto cluster = graph.ExtractCluster(1, quality, m_setindex); - graph.InsertCluster(0, std::move(cluster), quality); - Updated(graph); + graph.InsertCluster(/*level=*/0, std::move(cluster), quality); + Updated(graph, /*level=*/0); } void Cluster::AppendChunkFeerates(std::vector& ret) const noexcept @@ -957,7 +975,7 @@ uint64_t Cluster::AppendTrimData(std::vector& ret, std::vectorUpdated(graph); + new_cluster->Updated(graph, level); } // Wipe this Cluster, and return that it needs to be deleted. m_depgraph = DepGraph{}; @@ -1037,7 +1055,7 @@ bool Cluster::Split(TxGraphImpl& graph) noexcept return true; } -void Cluster::Merge(TxGraphImpl& graph, Cluster& other) noexcept +void Cluster::Merge(TxGraphImpl& graph, int level, Cluster& other) noexcept { /** Vector to store the positions in this Cluster for each position in other. */ std::vector remap(other.m_depgraph.PositionRange()); @@ -1067,8 +1085,8 @@ void Cluster::Merge(TxGraphImpl& graph, Cluster& other) noexcept auto& entry = graph.m_entries[idx]; // Discard any potential ChunkData prior to modifying the Cluster (as that could // invalidate its ordering). - if (m_level == 0) graph.ClearChunkData(entry); - entry.m_locator[m_level].SetPresent(this, new_pos); + if (level == 0) graph.ClearChunkData(entry); + entry.m_locator[level].SetPresent(this, new_pos); } // Purge the other Cluster, now that everything has been moved. other.m_depgraph = DepGraph{}; @@ -1076,7 +1094,7 @@ void Cluster::Merge(TxGraphImpl& graph, Cluster& other) noexcept other.m_mapping.clear(); } -void Cluster::ApplyDependencies(TxGraphImpl& graph, std::span> to_apply) noexcept +void Cluster::ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) noexcept { // This function is invoked by TxGraphImpl::ApplyDependencies after merging groups of Clusters // between which dependencies are added, which simply concatenates their linearizations. Invoke @@ -1091,14 +1109,14 @@ void Cluster::ApplyDependencies(TxGraphImpl& graph, std::spansecond].m_locator[m_level]; + auto& first_child = graph.m_entries[it->second].m_locator[level]; const auto child_idx = first_child.index; // Iterate over all to-be-added dependencies within that same child, gather the relevant // parents. SetType parents; while (it != to_apply.end()) { - auto& child = graph.m_entries[it->second].m_locator[m_level]; - auto& parent = graph.m_entries[it->first].m_locator[m_level]; + auto& child = graph.m_entries[it->second].m_locator[level]; + auto& parent = graph.m_entries[it->first].m_locator[level]; Assume(child.cluster == this && parent.cluster == this); if (child.index != child_idx) break; parents.Set(parent.index); @@ -1117,11 +1135,11 @@ void Cluster::ApplyDependencies(TxGraphImpl& graph, std::span TxGraphImpl::ExtractCluster(int level, QualityLevel qua std::unique_ptr ret = std::move(quality_clusters[setindex]); ret->m_quality = QualityLevel::NONE; ret->m_setindex = ClusterSetIndex(-1); - ret->m_level = -1; // Clean up space in quality_cluster. auto max_setindex = quality_clusters.size() - 1; if (setindex != max_setindex) { // If the cluster was not the last element of quality_clusters, move that to take its place. quality_clusters.back()->m_setindex = setindex; - quality_clusters.back()->m_level = level; quality_clusters[setindex] = std::move(quality_clusters.back()); } // The last element of quality_clusters is now unused; drop it. @@ -1176,7 +1192,6 @@ ClusterSetIndex TxGraphImpl::InsertCluster(int level, std::unique_ptr&& ClusterSetIndex ret = quality_clusters.size(); cluster->m_quality = quality; cluster->m_setindex = ret; - cluster->m_level = level; quality_clusters.push_back(std::move(cluster)); return ret; } @@ -1193,15 +1208,15 @@ void TxGraphImpl::SetClusterQuality(int level, QualityLevel old_quality, Cluster InsertCluster(level, std::move(cluster_ptr), new_quality); } -void TxGraphImpl::DeleteCluster(Cluster& cluster) noexcept +void TxGraphImpl::DeleteCluster(Cluster& cluster, int level) noexcept { // Extract the cluster from where it currently resides. - auto cluster_ptr = ExtractCluster(cluster.m_level, cluster.m_quality, cluster.m_setindex); + auto cluster_ptr = ExtractCluster(level, cluster.m_quality, cluster.m_setindex); // And throw it away. cluster_ptr.reset(); } -Cluster* TxGraphImpl::FindCluster(GraphIndex idx, int level) const noexcept +std::pair TxGraphImpl::FindClusterAndLevel(GraphIndex idx, int level) const noexcept { Assume(level >= 0 && level <= GetTopLevel()); auto& entry = m_entries[idx]; @@ -1213,23 +1228,22 @@ Cluster* TxGraphImpl::FindCluster(GraphIndex idx, int level) const noexcept // If the locator has the entry marked as explicitly removed, stop. if (entry.m_locator[l].IsRemoved()) break; // Otherwise, we have found the topmost ClusterSet that contains this entry. - return entry.m_locator[l].cluster; + return {entry.m_locator[l].cluster, l}; } // If no non-empty locator was found, or an explicitly removed was hit, return nothing. - return nullptr; + return {nullptr, -1}; } -Cluster* TxGraphImpl::PullIn(Cluster* cluster) noexcept +Cluster* TxGraphImpl::PullIn(Cluster* cluster, int level) noexcept { int to_level = GetTopLevel(); Assume(to_level == 1); - int level = cluster->m_level; Assume(level <= to_level); // Copy the Cluster from main to staging, if it's not already there. if (level == 0) { // Make the Cluster Acceptable before copying. This isn't strictly necessary, but doing it // now avoids doing double work later. - MakeAcceptable(*cluster); + MakeAcceptable(*cluster, level); cluster = cluster->CopyToStaging(*this); } return cluster; @@ -1246,8 +1260,8 @@ void TxGraphImpl::ApplyRemovals(int up_to_level) noexcept // Pull in all Clusters that are not in staging. if (level == 1) { for (GraphIndex index : to_remove) { - auto cluster = FindCluster(index, level); - if (cluster != nullptr) PullIn(cluster); + auto [cluster, cluster_level] = FindClusterAndLevel(index, level); + if (cluster != nullptr) PullIn(cluster, cluster_level); } } // Group the set of to-be-removed entries by Cluster::m_sequence. @@ -1263,7 +1277,7 @@ void TxGraphImpl::ApplyRemovals(int up_to_level) noexcept if (cluster != nullptr) { // If the first to_remove_span entry's Cluster exists, hand to_remove_span to it, so it // can pop off whatever applies to it. - cluster->ApplyRemovals(*this, to_remove_span); + cluster->ApplyRemovals(*this, level, to_remove_span); } else { // Otherwise, skip this already-removed entry. This may happen when // RemoveTransaction was called twice on the same Ref, for example. @@ -1347,15 +1361,15 @@ void TxGraphImpl::Compact() noexcept m_unlinked.clear(); } -void TxGraphImpl::Split(Cluster& cluster) noexcept +void TxGraphImpl::Split(Cluster& cluster, int level) noexcept { // To split a Cluster, first make sure all removals are applied (as we might need to split // again afterwards otherwise). - ApplyRemovals(cluster.m_level); - bool del = cluster.Split(*this); + ApplyRemovals(level); + bool del = cluster.Split(*this, level); if (del) { // Cluster::Split reports whether the Cluster is to be deleted. - DeleteCluster(cluster); + DeleteCluster(cluster, level); } } @@ -1368,7 +1382,7 @@ void TxGraphImpl::SplitAll(int up_to_level) noexcept for (auto quality : {QualityLevel::NEEDS_SPLIT, QualityLevel::NEEDS_SPLIT_ACCEPTABLE}) { auto& queue = GetClusterSet(level).m_clusters[int(quality)]; while (!queue.empty()) { - Split(*queue.back().get()); + Split(*queue.back().get(), level); } } } @@ -1586,7 +1600,7 @@ void TxGraphImpl::GroupClusters(int level) noexcept Compact(); } -void TxGraphImpl::Merge(std::span to_merge) noexcept +void TxGraphImpl::Merge(std::span to_merge, int level) noexcept { Assume(!to_merge.empty()); // Nothing to do if a group consists of just a single Cluster. @@ -1608,8 +1622,8 @@ void TxGraphImpl::Merge(std::span to_merge) noexcept // Merge all further Clusters in the group into the first one, and delete them. for (size_t i = 1; i < to_merge.size(); ++i) { - to_merge[0]->Merge(*this, *to_merge[i]); - DeleteCluster(*to_merge[i]); + to_merge[0]->Merge(*this, level, *to_merge[i]); + DeleteCluster(*to_merge[i], level); } } @@ -1633,11 +1647,11 @@ void TxGraphImpl::ApplyDependencies(int level) noexcept // Pull in all the Clusters that contain dependencies. if (level == 1) { for (Cluster*& cluster : cluster_span) { - cluster = PullIn(cluster); + cluster = PullIn(cluster, cluster->GetLevel(*this)); } } // Invoke Merge() to merge them into a single Cluster. - Merge(cluster_span); + Merge(cluster_span, level); // Actually apply all to-be-added dependencies (all parents and children from this grouping // belong to the same Cluster at this point because of the merging above). auto deps_span = std::span{clusterset.m_deps_to_add} @@ -1645,7 +1659,7 @@ void TxGraphImpl::ApplyDependencies(int level) noexcept Assume(!deps_span.empty()); const auto& loc = m_entries[deps_span[0].second].m_locator[level]; Assume(loc.IsPresent()); - loc.cluster->ApplyDependencies(*this, deps_span); + loc.cluster->ApplyDependencies(*this, level, deps_span); } // Wipe the list of to-be-added dependencies now that they are applied. @@ -1656,7 +1670,7 @@ void TxGraphImpl::ApplyDependencies(int level) noexcept clusterset.m_group_data = GroupData{}; } -std::pair Cluster::Relinearize(TxGraphImpl& graph, uint64_t max_iters) noexcept +std::pair Cluster::Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept { // We can only relinearize Clusters that do not need splitting. Assume(!NeedsSplitting()); @@ -1673,22 +1687,22 @@ std::pair Cluster::Relinearize(TxGraphImpl& graph, uint64_t max_ // Update the Cluster's quality. bool improved = false; if (optimal) { - graph.SetClusterQuality(m_level, m_quality, m_setindex, QualityLevel::OPTIMAL); + graph.SetClusterQuality(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); + graph.SetClusterQuality(level, m_quality, m_setindex, QualityLevel::ACCEPTABLE); improved = true; } // Update the Entry objects. - Updated(graph); + Updated(graph, level); return {cost, improved}; } -void TxGraphImpl::MakeAcceptable(Cluster& cluster) noexcept +void TxGraphImpl::MakeAcceptable(Cluster& cluster, int level) noexcept { // Relinearize the Cluster if needed. if (!cluster.NeedsSplitting() && !cluster.IsAcceptable() && !cluster.IsOversized()) { - cluster.Relinearize(*this, m_acceptable_iters); + cluster.Relinearize(*this, level, m_acceptable_iters); } } @@ -1699,7 +1713,7 @@ void TxGraphImpl::MakeAllAcceptable(int level) noexcept if (clusterset.m_oversized == true) return; auto& queue = clusterset.m_clusters[int(QualityLevel::NEEDS_RELINEARIZE)]; while (!queue.empty()) { - MakeAcceptable(*queue.back().get()); + MakeAcceptable(*queue.back().get(), level); } } @@ -1735,7 +1749,7 @@ TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept int level = GetTopLevel(); auto& clusterset = GetClusterSet(level); InsertCluster(level, std::move(cluster), oversized ? QualityLevel::OVERSIZED_SINGLETON : QualityLevel::OPTIMAL); - cluster_ptr->Updated(*this); + cluster_ptr->Updated(*this, level); ++clusterset.m_txcount; // Deal with individually oversized transactions. if (oversized) { @@ -1860,7 +1874,6 @@ FeePerWeight Cluster::GetIndividualFeerate(DepGraphIndex idx) noexcept void Cluster::MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept { - Assume(m_level == 1); // Mark all transactions of a Cluster missing, needed when aborting staging, so that the // corresponding Locators don't retain references into aborted Clusters. for (auto ci : m_linearization) { @@ -1881,10 +1894,10 @@ std::vector TxGraphImpl::GetAncestors(const Ref& arg, Level level // Ancestry cannot be known if unapplied dependencies remain. Assume(GetClusterSet(level).m_deps_to_add.empty()); // Find the Cluster the argument is in, and return the empty vector if it isn't in any. - auto cluster = FindCluster(GetRefIndex(arg), level); + auto [cluster, cluster_level] = FindClusterAndLevel(GetRefIndex(arg), level); if (cluster == nullptr) return {}; // Dispatch to the Cluster. - std::pair match = {cluster, m_entries[GetRefIndex(arg)].m_locator[cluster->m_level].index}; + std::pair match = {cluster, m_entries[GetRefIndex(arg)].m_locator[cluster_level].index}; auto matches = std::span(&match, 1); std::vector ret; cluster->GetAncestorRefs(*this, matches, ret); @@ -1902,10 +1915,10 @@ std::vector TxGraphImpl::GetDescendants(const Ref& arg, Level lev // Ancestry cannot be known if unapplied dependencies remain. Assume(GetClusterSet(level).m_deps_to_add.empty()); // Find the Cluster the argument is in, and return the empty vector if it isn't in any. - auto cluster = FindCluster(GetRefIndex(arg), level); + auto [cluster, cluster_level] = FindClusterAndLevel(GetRefIndex(arg), level); if (cluster == nullptr) return {}; // Dispatch to the Cluster. - std::pair match = {cluster, m_entries[GetRefIndex(arg)].m_locator[cluster->m_level].index}; + std::pair match = {cluster, m_entries[GetRefIndex(arg)].m_locator[cluster_level].index}; auto matches = std::span(&match, 1); std::vector ret; cluster->GetDescendantRefs(*this, matches, ret); @@ -1929,10 +1942,10 @@ std::vector TxGraphImpl::GetAncestorsUnion(std::spanm_level].index); + matches.emplace_back(cluster, m_entries[GetRefIndex(*arg)].m_locator[cluster_level].index); } // Group by Cluster. std::sort(matches.begin(), matches.end(), [](auto& a, auto& b) noexcept { return CompareClusters(a.first, b.first) < 0; }); @@ -1962,10 +1975,10 @@ std::vector TxGraphImpl::GetDescendantsUnion(std::spanm_level].index); + matches.emplace_back(cluster, m_entries[GetRefIndex(*arg)].m_locator[cluster_level].index); } // Group by Cluster. std::sort(matches.begin(), matches.end(), [](auto& a, auto& b) noexcept { return CompareClusters(a.first, b.first) < 0; }); @@ -1990,10 +2003,10 @@ std::vector TxGraphImpl::GetCluster(const Ref& arg, Level level_s // Cluster linearization cannot be known if unapplied dependencies remain. Assume(GetClusterSet(level).m_deps_to_add.empty()); // Find the Cluster the argument is in, and return the empty vector if it isn't in any. - auto cluster = FindCluster(GetRefIndex(arg), level); + auto [cluster, cluster_level] = FindClusterAndLevel(GetRefIndex(arg), level); if (cluster == nullptr) return {}; // Make sure the Cluster has an acceptable quality level, and then dispatch to it. - MakeAcceptable(*cluster); + MakeAcceptable(*cluster, cluster_level); std::vector ret(cluster->GetTxCount()); cluster->GetClusterRefs(*this, ret, 0); return ret; @@ -2014,7 +2027,8 @@ FeePerWeight TxGraphImpl::GetIndividualFeerate(const Ref& arg) noexcept // Find the cluster the argument is in (the level does not matter as individual feerates will // be identical if it occurs in both), and return the empty FeePerWeight if it isn't in any. Cluster* cluster{nullptr}; - for (int level = 0; level <= GetTopLevel(); ++level) { + int level; + for (level = 0; level <= GetTopLevel(); ++level) { // Apply removals, so that we can correctly report FeePerWeight{} for non-existing // transactions. ApplyRemovals(level); @@ -2025,7 +2039,7 @@ FeePerWeight TxGraphImpl::GetIndividualFeerate(const Ref& arg) noexcept } if (cluster == nullptr) return {}; // Dispatch to the Cluster. - return cluster->GetIndividualFeerate(m_entries[GetRefIndex(arg)].m_locator[cluster->m_level].index); + return cluster->GetIndividualFeerate(m_entries[GetRefIndex(arg)].m_locator[level].index); } FeePerWeight TxGraphImpl::GetMainChunkFeerate(const Ref& arg) noexcept @@ -2038,11 +2052,11 @@ FeePerWeight TxGraphImpl::GetMainChunkFeerate(const Ref& arg) noexcept // Chunk feerates cannot be accurately known if unapplied dependencies remain. Assume(m_main_clusterset.m_deps_to_add.empty()); // Find the cluster the argument is in, and return the empty FeePerWeight if it isn't in any. - auto cluster = FindCluster(GetRefIndex(arg), 0); + auto [cluster, cluster_level] = FindClusterAndLevel(GetRefIndex(arg), /*level=*/0); if (cluster == nullptr) return {}; // Make sure the Cluster has an acceptable quality level, and then return the transaction's // chunk feerate. - MakeAcceptable(*cluster); + MakeAcceptable(*cluster, cluster_level); const auto& entry = m_entries[GetRefIndex(arg)]; return entry.m_main_chunk_feerate; } @@ -2130,8 +2144,8 @@ void TxGraphImpl::CommitStaging() noexcept // there. All of these have been copied to staging in PullIn(). auto conflicts = GetConflicts(); for (Cluster* conflict : conflicts) { - conflict->Clear(*this); - DeleteCluster(*conflict); + conflict->Clear(*this, /*level=*/0); + DeleteCluster(*conflict, /*level=*/0); } // Mark the removed transactions as Missing (so the staging locator for these transactions // can be reused if another staging is created). @@ -2157,7 +2171,7 @@ void TxGraphImpl::CommitStaging() noexcept Compact(); } -void Cluster::SetFee(TxGraphImpl& graph, DepGraphIndex idx, int64_t fee) noexcept +void Cluster::SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept { // Make sure the specified DepGraphIndex exists in this Cluster. Assume(m_depgraph.Positions()[idx]); @@ -2169,11 +2183,11 @@ void Cluster::SetFee(TxGraphImpl& graph, DepGraphIndex idx, int64_t fee) noexcep if (m_quality == QualityLevel::OVERSIZED_SINGLETON) { // Nothing to do. } else if (!NeedsSplitting()) { - graph.SetClusterQuality(m_level, m_quality, m_setindex, QualityLevel::NEEDS_RELINEARIZE); + graph.SetClusterQuality(level, m_quality, m_setindex, QualityLevel::NEEDS_RELINEARIZE); } else { - graph.SetClusterQuality(m_level, m_quality, m_setindex, QualityLevel::NEEDS_SPLIT); + graph.SetClusterQuality(level, m_quality, m_setindex, QualityLevel::NEEDS_SPLIT); } - Updated(graph); + Updated(graph, level); } void TxGraphImpl::SetTransactionFee(const Ref& ref, int64_t fee) noexcept @@ -2187,7 +2201,7 @@ void TxGraphImpl::SetTransactionFee(const Ref& ref, int64_t fee) noexcept for (int level = 0; level < MAX_LEVELS; ++level) { auto& locator = entry.m_locator[level]; if (locator.IsPresent()) { - locator.cluster->SetFee(*this, locator.index, fee); + locator.cluster->SetFee(*this, level, locator.index, fee); } } } @@ -2207,8 +2221,8 @@ std::strong_ordering TxGraphImpl::CompareMainOrder(const Ref& a, const Ref& b) n const auto& locator_b = entry_b.m_locator[0]; Assume(locator_a.IsPresent()); Assume(locator_b.IsPresent()); - MakeAcceptable(*locator_a.cluster); - MakeAcceptable(*locator_b.cluster); + MakeAcceptable(*locator_a.cluster, /*level=*/0); + MakeAcceptable(*locator_b.cluster, /*level=*/0); // Invoke comparison logic. return CompareMainTransactions(GetRefIndex(a), GetRefIndex(b)); } @@ -2274,8 +2288,9 @@ void Cluster::SanityCheck(const TxGraphImpl& graph, int level) const assert(m_depgraph.TxCount() == m_linearization.size()); // The number of transactions in a Cluster cannot exceed m_max_cluster_count. assert(m_linearization.size() <= graph.m_max_cluster_count); - // The level must match the level the Cluster occurs in. - assert(m_level == level); + // The level must match the Cluster's own idea of its level (but GetLevel can only be called + // for non-empty Clusters). + assert(GetTxCount() == 0 || level == GetLevel(graph)); // The sum of their sizes cannot exceed m_max_cluster_size, unless it is an individually // oversized transaction singleton. Note that groups of to-be-merged clusters which would // exceed this limit are marked oversized, which means they are never applied. @@ -2514,7 +2529,7 @@ bool TxGraphImpl::DoWork(uint64_t iters) noexcept // 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); + auto [cost, improved] = queue[pos].get()->Relinearize(*this, level, 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 From 2b1d30250877ba2576cfc6d56fe5aa0d9e77f706 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 17 Sep 2025 10:42:31 -0400 Subject: [PATCH 02/13] txgraph: move some sanity checks from Cluster to TxGraphImpl (refactor) --- src/txgraph.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index 41764ff89f..4dee1d027d 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -2286,19 +2286,6 @@ void Cluster::SanityCheck(const TxGraphImpl& graph, int level) const assert(m_depgraph.PositionRange() == m_mapping.size()); // The linearization for this Cluster must contain every transaction once. assert(m_depgraph.TxCount() == m_linearization.size()); - // The number of transactions in a Cluster cannot exceed m_max_cluster_count. - assert(m_linearization.size() <= graph.m_max_cluster_count); - // The level must match the Cluster's own idea of its level (but GetLevel can only be called - // for non-empty Clusters). - assert(GetTxCount() == 0 || level == GetLevel(graph)); - // The sum of their sizes cannot exceed m_max_cluster_size, unless it is an individually - // oversized transaction singleton. Note that groups of to-be-merged clusters which would - // exceed this limit are marked oversized, which means they are never applied. - assert(m_quality == QualityLevel::OVERSIZED_SINGLETON || GetTotalTxSize() <= graph.m_max_cluster_size); - // m_quality and m_setindex are checked in TxGraphImpl::SanityCheck. - - // OVERSIZED clusters are singletons. - assert(m_quality != QualityLevel::OVERSIZED_SINGLETON || m_linearization.size() == 1); // Compute the chunking of m_linearization. LinearizationChunking linchunking(m_depgraph, m_linearization); @@ -2416,6 +2403,19 @@ void TxGraphImpl::SanityCheck() const // ... for all clusters in them ... for (ClusterSetIndex setindex = 0; setindex < quality_clusters.size(); ++setindex) { const auto& cluster = *quality_clusters[setindex]; + // The number of transactions in a Cluster cannot exceed m_max_cluster_count. + assert(cluster.GetTxCount() <= m_max_cluster_count); + // The level must match the Cluster's own idea of what level it is in (but GetLevel + // can only be called for non-empty Clusters). + assert(cluster.GetTxCount() == 0 || level == cluster.GetLevel(*this)); + // The sum of their sizes cannot exceed m_max_cluster_size, unless it is an + // individually oversized transaction singleton. Note that groups of to-be-merged + // clusters which would exceed this limit are marked oversized, which means they + // are never applied. + assert(cluster.IsOversized() || cluster.GetTotalTxSize() <= m_max_cluster_size); + // OVERSIZED clusters are singletons. + assert(!cluster.IsOversized() || cluster.GetTxCount() == 1); + // Check the sequence number. assert(cluster.m_sequence < m_next_sequence_counter); assert(sequences.count(cluster.m_sequence) == 0); From b1637a90deb8528a6892dbc1065876583b5075d6 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 4 Aug 2025 17:18:09 -0400 Subject: [PATCH 03/13] txgraph: avoid holes in DepGraph positions (mem optimization) --- src/txgraph.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index 4dee1d027d..4c4772ef1b 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -1001,9 +1001,12 @@ bool Cluster::Split(TxGraphImpl& graph, int level) 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. + auto component_size = component.Count(); + auto split_quality = component_size <= 2 ? QualityLevel::OPTIMAL : new_quality; + if (first && component == todo && SetType::Fill(component_size) == component) { + // The existing Cluster is an entire component, without holes. Leave it be, but update + // its quality. If there are holes, we continue, so that the Cluster is reconstructed + // without holes, reducing memory usage. Assume(todo == m_depgraph.Positions()); graph.SetClusterQuality(level, m_quality, m_setindex, split_quality); // If this made the quality ACCEPTABLE or OPTIMAL, we need to compute and cache its @@ -1064,12 +1067,11 @@ void Cluster::Merge(TxGraphImpl& graph, int level, Cluster& other) noexcept auto idx = other.m_mapping[pos]; // Copy the transaction into this Cluster, and remember its position. auto new_pos = m_depgraph.AddTransaction(other.m_depgraph.FeeRate(pos)); + // Since this cluster must have been made hole-free before being merged into, all added + // transactions should appear at the end. + Assume(new_pos == m_mapping.size()); remap[pos] = new_pos; - if (new_pos == m_mapping.size()) { - m_mapping.push_back(idx); - } else { - m_mapping[new_pos] = idx; - } + m_mapping.push_back(idx); m_linearization.push_back(new_pos); // Copy the transaction's dependencies, translating them using remap. Note that since // pos iterates over other.m_linearization, which is in topological order, all parents @@ -2286,6 +2288,10 @@ void Cluster::SanityCheck(const TxGraphImpl& graph, int level) const assert(m_depgraph.PositionRange() == m_mapping.size()); // The linearization for this Cluster must contain every transaction once. assert(m_depgraph.TxCount() == m_linearization.size()); + // Unless a split is to be applied, the cluster cannot have any holes. + if (!NeedsSplitting()) { + assert(m_depgraph.Positions() == SetType::Fill(m_depgraph.TxCount())); + } // Compute the chunking of m_linearization. LinearizationChunking linchunking(m_depgraph, m_linearization); From bb5cb222ae55a61646cb721ed19618ed332d37ff Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Mon, 4 Aug 2025 18:01:05 -0400 Subject: [PATCH 04/13] depgraph: add memory usage control (feature) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lőrinc --- src/cluster_linearize.h | 12 +++ src/test/fuzz/cluster_linearize.cpp | 113 +++++++++++++++------------- 2 files changed, 73 insertions(+), 52 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index bec44d973e..73c8a0378f 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -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. */ diff --git a/src/test/fuzz/cluster_linearize.cpp b/src/test/fuzz/cluster_linearize.cpp index 5cdcf79946..61b95c7131 100644 --- a/src/test/fuzz/cluster_linearize.cpp +++ b/src/test/fuzz/cluster_linearize.cpp @@ -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(); - if (num_tx_sim == 0 || ((command % 3) <= 0 && num_tx_sim < TestBitSet::Size())) { - // AddTransaction. - auto fee = provider.ConsumeIntegralInRange(-0x8000000000000, 0x7ffffffffffff); - auto size = provider.ConsumeIntegralInRange(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() % 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(-0x8000000000000, 0x7ffffffffffff); + auto size = provider.ConsumeIntegralInRange(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. From 4ba562e5f4e4aa2f390e6f3e871be9b0d216794e Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Tue, 5 Aug 2025 09:50:41 -0400 Subject: [PATCH 05/13] txgraph: keep data structures compact (mem optimization) --- src/txgraph.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index 4c4772ef1b..d730c397bf 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -177,6 +177,8 @@ public: void Clear(TxGraphImpl& graph, int level) noexcept; /** Change a Cluster's level from 1 (staging) to 0 (main). */ void MoveToMain(TxGraphImpl& graph) noexcept; + /** Minimize this Cluster's memory usage. */ + void Compact() noexcept; // Functions that implement the Cluster-specific side of internal TxGraphImpl mutations. @@ -910,6 +912,7 @@ void Cluster::ApplyRemovals(TxGraphImpl& graph, int level, std::span [&](auto pos) { return todo[pos]; }), m_linearization.end()); quality = QualityLevel::NEEDS_SPLIT; } + Compact(); graph.SetClusterQuality(level, m_quality, m_setindex, quality); Updated(graph, level); } @@ -937,6 +940,13 @@ void Cluster::MoveToMain(TxGraphImpl& graph) noexcept Updated(graph, /*level=*/0); } +void Cluster::Compact() noexcept +{ + m_linearization.shrink_to_fit(); + m_mapping.shrink_to_fit(); + m_depgraph.Compact(); +} + void Cluster::AppendChunkFeerates(std::vector& ret) const noexcept { auto chunk_feerates = ChunkLinearization(m_depgraph, m_linearization); @@ -1050,6 +1060,7 @@ bool Cluster::Split(TxGraphImpl& graph, int level) noexcept // Update all the Locators of moved transactions. for (Cluster* new_cluster : new_clusters) { new_cluster->Updated(graph, level); + new_cluster->Compact(); } // Wipe this Cluster, and return that it needs to be deleted. m_depgraph = DepGraph{}; @@ -1627,6 +1638,7 @@ void TxGraphImpl::Merge(std::span to_merge, int level) noexcept to_merge[0]->Merge(*this, level, *to_merge[i]); DeleteCluster(*to_merge[i], level); } + to_merge[0]->Compact(); } void TxGraphImpl::ApplyDependencies(int level) noexcept From 7680bb8fd48d2357d3c1a7b8121a6ad88d2f6ccf Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Tue, 5 Aug 2025 09:50:54 -0400 Subject: [PATCH 06/13] txgraph: keep track of Cluster memory usage (preparation) --- src/txgraph.cpp | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index d730c397bf..a2392bc8cd 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -153,6 +153,9 @@ public: return m_quality == QualityLevel::NEEDS_SPLIT || m_quality == QualityLevel::NEEDS_SPLIT_ACCEPTABLE; } + /** Total memory usage currently for this Cluster, including all its dynamic memory, plus Cluster + * structure itself, and ClusterSet::m_clusters entry. */ + size_t TotalMemoryUsage() const noexcept; /** Get the number of transactions in this Cluster. */ LinearizationIndex GetTxCount() const noexcept { return m_linearization.size(); } /** Get the total size of the transactions in this Cluster. */ @@ -308,6 +311,9 @@ private: GraphIndex m_txcount_oversized{0}; /** Whether this graph is oversized (if known). */ std::optional m_oversized{false}; + /** The combined TotalMemoryUsage of all clusters in this level (only Clusters that + * are materialized; in staging, implicit Clusters from main are not counted), */ + size_t m_cluster_usage{0}; ClusterSet() noexcept = default; }; @@ -703,6 +709,18 @@ void TxGraphImpl::CreateChunkData(GraphIndex idx, LinearizationIndex chunk_count } } +size_t Cluster::TotalMemoryUsage() const noexcept +{ + return // Dynamic memory allocated in this Cluster. + memusage::DynamicUsage(m_mapping) + memusage::DynamicUsage(m_linearization) + + // Dynamic memory usage inside m_depgraph. + m_depgraph.DynamicMemoryUsage() + + // Memory usage of the allocated Cluster itself. + memusage::MallocUsage(sizeof(Cluster)) + + // Memory usage of the ClusterSet::m_clusters entry. + sizeof(std::unique_ptr); +} + uint64_t Cluster::GetTotalTxSize() const noexcept { uint64_t ret{0}; @@ -851,9 +869,11 @@ Cluster* Cluster::CopyToStaging(TxGraphImpl& graph) const noexcept ptr->m_mapping = m_mapping; ptr->m_linearization = m_linearization; // Insert the new Cluster into the graph. - graph.InsertCluster(1, std::move(ret), m_quality); + graph.InsertCluster(/*level=*/1, std::move(ret), m_quality); // Update its Locators. ptr->Updated(graph, /*level=*/1); + // Update memory usage. + graph.GetClusterSet(/*level=*/1).m_cluster_usage += ptr->TotalMemoryUsage(); return ptr; } @@ -862,6 +882,7 @@ void Cluster::ApplyRemovals(TxGraphImpl& graph, int level, std::span // Iterate over the prefix of to_remove that applies to this cluster. Assume(!to_remove.empty()); SetType todo; + graph.GetClusterSet(level).m_cluster_usage -= TotalMemoryUsage(); do { GraphIndex idx = to_remove.front(); Assume(idx < graph.m_entries.size()); @@ -913,12 +934,14 @@ void Cluster::ApplyRemovals(TxGraphImpl& graph, int level, std::span quality = QualityLevel::NEEDS_SPLIT; } Compact(); + graph.GetClusterSet(level).m_cluster_usage += TotalMemoryUsage(); graph.SetClusterQuality(level, m_quality, m_setindex, quality); Updated(graph, level); } void Cluster::Clear(TxGraphImpl& graph, int level) noexcept { + graph.GetClusterSet(level).m_cluster_usage -= TotalMemoryUsage(); for (auto i : m_linearization) { graph.ClearLocator(level, m_mapping[i], m_quality == QualityLevel::OVERSIZED_SINGLETON); } @@ -935,6 +958,10 @@ void Cluster::MoveToMain(TxGraphImpl& graph) noexcept entry.m_locator[1].SetMissing(); } auto quality = m_quality; + // Subtract memory usage from staging and add it to main. + graph.GetClusterSet(/*level=*/1).m_cluster_usage -= TotalMemoryUsage(); + graph.GetClusterSet(/*level=*/0).m_cluster_usage += TotalMemoryUsage(); + // Remove cluster itself from staging and add it to main. auto cluster = graph.ExtractCluster(1, quality, m_setindex); graph.InsertCluster(/*level=*/0, std::move(cluster), quality); Updated(graph, /*level=*/0); @@ -1036,6 +1063,8 @@ bool Cluster::Split(TxGraphImpl& graph, int level) noexcept graph.InsertCluster(level, std::move(new_cluster), split_quality); todo -= component; } + // We have to split the Cluster up. Remove accounting for the existing one first. + graph.GetClusterSet(level).m_cluster_usage -= TotalMemoryUsage(); // Redistribute the transactions. for (auto i : m_linearization) { /** The cluster which transaction originally in position i is moved to. */ @@ -1057,10 +1086,11 @@ bool Cluster::Split(TxGraphImpl& graph, int level) noexcept for (auto par : m_depgraph.GetReducedParents(i)) new_parents.Set(remap[par].second); new_cluster->m_depgraph.AddDependencies(new_parents, remap[i].second); } - // Update all the Locators of moved transactions. + // Update all the Locators of moved transactions, and memory usage. for (Cluster* new_cluster : new_clusters) { new_cluster->Updated(graph, level); new_cluster->Compact(); + graph.GetClusterSet(level).m_cluster_usage += new_cluster->TotalMemoryUsage(); } // Wipe this Cluster, and return that it needs to be deleted. m_depgraph = DepGraph{}; @@ -1624,7 +1654,9 @@ void TxGraphImpl::Merge(std::span to_merge, int level) noexcept // moves. size_t max_size_pos{0}; DepGraphIndex max_size = to_merge[max_size_pos]->GetTxCount(); + GetClusterSet(level).m_cluster_usage -= to_merge[max_size_pos]->TotalMemoryUsage(); for (size_t i = 1; i < to_merge.size(); ++i) { + GetClusterSet(level).m_cluster_usage -= to_merge[i]->TotalMemoryUsage(); DepGraphIndex size = to_merge[i]->GetTxCount(); if (size > max_size) { max_size_pos = i; @@ -1639,6 +1671,7 @@ void TxGraphImpl::Merge(std::span to_merge, int level) noexcept DeleteCluster(*to_merge[i], level); } to_merge[0]->Compact(); + GetClusterSet(level).m_cluster_usage += to_merge[0]->TotalMemoryUsage(); } void TxGraphImpl::ApplyDependencies(int level) noexcept @@ -1764,6 +1797,7 @@ TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept auto& clusterset = GetClusterSet(level); InsertCluster(level, std::move(cluster), oversized ? QualityLevel::OVERSIZED_SINGLETON : QualityLevel::OPTIMAL); cluster_ptr->Updated(*this, level); + clusterset.m_cluster_usage += cluster_ptr->TotalMemoryUsage(); ++clusterset.m_txcount; // Deal with individually oversized transactions. if (oversized) { @@ -2116,6 +2150,7 @@ void TxGraphImpl::StartStaging() noexcept m_staging_clusterset->m_group_data = m_main_clusterset.m_group_data; m_staging_clusterset->m_oversized = m_main_clusterset.m_oversized; Assume(m_staging_clusterset->m_oversized.has_value()); + m_staging_clusterset->m_cluster_usage = 0; } void TxGraphImpl::AbortStaging() noexcept @@ -2413,6 +2448,7 @@ void TxGraphImpl::SanityCheck() const assert(level < MAX_LEVELS); auto& clusterset = GetClusterSet(level); std::set actual_clusters; + size_t recomputed_cluster_usage{0}; // For all quality levels... for (int qual = 0; qual < int(QualityLevel::NONE); ++qual) { @@ -2448,9 +2484,14 @@ void TxGraphImpl::SanityCheck() const // Check that the cluster's quality and setindex matches its position in the quality list. assert(cluster.m_quality == quality); assert(cluster.m_setindex == setindex); + // Count memory usage. + recomputed_cluster_usage += cluster.TotalMemoryUsage(); } } + // Verify memory usage. + assert(clusterset.m_cluster_usage == recomputed_cluster_usage); + // Verify that all to-be-removed transactions have valid identifiers. for (GraphIndex idx : clusterset.m_to_remove) { assert(idx < m_entries.size()); From 04c808ac4c47dcbaffa06476403db649c757436e Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Tue, 5 Aug 2025 11:06:15 -0400 Subject: [PATCH 07/13] txgraph: expose memory usage estimate function (feature) --- src/test/fuzz/txgraph.cpp | 15 +++++++++++++++ src/txgraph.cpp | 17 +++++++++++++++++ src/txgraph.h | 7 +++++++ 3 files changed, 39 insertions(+) diff --git a/src/test/fuzz/txgraph.cpp b/src/test/fuzz/txgraph.cpp index 0000103471..fbf22bfc4c 100644 --- a/src/test/fuzz/txgraph.cpp +++ b/src/test/fuzz/txgraph.cpp @@ -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; } } } diff --git a/src/txgraph.cpp b/src/txgraph.cpp index a2392bc8cd..41a59b5b75 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -629,6 +629,8 @@ public: std::unique_ptr GetBlockBuilder() noexcept final; std::pair, FeePerWeight> GetWorstMainChunk() noexcept final; + size_t GetMainMemoryUsage() noexcept final; + void SanityCheck() const final; }; @@ -2980,6 +2982,21 @@ std::vector TxGraphImpl::Trim() noexcept return ret; } +size_t TxGraphImpl::GetMainMemoryUsage() noexcept +{ + // Make sure splits/merges are applied, as memory usage may not be representative otherwise. + SplitAll(/*up_to_level=*/0); + ApplyDependencies(/*level=*/0); + // Compute memory usage + size_t usage = /* From clusters */ + m_main_clusterset.m_cluster_usage + + /* From Entry objects. */ + sizeof(Entry) * m_main_clusterset.m_txcount + + /* From the chunk index. */ + memusage::DynamicUsage(m_main_chunkindex); + return usage; +} + } // namespace TxGraph::Ref::~Ref() diff --git a/src/txgraph.h b/src/txgraph.h index 20ea97ac5e..1d8d31a542 100644 --- a/src/txgraph.h +++ b/src/txgraph.h @@ -202,6 +202,13 @@ public: * graph must not be oversized. If the graph is empty, {{}, FeePerWeight{}} is returned. */ virtual std::pair, 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; From 2602d89edd04693d0ebbc6b3c87a7c3fe90dcf40 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 5 Sep 2025 11:12:23 -0400 Subject: [PATCH 08/13] txgraph: avoid accessing other Cluster internals (refactor) This adds 4 functions to Cluster to help implement Merge() and Split() without needing access to the internals of the other Cluster. This is a preparation for a follow-up that will make Clusters a virtual class whose internals are abstracted away. --- src/txgraph.cpp | 86 ++++++++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index 41a59b5b75..35985482bb 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -120,9 +121,7 @@ class Cluster public: Cluster() noexcept = delete; /** Construct an empty Cluster. */ - explicit Cluster(uint64_t sequence) noexcept; - /** Construct a singleton Cluster. */ - explicit Cluster(uint64_t sequence, TxGraphImpl& graph, const FeePerWeight& feerate, GraphIndex graph_index) noexcept; + explicit Cluster(uint64_t sequence) noexcept : m_sequence(sequence) {} // Cannot move or copy (would invalidate Cluster* in Locator and ClusterSet). */ Cluster(const Cluster&) = delete; @@ -153,15 +152,25 @@ public: return m_quality == QualityLevel::NEEDS_SPLIT || m_quality == QualityLevel::NEEDS_SPLIT_ACCEPTABLE; } + /** Total memory usage currently for this Cluster, including all its dynamic memory, plus Cluster * structure itself, and ClusterSet::m_clusters entry. */ size_t TotalMemoryUsage() const noexcept; + /** Determine the range of DepGraphIndexes used by this Cluster. */ + DepGraphIndex GetDepGraphIndexRange() const noexcept { return m_depgraph.PositionRange(); } /** Get the number of transactions in this Cluster. */ LinearizationIndex GetTxCount() const noexcept { return m_linearization.size(); } /** Get the total size of the transactions in this Cluster. */ uint64_t GetTotalTxSize() const noexcept; /** Given a DepGraphIndex into this Cluster, find the corresponding GraphIndex. */ GraphIndex GetClusterEntry(DepGraphIndex index) const noexcept { return m_mapping[index]; } + /** Append a transaction with given GraphIndex at the end of this Cluster and its + * linearization. Return the DepGraphIndex it was placed at. */ + DepGraphIndex AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept; + /** Add dependencies to a given child in this cluster. */ + void AddDependencies(SetType parents, DepGraphIndex child) noexcept; + /** Invoke visitor_fn for each transaction in the cluster, in linearization order, then wipe this Cluster. */ + void ExtractTransactions(const std::function& visit_fn) noexcept; /** Figure out what level this Cluster exists at in the graph. In most cases this is known by * the caller already (see all "int level" arguments below), but not always. */ int GetLevel(const TxGraphImpl& graph) const noexcept; @@ -186,7 +195,7 @@ public: // Functions that implement the Cluster-specific side of internal TxGraphImpl mutations. /** Apply all removals from the front of to_remove that apply to this Cluster, popping them - * off. These must be at least one such entry. */ + * off. There must be at least one such entry. */ void ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept; /** Split this cluster (must have a NEEDS_SPLIT* quality). Returns whether to delete this * Cluster afterwards. */ @@ -732,6 +741,31 @@ uint64_t Cluster::GetTotalTxSize() const noexcept return ret; } +DepGraphIndex Cluster::AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept +{ + Assume(graph_idx != GraphIndex(-1)); + auto ret = m_depgraph.AddTransaction(feerate); + m_mapping.push_back(graph_idx); + m_linearization.push_back(ret); + return ret; +} + +void Cluster::AddDependencies(SetType parent, DepGraphIndex child) noexcept +{ + m_depgraph.AddDependencies(parent, child); +} + +void Cluster::ExtractTransactions(const std::function& visit_fn) noexcept +{ + for (auto pos : m_linearization) { + visit_fn(pos, m_mapping[pos], FeePerWeight::FromFeeFrac(m_depgraph.FeeRate(pos)), m_depgraph.GetReducedParents(pos)); + } + // Purge this Cluster, now that everything has been moved. + m_depgraph = DepGraph{}; + m_linearization.clear(); + m_mapping.clear(); +} + int Cluster::GetLevel(const TxGraphImpl& graph) const noexcept { // GetLevel() does not work for empty Clusters. @@ -1072,12 +1106,7 @@ bool Cluster::Split(TxGraphImpl& graph, int level) noexcept /** The cluster which transaction originally in position i is moved to. */ Cluster* new_cluster = remap[i].first; // Copy the transaction to the new cluster's depgraph, and remember the position. - remap[i].second = new_cluster->m_depgraph.AddTransaction(m_depgraph.FeeRate(i)); - // Create new mapping entry. - new_cluster->m_mapping.push_back(m_mapping[i]); - // Create a new linearization entry. As we're only appending transactions, they equal the - // DepGraphIndex. - new_cluster->m_linearization.push_back(remap[i].second); + remap[i].second = new_cluster->AppendTransaction(m_mapping[i], FeePerWeight::FromFeeFrac(m_depgraph.FeeRate(i))); } // Redistribute the dependencies. for (auto i : m_linearization) { @@ -1086,7 +1115,7 @@ bool Cluster::Split(TxGraphImpl& graph, int level) noexcept // Copy its parents, translating positions. SetType new_parents; for (auto par : m_depgraph.GetReducedParents(i)) new_parents.Set(remap[par].second); - new_cluster->m_depgraph.AddDependencies(new_parents, remap[i].second); + new_cluster->AddDependencies(new_parents, remap[i].second); } // Update all the Locators of moved transactions, and memory usage. for (Cluster* new_cluster : new_clusters) { @@ -1104,12 +1133,11 @@ bool Cluster::Split(TxGraphImpl& graph, int level) noexcept void Cluster::Merge(TxGraphImpl& graph, int level, Cluster& other) noexcept { /** Vector to store the positions in this Cluster for each position in other. */ - std::vector remap(other.m_depgraph.PositionRange()); + std::vector remap(other.GetDepGraphIndexRange()); // Iterate over all transactions in the other Cluster (the one being absorbed). - for (auto pos : other.m_linearization) { - auto idx = other.m_mapping[pos]; + other.ExtractTransactions([&](DepGraphIndex pos, GraphIndex idx, FeePerWeight feerate, SetType other_parents) noexcept { // Copy the transaction into this Cluster, and remember its position. - auto new_pos = m_depgraph.AddTransaction(other.m_depgraph.FeeRate(pos)); + auto new_pos = m_depgraph.AddTransaction(feerate); // Since this cluster must have been made hole-free before being merged into, all added // transactions should appear at the end. Assume(new_pos == m_mapping.size()); @@ -1117,26 +1145,22 @@ void Cluster::Merge(TxGraphImpl& graph, int level, Cluster& other) noexcept m_mapping.push_back(idx); m_linearization.push_back(new_pos); // Copy the transaction's dependencies, translating them using remap. Note that since - // pos iterates over other.m_linearization, which is in topological order, all parents - // of pos should already be in remap. + // pos iterates in linearization order, which is topological, all parents of pos should + // already be in remap. SetType parents; - for (auto par : other.m_depgraph.GetReducedParents(pos)) { + for (auto par : other_parents) { parents.Set(remap[par]); } m_depgraph.AddDependencies(parents, remap[pos]); // Update the transaction's Locator. There is no need to call Updated() to update chunk // feerates, as Updated() will be invoked by Cluster::ApplyDependencies on the resulting - // merged Cluster later anyway). + // merged Cluster later anyway. auto& entry = graph.m_entries[idx]; // Discard any potential ChunkData prior to modifying the Cluster (as that could // invalidate its ordering). if (level == 0) graph.ClearChunkData(entry); entry.m_locator[level].SetPresent(this, new_pos); - } - // Purge the other Cluster, now that everything has been moved. - other.m_depgraph = DepGraph{}; - other.m_linearization.clear(); - other.m_mapping.clear(); + }); } void Cluster::ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) noexcept @@ -1766,17 +1790,6 @@ void TxGraphImpl::MakeAllAcceptable(int level) noexcept } } -Cluster::Cluster(uint64_t sequence) noexcept : m_sequence{sequence} {} - -Cluster::Cluster(uint64_t sequence, TxGraphImpl& graph, const FeePerWeight& feerate, GraphIndex graph_index) noexcept : - m_sequence{sequence} -{ - // Create a new transaction in the DepGraph, and remember its position in m_mapping. - auto cluster_idx = m_depgraph.AddTransaction(feerate); - m_mapping.push_back(graph_index); - m_linearization.push_back(cluster_idx); -} - TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept { Assume(m_main_chunkindex_observers == 0 || GetTopLevel() != 0); @@ -1793,7 +1806,8 @@ TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept GetRefIndex(ret) = idx; // Construct a new singleton Cluster (which is necessarily optimally linearized). bool oversized = uint64_t(feerate.size) > m_max_cluster_size; - auto cluster = std::make_unique(m_next_sequence_counter++, *this, feerate, idx); + auto cluster = std::make_unique(m_next_sequence_counter++); + cluster->AppendTransaction(idx, feerate); auto cluster_ptr = cluster.get(); int level = GetTopLevel(); auto& clusterset = GetClusterSet(level); From 726b995739ab7b88fb5d1c85a33e6c68a4e146bc Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 8 Oct 2025 16:41:08 -0400 Subject: [PATCH 09/13] txgraph: make Cluster an abstract class (refactor) --- src/txgraph.cpp | 195 ++++++++++++++++++++++++++++++------------------ 1 file changed, 122 insertions(+), 73 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index 35985482bb..8e881fca7d 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -99,17 +99,10 @@ struct TrimTxData class Cluster { friend class TxGraphImpl; + +protected: using GraphIndex = TxGraph::GraphIndex; using SetType = BitSet; - /** The DepGraph for this cluster, holding all feerates, and ancestors/descendants. */ - DepGraph m_depgraph; - /** m_mapping[i] gives the GraphIndex for the position i transaction in m_depgraph. Values for - * positions i that do not exist in m_depgraph shouldn't ever be accessed and thus don't - * matter. m_mapping.size() equals m_depgraph.PositionRange(). */ - std::vector m_mapping; - /** The current linearization of the cluster. m_linearization.size() equals - * m_depgraph.TxCount(). This is always kept topological. */ - std::vector m_linearization; /** The quality level of m_linearization. */ QualityLevel m_quality{QualityLevel::NONE}; /** Which position this Cluster has in Graph::ClusterSet::m_clusters[m_quality]. */ @@ -118,11 +111,12 @@ class Cluster transactions in distinct clusters). */ uint64_t m_sequence; -public: - Cluster() noexcept = delete; - /** Construct an empty Cluster. */ explicit Cluster(uint64_t sequence) noexcept : m_sequence(sequence) {} +public: + // Provide virtual destructor, for safe polymorphic usage inside std::unique_ptr. + virtual ~Cluster() = default; + // Cannot move or copy (would invalidate Cluster* in Locator and ClusterSet). */ Cluster(const Cluster&) = delete; Cluster& operator=(const Cluster&) = delete; @@ -155,85 +149,137 @@ public: /** Total memory usage currently for this Cluster, including all its dynamic memory, plus Cluster * structure itself, and ClusterSet::m_clusters entry. */ - size_t TotalMemoryUsage() const noexcept; + virtual size_t TotalMemoryUsage() const noexcept = 0; /** Determine the range of DepGraphIndexes used by this Cluster. */ - DepGraphIndex GetDepGraphIndexRange() const noexcept { return m_depgraph.PositionRange(); } + virtual DepGraphIndex GetDepGraphIndexRange() const noexcept = 0; /** Get the number of transactions in this Cluster. */ - LinearizationIndex GetTxCount() const noexcept { return m_linearization.size(); } + virtual LinearizationIndex GetTxCount() const noexcept = 0; /** Get the total size of the transactions in this Cluster. */ - uint64_t GetTotalTxSize() const noexcept; + virtual uint64_t GetTotalTxSize() const noexcept = 0; /** Given a DepGraphIndex into this Cluster, find the corresponding GraphIndex. */ - GraphIndex GetClusterEntry(DepGraphIndex index) const noexcept { return m_mapping[index]; } + virtual GraphIndex GetClusterEntry(DepGraphIndex index) const noexcept = 0; /** Append a transaction with given GraphIndex at the end of this Cluster and its * linearization. Return the DepGraphIndex it was placed at. */ - DepGraphIndex AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept; + virtual DepGraphIndex AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept = 0; /** Add dependencies to a given child in this cluster. */ - void AddDependencies(SetType parents, DepGraphIndex child) noexcept; + virtual void AddDependencies(SetType parents, DepGraphIndex child) noexcept = 0; /** Invoke visitor_fn for each transaction in the cluster, in linearization order, then wipe this Cluster. */ - void ExtractTransactions(const std::function& visit_fn) noexcept; + virtual void ExtractTransactions(const std::function& visit_fn) noexcept = 0; /** Figure out what level this Cluster exists at in the graph. In most cases this is known by * the caller already (see all "int level" arguments below), but not always. */ - int GetLevel(const TxGraphImpl& graph) const noexcept; + virtual int GetLevel(const TxGraphImpl& graph) const noexcept = 0; /** Only called by Graph::SwapIndexes. */ - void UpdateMapping(DepGraphIndex cluster_idx, GraphIndex graph_idx) noexcept { m_mapping[cluster_idx] = graph_idx; } + virtual void UpdateMapping(DepGraphIndex cluster_idx, GraphIndex graph_idx) noexcept = 0; /** Push changes to Cluster and its linearization to the TxGraphImpl Entry objects. */ - void Updated(TxGraphImpl& graph, int level) noexcept; + virtual void Updated(TxGraphImpl& graph, int level) noexcept = 0; /** Create a copy of this Cluster in staging, returning a pointer to it (used by PullIn). */ - Cluster* CopyToStaging(TxGraphImpl& graph) const noexcept; + virtual Cluster* CopyToStaging(TxGraphImpl& graph) const noexcept = 0; /** Get the list of Clusters in main that conflict with this one (which is assumed to be in staging). */ - void GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept; + virtual void GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept = 0; /** Mark all the Entry objects belonging to this staging Cluster as missing. The Cluster must be * deleted immediately after. */ - void MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept; + virtual void MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept = 0; /** Remove all transactions from a Cluster. */ - void Clear(TxGraphImpl& graph, int level) noexcept; + virtual void Clear(TxGraphImpl& graph, int level) noexcept = 0; /** Change a Cluster's level from 1 (staging) to 0 (main). */ - void MoveToMain(TxGraphImpl& graph) noexcept; + virtual void MoveToMain(TxGraphImpl& graph) noexcept = 0; /** Minimize this Cluster's memory usage. */ - void Compact() noexcept; + virtual void Compact() noexcept = 0; // Functions that implement the Cluster-specific side of internal TxGraphImpl mutations. /** Apply all removals from the front of to_remove that apply to this Cluster, popping them * off. There must be at least one such entry. */ - void ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept; + virtual void ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept = 0; /** Split this cluster (must have a NEEDS_SPLIT* quality). Returns whether to delete this * Cluster afterwards. */ - [[nodiscard]] bool Split(TxGraphImpl& graph, int level) noexcept; + [[nodiscard]] virtual bool Split(TxGraphImpl& graph, int level) noexcept = 0; /** Move all transactions from cluster to *this (as separate components). */ - void Merge(TxGraphImpl& graph, int level, Cluster& cluster) noexcept; + virtual void Merge(TxGraphImpl& graph, int level, Cluster& cluster) noexcept = 0; /** Given a span of (parent, child) pairs that all belong to this Cluster, apply them. */ - void ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) noexcept; + virtual void ApplyDependencies(TxGraphImpl& graph, int level, std::span> 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. */ - std::pair Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept; + virtual std::pair Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept = 0; /** For every chunk in the cluster, append its FeeFrac to ret. */ - void AppendChunkFeerates(std::vector& ret) const noexcept; + virtual void AppendChunkFeerates(std::vector& ret) const noexcept = 0; /** Add a TrimTxData entry (filling m_chunk_feerate, m_index, m_tx_size) for every * transaction in the Cluster to ret. Implicit dependencies between consecutive transactions * in the linearization are added to deps. Return the Cluster's total transaction size. */ - uint64_t AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept; + virtual uint64_t AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept = 0; // Functions that implement the Cluster-specific side of public TxGraph functions. /** Process elements from the front of args that apply to this cluster, and append Refs for the * union of their ancestors to output. */ - void GetAncestorRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept; + virtual void GetAncestorRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept = 0; /** Process elements from the front of args that apply to this cluster, and append Refs for the * union of their descendants to output. */ - void GetDescendantRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept; + virtual void GetDescendantRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept = 0; /** Populate range with refs for the transactions in this Cluster's linearization, from * position start_pos until start_pos+range.size()-1, inclusive. Returns whether that * range includes the last transaction in the linearization. */ - bool GetClusterRefs(TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept; + virtual bool GetClusterRefs(TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept = 0; /** Get the individual transaction feerate of a Cluster element. */ - FeePerWeight GetIndividualFeerate(DepGraphIndex idx) noexcept; + virtual FeePerWeight GetIndividualFeerate(DepGraphIndex idx) noexcept = 0; /** Modify the fee of a Cluster element. */ - void SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept; + virtual void SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept = 0; // Debugging functions. - void SanityCheck(const TxGraphImpl& graph, int level) const; + virtual void SanityCheck(const TxGraphImpl& graph, int level) const = 0; +}; + +/** An implementation of Cluster that uses a DepGraph and vectors, to support arbitrary numbers of + * transactions up to MAX_CLUSTER_COUNT_LIMIT. */ +class GenericClusterImpl final : public Cluster +{ + friend class TxGraphImpl; + /** The DepGraph for this cluster, holding all feerates, and ancestors/descendants. */ + DepGraph m_depgraph; + /** m_mapping[i] gives the GraphIndex for the position i transaction in m_depgraph. Values for + * positions i that do not exist in m_depgraph shouldn't ever be accessed and thus don't + * matter. m_mapping.size() equals m_depgraph.PositionRange(). */ + std::vector m_mapping; + /** The current linearization of the cluster. m_linearization.size() equals + * m_depgraph.TxCount(). This is always kept topological. */ + std::vector m_linearization; + +public: + GenericClusterImpl() noexcept = delete; + /** Construct an empty GenericClusterImpl. */ + explicit GenericClusterImpl(uint64_t sequence) noexcept; + + size_t TotalMemoryUsage() const noexcept final; + DepGraphIndex GetDepGraphIndexRange() const noexcept final { return m_depgraph.PositionRange(); } + LinearizationIndex GetTxCount() const noexcept final { return m_linearization.size(); } + uint64_t GetTotalTxSize() const noexcept final; + GraphIndex GetClusterEntry(DepGraphIndex index) const noexcept final { return m_mapping[index]; } + DepGraphIndex AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept final; + void AddDependencies(SetType parents, DepGraphIndex child) noexcept final; + void ExtractTransactions(const std::function& visit_fn) noexcept final; + int GetLevel(const TxGraphImpl& graph) const noexcept final; + void UpdateMapping(DepGraphIndex cluster_idx, GraphIndex graph_idx) noexcept final { m_mapping[cluster_idx] = graph_idx; } + void Updated(TxGraphImpl& graph, int level) noexcept final; + Cluster* CopyToStaging(TxGraphImpl& graph) const noexcept final; + void GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept final; + void MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept final; + void Clear(TxGraphImpl& graph, int level) noexcept final; + void MoveToMain(TxGraphImpl& graph) noexcept final; + void Compact() noexcept final; + void ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept final; + [[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> to_apply) noexcept final; + std::pair Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept final; + void AppendChunkFeerates(std::vector& ret) const noexcept final; + uint64_t AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept final; + void GetAncestorRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept final; + void GetDescendantRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept final; + bool GetClusterRefs(TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept final; + FeePerWeight GetIndividualFeerate(DepGraphIndex idx) noexcept final; + void SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept final; + void SanityCheck(const TxGraphImpl& graph, int level) const final; }; @@ -263,6 +309,7 @@ public: class TxGraphImpl final : public TxGraph { friend class Cluster; + friend class GenericClusterImpl; friend class BlockBuilderImpl; private: /** Internal RNG. */ @@ -720,19 +767,19 @@ void TxGraphImpl::CreateChunkData(GraphIndex idx, LinearizationIndex chunk_count } } -size_t Cluster::TotalMemoryUsage() const noexcept +size_t GenericClusterImpl::TotalMemoryUsage() const noexcept { return // Dynamic memory allocated in this Cluster. memusage::DynamicUsage(m_mapping) + memusage::DynamicUsage(m_linearization) + // Dynamic memory usage inside m_depgraph. m_depgraph.DynamicMemoryUsage() + // Memory usage of the allocated Cluster itself. - memusage::MallocUsage(sizeof(Cluster)) + + memusage::MallocUsage(sizeof(GenericClusterImpl)) + // Memory usage of the ClusterSet::m_clusters entry. sizeof(std::unique_ptr); } -uint64_t Cluster::GetTotalTxSize() const noexcept +uint64_t GenericClusterImpl::GetTotalTxSize() const noexcept { uint64_t ret{0}; for (auto i : m_linearization) { @@ -741,7 +788,7 @@ uint64_t Cluster::GetTotalTxSize() const noexcept return ret; } -DepGraphIndex Cluster::AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept +DepGraphIndex GenericClusterImpl::AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept { Assume(graph_idx != GraphIndex(-1)); auto ret = m_depgraph.AddTransaction(feerate); @@ -750,12 +797,12 @@ DepGraphIndex Cluster::AppendTransaction(GraphIndex graph_idx, FeePerWeight feer return ret; } -void Cluster::AddDependencies(SetType parent, DepGraphIndex child) noexcept +void GenericClusterImpl::AddDependencies(SetType parents, DepGraphIndex child) noexcept { - m_depgraph.AddDependencies(parent, child); + m_depgraph.AddDependencies(parents, child); } -void Cluster::ExtractTransactions(const std::function& visit_fn) noexcept +void GenericClusterImpl::ExtractTransactions(const std::function& visit_fn) noexcept { for (auto pos : m_linearization) { visit_fn(pos, m_mapping[pos], FeePerWeight::FromFeeFrac(m_depgraph.FeeRate(pos)), m_depgraph.GetReducedParents(pos)); @@ -766,7 +813,7 @@ void Cluster::ExtractTransactions(const std::function& out) const noexcept +void GenericClusterImpl::GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept { for (auto i : m_linearization) { auto& entry = graph.m_entries[m_mapping[i]]; @@ -895,10 +942,10 @@ std::vector TxGraphImpl::GetConflicts() const noexcept return ret; } -Cluster* Cluster::CopyToStaging(TxGraphImpl& graph) const noexcept +Cluster* GenericClusterImpl::CopyToStaging(TxGraphImpl& graph) const noexcept { // Construct an empty Cluster. - auto ret = std::make_unique(graph.m_next_sequence_counter++); + auto ret = std::make_unique(graph.m_next_sequence_counter++); auto ptr = ret.get(); // Copy depgraph, mapping, and linearization/ ptr->m_depgraph = m_depgraph; @@ -913,7 +960,7 @@ Cluster* Cluster::CopyToStaging(TxGraphImpl& graph) const noexcept return ptr; } -void Cluster::ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept +void GenericClusterImpl::ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept { // Iterate over the prefix of to_remove that applies to this cluster. Assume(!to_remove.empty()); @@ -975,7 +1022,7 @@ void Cluster::ApplyRemovals(TxGraphImpl& graph, int level, std::span Updated(graph, level); } -void Cluster::Clear(TxGraphImpl& graph, int level) noexcept +void GenericClusterImpl::Clear(TxGraphImpl& graph, int level) noexcept { graph.GetClusterSet(level).m_cluster_usage -= TotalMemoryUsage(); for (auto i : m_linearization) { @@ -986,7 +1033,7 @@ void Cluster::Clear(TxGraphImpl& graph, int level) noexcept m_mapping.clear(); } -void Cluster::MoveToMain(TxGraphImpl& graph) noexcept +void GenericClusterImpl::MoveToMain(TxGraphImpl& graph) noexcept { for (auto i : m_linearization) { GraphIndex idx = m_mapping[i]; @@ -1003,21 +1050,21 @@ void Cluster::MoveToMain(TxGraphImpl& graph) noexcept Updated(graph, /*level=*/0); } -void Cluster::Compact() noexcept +void GenericClusterImpl::Compact() noexcept { m_linearization.shrink_to_fit(); m_mapping.shrink_to_fit(); m_depgraph.Compact(); } -void Cluster::AppendChunkFeerates(std::vector& ret) const noexcept +void GenericClusterImpl::AppendChunkFeerates(std::vector& ret) const noexcept { auto chunk_feerates = ChunkLinearization(m_depgraph, m_linearization); ret.reserve(ret.size() + chunk_feerates.size()); ret.insert(ret.end(), chunk_feerates.begin(), chunk_feerates.end()); } -uint64_t Cluster::AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept +uint64_t GenericClusterImpl::AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept { const LinearizationChunking linchunking(m_depgraph, m_linearization); LinearizationIndex pos{0}; @@ -1048,7 +1095,7 @@ uint64_t Cluster::AppendTrimData(std::vector& ret, std::vector(graph.m_next_sequence_counter++); + auto new_cluster = std::make_unique(graph.m_next_sequence_counter++); new_clusters.push_back(new_cluster.get()); // Remember that all the component's transactions go to this new Cluster. The positions // will be determined below, so use -1 for now. @@ -1130,7 +1177,7 @@ bool Cluster::Split(TxGraphImpl& graph, int level) noexcept return true; } -void Cluster::Merge(TxGraphImpl& graph, int level, Cluster& other) noexcept +void GenericClusterImpl::Merge(TxGraphImpl& graph, int level, Cluster& other) noexcept { /** Vector to store the positions in this Cluster for each position in other. */ std::vector remap(other.GetDepGraphIndexRange()); @@ -1163,7 +1210,7 @@ void Cluster::Merge(TxGraphImpl& graph, int level, Cluster& other) noexcept }); } -void Cluster::ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) noexcept +void GenericClusterImpl::ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) noexcept { // This function is invoked by TxGraphImpl::ApplyDependencies after merging groups of Clusters // between which dependencies are added, which simply concatenates their linearizations. Invoke @@ -1743,7 +1790,7 @@ void TxGraphImpl::ApplyDependencies(int level) noexcept clusterset.m_group_data = GroupData{}; } -std::pair Cluster::Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept +std::pair GenericClusterImpl::Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept { // We can only relinearize Clusters that do not need splitting. Assume(!NeedsSplitting()); @@ -1790,6 +1837,8 @@ void TxGraphImpl::MakeAllAcceptable(int level) noexcept } } +GenericClusterImpl::GenericClusterImpl(uint64_t sequence) noexcept : Cluster{sequence} {} + TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept { Assume(m_main_chunkindex_observers == 0 || GetTopLevel() != 0); @@ -1806,7 +1855,7 @@ TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept GetRefIndex(ret) = idx; // Construct a new singleton Cluster (which is necessarily optimally linearized). bool oversized = uint64_t(feerate.size) > m_max_cluster_size; - auto cluster = std::make_unique(m_next_sequence_counter++); + auto cluster = std::make_unique(m_next_sequence_counter++); cluster->AppendTransaction(idx, feerate); auto cluster_ptr = cluster.get(); int level = GetTopLevel(); @@ -1879,7 +1928,7 @@ bool TxGraphImpl::Exists(const Ref& arg, Level level_select) noexcept return cluster != nullptr; } -void Cluster::GetAncestorRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept +void GenericClusterImpl::GetAncestorRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept { /** The union of all ancestors to be returned. */ SetType ancestors_union; @@ -1898,7 +1947,7 @@ void Cluster::GetAncestorRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept +void GenericClusterImpl::GetDescendantRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept { /** The union of all descendants to be returned. */ SetType descendants_union; @@ -1917,7 +1966,7 @@ void Cluster::GetDescendantRefs(const TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept +bool GenericClusterImpl::GetClusterRefs(TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept { // Translate the transactions in the Cluster (in linearization order, starting at start_pos in // the linearization) to Refs, and fill them in range. @@ -1931,12 +1980,12 @@ bool Cluster::GetClusterRefs(TxGraphImpl& graph, std::span range, return start_pos == m_linearization.size(); } -FeePerWeight Cluster::GetIndividualFeerate(DepGraphIndex idx) noexcept +FeePerWeight GenericClusterImpl::GetIndividualFeerate(DepGraphIndex idx) noexcept { return FeePerWeight::FromFeeFrac(m_depgraph.FeeRate(idx)); } -void Cluster::MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept +void GenericClusterImpl::MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept { // Mark all transactions of a Cluster missing, needed when aborting staging, so that the // corresponding Locators don't retain references into aborted Clusters. @@ -2236,7 +2285,7 @@ void TxGraphImpl::CommitStaging() noexcept Compact(); } -void Cluster::SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept +void GenericClusterImpl::SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept { // Make sure the specified DepGraphIndex exists in this Cluster. Assume(m_depgraph.Positions()[idx]); @@ -2345,7 +2394,7 @@ std::pair, std::vector> TxGraphImpl::GetMainStagin return std::make_pair(std::move(main_feerates), std::move(staging_feerates)); } -void Cluster::SanityCheck(const TxGraphImpl& graph, int level) const +void GenericClusterImpl::SanityCheck(const TxGraphImpl& graph, int level) const { // There must be an m_mapping for each m_depgraph position (including holes). assert(m_depgraph.PositionRange() == m_mapping.size()); From 6baf12621f66f124cadffe4bb00cd11b75a7c3ea Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 9 Oct 2025 13:03:10 -0400 Subject: [PATCH 10/13] txgraph: comment fixes (doc fix) --- src/txgraph.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index 8e881fca7d..6d45fb891a 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -30,7 +30,7 @@ class TxGraphImpl; /** Position of a DepGraphIndex within a Cluster::m_linearization. */ using LinearizationIndex = uint32_t; -/** Position of a Cluster within Graph::ClusterSet::m_clusters. */ +/** Position of a Cluster within TxGraphImpl::ClusterSet::m_clusters. */ using ClusterSetIndex = uint32_t; /** Quality levels for cached cluster linearizations. */ @@ -105,7 +105,7 @@ protected: using SetType = BitSet; /** The quality level of m_linearization. */ QualityLevel m_quality{QualityLevel::NONE}; - /** Which position this Cluster has in Graph::ClusterSet::m_clusters[m_quality]. */ + /** Which position this Cluster has in TxGraphImpl::ClusterSet::m_clusters[m_quality]. */ ClusterSetIndex m_setindex{ClusterSetIndex(-1)}; /** Sequence number for this Cluster (for tie-breaking comparison between equal-chunk-feerate transactions in distinct clusters). */ @@ -168,7 +168,7 @@ public: /** Figure out what level this Cluster exists at in the graph. In most cases this is known by * the caller already (see all "int level" arguments below), but not always. */ virtual int GetLevel(const TxGraphImpl& graph) const noexcept = 0; - /** Only called by Graph::SwapIndexes. */ + /** Only called by TxGraphImpl::SwapIndexes. */ virtual void UpdateMapping(DepGraphIndex cluster_idx, GraphIndex graph_idx) noexcept = 0; /** Push changes to Cluster and its linearization to the TxGraphImpl Entry objects. */ virtual void Updated(TxGraphImpl& graph, int level) noexcept = 0; @@ -553,7 +553,7 @@ public: Cluster* FindCluster(GraphIndex idx, int level) const noexcept { return FindClusterAndLevel(idx, level).first; } /** Like FindCluster, but also return what level the match was found in (-1 if not found). */ std::pair FindClusterAndLevel(GraphIndex idx, int level) const noexcept; - /** Extract a Cluster from its ClusterSet. */ + /** Extract a Cluster from its ClusterSet, and set its quality to QualityLevel::NONE. */ std::unique_ptr ExtractCluster(int level, QualityLevel quality, ClusterSetIndex setindex) noexcept; /** Delete a Cluster. */ void DeleteCluster(Cluster& cluster, int level) noexcept; From e93b0f09cc2ad7b743f896a09c48d584dfcc2787 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sun, 7 Sep 2025 10:12:36 -0400 Subject: [PATCH 11/13] txgraph: abstract out creation of empty Clusters (refactor) --- src/txgraph.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index 6d45fb891a..a09754abb2 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -578,6 +578,18 @@ public: void ClearChunkData(Entry& entry) noexcept; /** Give an Entry a ChunkData object. */ void CreateChunkData(GraphIndex idx, LinearizationIndex chunk_count) noexcept; + /** Create an empty GenericClusterImpl object. */ + std::unique_ptr CreateEmptyGenericCluster() noexcept + { + return std::make_unique(m_next_sequence_counter++); + } + /** Create an empty Cluster of the appropriate implementation for the specified (maximum) tx + * count. */ + std::unique_ptr CreateEmptyCluster(DepGraphIndex tx_count) noexcept + { + (void)tx_count; + return CreateEmptyGenericCluster(); + } // Functions for handling Refs. @@ -945,7 +957,7 @@ std::vector TxGraphImpl::GetConflicts() const noexcept Cluster* GenericClusterImpl::CopyToStaging(TxGraphImpl& graph) const noexcept { // Construct an empty Cluster. - auto ret = std::make_unique(graph.m_next_sequence_counter++); + auto ret = graph.CreateEmptyGenericCluster(); auto ptr = ret.get(); // Copy depgraph, mapping, and linearization/ ptr->m_depgraph = m_depgraph; @@ -1136,7 +1148,7 @@ bool GenericClusterImpl::Split(TxGraphImpl& graph, int level) noexcept } first = false; // Construct a new Cluster to hold the found component. - auto new_cluster = std::make_unique(graph.m_next_sequence_counter++); + auto new_cluster = graph.CreateEmptyCluster(component_size); new_clusters.push_back(new_cluster.get()); // Remember that all the component's transactions go to this new Cluster. The positions // will be determined below, so use -1 for now. @@ -1855,7 +1867,7 @@ TxGraph::Ref TxGraphImpl::AddTransaction(const FeePerWeight& feerate) noexcept GetRefIndex(ret) = idx; // Construct a new singleton Cluster (which is necessarily optimally linearized). bool oversized = uint64_t(feerate.size) > m_max_cluster_size; - auto cluster = std::make_unique(m_next_sequence_counter++); + auto cluster = CreateEmptyCluster(1); cluster->AppendTransaction(idx, feerate); auto cluster_ptr = cluster.get(); int level = GetTopLevel(); From e34625073253132c4b711b3c33ee50be60290e35 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Thu, 9 Oct 2025 10:57:53 -0400 Subject: [PATCH 12/13] txgraph: give Clusters a range of intended tx counts (preparation) --- src/txgraph.cpp | 57 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index a09754abb2..f66b470779 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -147,6 +147,10 @@ public: m_quality == QualityLevel::NEEDS_SPLIT_ACCEPTABLE; } + /** Get the smallest number of transactions this Cluster is intended for. */ + virtual DepGraphIndex GetMinIntendedTxCount() const noexcept = 0; + /** Get the maximum number of transactions this Cluster supports. */ + virtual DepGraphIndex GetMaxTxCount() const noexcept = 0; /** Total memory usage currently for this Cluster, including all its dynamic memory, plus Cluster * structure itself, and ClusterSet::m_clusters entry. */ virtual size_t TotalMemoryUsage() const noexcept = 0; @@ -246,11 +250,18 @@ class GenericClusterImpl final : public Cluster std::vector m_linearization; public: + /** The smallest number of transactions this Cluster implementation is intended for. */ + static constexpr DepGraphIndex MIN_INTENDED_TX_COUNT{1}; + /** The largest number of transactions this Cluster implementation supports. */ + static constexpr DepGraphIndex MAX_TX_COUNT{SetType::Size()}; + GenericClusterImpl() noexcept = delete; /** Construct an empty GenericClusterImpl. */ explicit GenericClusterImpl(uint64_t sequence) noexcept; size_t TotalMemoryUsage() const noexcept final; + constexpr DepGraphIndex GetMinIntendedTxCount() const noexcept final { return MIN_INTENDED_TX_COUNT; } + constexpr DepGraphIndex GetMaxTxCount() const noexcept final { return MAX_TX_COUNT; } DepGraphIndex GetDepGraphIndexRange() const noexcept final { return m_depgraph.PositionRange(); } LinearizationIndex GetTxCount() const noexcept final { return m_linearization.size(); } uint64_t GetTotalTxSize() const noexcept final; @@ -587,8 +598,11 @@ public: * count. */ std::unique_ptr CreateEmptyCluster(DepGraphIndex tx_count) noexcept { - (void)tx_count; - return CreateEmptyGenericCluster(); + if (tx_count >= GenericClusterImpl::MIN_INTENDED_TX_COUNT && tx_count <= GenericClusterImpl::MAX_TX_COUNT) { + return CreateEmptyGenericCluster(); + } + assert(false); + return {}; } // Functions for handling Refs. @@ -1135,10 +1149,12 @@ bool GenericClusterImpl::Split(TxGraphImpl& graph, int level) noexcept auto component = m_depgraph.FindConnectedComponent(todo); auto component_size = component.Count(); auto split_quality = component_size <= 2 ? QualityLevel::OPTIMAL : new_quality; - if (first && component == todo && SetType::Fill(component_size) == component) { + if (first && component == todo && SetType::Fill(component_size) == component && component_size >= MIN_INTENDED_TX_COUNT) { // The existing Cluster is an entire component, without holes. Leave it be, but update // its quality. If there are holes, we continue, so that the Cluster is reconstructed - // without holes, reducing memory usage. + // without holes, reducing memory usage. If the component's size is below the intended + // transaction count for this Cluster implementation, continue so that it can get + // converted. Assume(todo == m_depgraph.Positions()); graph.SetClusterQuality(level, m_quality, m_setindex, split_quality); // If this made the quality ACCEPTABLE or OPTIMAL, we need to compute and cache its @@ -1740,9 +1756,11 @@ void TxGraphImpl::Merge(std::span to_merge, int level) noexcept size_t max_size_pos{0}; DepGraphIndex max_size = to_merge[max_size_pos]->GetTxCount(); GetClusterSet(level).m_cluster_usage -= to_merge[max_size_pos]->TotalMemoryUsage(); + DepGraphIndex total_size = max_size; for (size_t i = 1; i < to_merge.size(); ++i) { GetClusterSet(level).m_cluster_usage -= to_merge[i]->TotalMemoryUsage(); DepGraphIndex size = to_merge[i]->GetTxCount(); + total_size += size; if (size > max_size) { max_size_pos = i; max_size = size; @@ -1750,13 +1768,26 @@ void TxGraphImpl::Merge(std::span to_merge, int level) noexcept } if (max_size_pos != 0) std::swap(to_merge[0], to_merge[max_size_pos]); - // Merge all further Clusters in the group into the first one, and delete them. - for (size_t i = 1; i < to_merge.size(); ++i) { - to_merge[0]->Merge(*this, level, *to_merge[i]); + size_t start_idx = 1; + Cluster* into_cluster = to_merge[0]; + if (total_size > into_cluster->GetMaxTxCount()) { + // The into_merge cluster is too small to fit all transactions being merged. Construct a + // a new Cluster using an implementation that matches the total size, and merge everything + // in there. + auto new_cluster = CreateEmptyCluster(total_size); + into_cluster = new_cluster.get(); + InsertCluster(level, std::move(new_cluster), QualityLevel::OPTIMAL); + start_idx = 0; + } + + // Merge all further Clusters in the group into the result (first one, or new one), and delete + // them. + for (size_t i = start_idx; i < to_merge.size(); ++i) { + into_cluster->Merge(*this, level, *to_merge[i]); DeleteCluster(*to_merge[i], level); } - to_merge[0]->Compact(); - GetClusterSet(level).m_cluster_usage += to_merge[0]->TotalMemoryUsage(); + into_cluster->Compact(); + GetClusterSet(level).m_cluster_usage += into_cluster->TotalMemoryUsage(); } void TxGraphImpl::ApplyDependencies(int level) noexcept @@ -2546,6 +2577,14 @@ void TxGraphImpl::SanityCheck() const assert(cluster.IsOversized() || cluster.GetTotalTxSize() <= m_max_cluster_size); // OVERSIZED clusters are singletons. assert(!cluster.IsOversized() || cluster.GetTxCount() == 1); + // Transaction counts cannot exceed the Cluster implementation's maximum + // supported transactions count. + assert(cluster.GetTxCount() <= cluster.GetMaxTxCount()); + // Unless a Split is yet to be applied, the number of transactions must not be + // below the Cluster implementation's intended transaction count. + if (!cluster.NeedsSplitting()) { + assert(cluster.GetTxCount() >= cluster.GetMinIntendedTxCount()); + } // Check the sequence number. assert(cluster.m_sequence < m_next_sequence_counter); From 023cd5a5469ad61205bf7bb1135895f2b4a20ea9 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 5 Sep 2025 16:06:35 -0400 Subject: [PATCH 13/13] txgraph: add SingletonClusterImpl (mem optimization) This adds a specialized Cluster implementation for singleton clusters, saving a significant amount of memory by avoiding the need for m_depgraph, m_mapping, and m_linearization, and their overheads. --- src/txgraph.cpp | 344 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 341 insertions(+), 3 deletions(-) diff --git a/src/txgraph.cpp b/src/txgraph.cpp index f66b470779..015bdd3458 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -183,7 +183,7 @@ public: /** Mark all the Entry objects belonging to this staging Cluster as missing. The Cluster must be * deleted immediately after. */ virtual void MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept = 0; - /** Remove all transactions from a Cluster. */ + /** Remove all transactions from a (non-empty) Cluster. */ virtual void Clear(TxGraphImpl& graph, int level) noexcept = 0; /** Change a Cluster's level from 1 (staging) to 0 (main). */ virtual void MoveToMain(TxGraphImpl& graph) noexcept = 0; @@ -251,7 +251,7 @@ class GenericClusterImpl final : public Cluster public: /** The smallest number of transactions this Cluster implementation is intended for. */ - static constexpr DepGraphIndex MIN_INTENDED_TX_COUNT{1}; + static constexpr DepGraphIndex MIN_INTENDED_TX_COUNT{2}; /** The largest number of transactions this Cluster implementation supports. */ static constexpr DepGraphIndex MAX_TX_COUNT{SetType::Size()}; @@ -293,6 +293,61 @@ public: void SanityCheck(const TxGraphImpl& graph, int level) const final; }; +/** An implementation of Cluster that only supports 1 transaction. */ +class SingletonClusterImpl final : public Cluster +{ + friend class TxGraphImpl; + + /** The feerate of the (singular) transaction in this Cluster. */ + FeePerWeight m_feerate; + /** Constant to indicate that this Cluster is empty. */ + static constexpr auto NO_GRAPH_INDEX = GraphIndex(-1); + /** The GraphIndex of the transaction. NO_GRAPH_INDEX if this Cluster is empty. */ + GraphIndex m_graph_index = NO_GRAPH_INDEX; + +public: + /** The smallest number of transactions this Cluster implementation is intended for. */ + static constexpr DepGraphIndex MIN_INTENDED_TX_COUNT{1}; + /** The largest number of transactions this Cluster implementation supports. */ + static constexpr DepGraphIndex MAX_TX_COUNT{1}; + + SingletonClusterImpl() noexcept = delete; + /** Construct an empty SingletonClusterImpl. */ + explicit SingletonClusterImpl(uint64_t sequence) noexcept : Cluster(sequence) {} + + size_t TotalMemoryUsage() const noexcept final; + constexpr DepGraphIndex GetMinIntendedTxCount() const noexcept final { return MIN_INTENDED_TX_COUNT; } + constexpr DepGraphIndex GetMaxTxCount() const noexcept final { return MAX_TX_COUNT; } + LinearizationIndex GetTxCount() const noexcept final { return m_graph_index != NO_GRAPH_INDEX; } + DepGraphIndex GetDepGraphIndexRange() const noexcept final { return GetTxCount(); } + uint64_t GetTotalTxSize() const noexcept final { return GetTxCount() ? m_feerate.size : 0; } + GraphIndex GetClusterEntry(DepGraphIndex index) const noexcept final { Assume(index == 0); Assume(GetTxCount()); return m_graph_index; } + DepGraphIndex AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept final; + void AddDependencies(SetType parents, DepGraphIndex child) noexcept final; + void ExtractTransactions(const std::function& visit_fn) noexcept final; + int GetLevel(const TxGraphImpl& graph) const noexcept final; + void UpdateMapping(DepGraphIndex cluster_idx, GraphIndex graph_idx) noexcept final { Assume(cluster_idx == 0); m_graph_index = graph_idx; } + void Updated(TxGraphImpl& graph, int level) noexcept final; + Cluster* CopyToStaging(TxGraphImpl& graph) const noexcept final; + void GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept final; + void MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept final; + void Clear(TxGraphImpl& graph, int level) noexcept final; + void MoveToMain(TxGraphImpl& graph) noexcept final; + void Compact() noexcept final; + void ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept final; + [[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> to_apply) noexcept final; + std::pair Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept final; + void AppendChunkFeerates(std::vector& ret) const noexcept final; + uint64_t AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept final; + void GetAncestorRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept final; + void GetDescendantRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept final; + bool GetClusterRefs(TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept final; + FeePerWeight GetIndividualFeerate(DepGraphIndex idx) noexcept final; + void SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept final; + void SanityCheck(const TxGraphImpl& graph, int level) const final; +}; /** The transaction graph, including staged changes. * @@ -320,6 +375,7 @@ public: class TxGraphImpl final : public TxGraph { friend class Cluster; + friend class SingletonClusterImpl; friend class GenericClusterImpl; friend class BlockBuilderImpl; private: @@ -594,10 +650,18 @@ public: { return std::make_unique(m_next_sequence_counter++); } + /** Create an empty SingletonClusterImpl object. */ + std::unique_ptr CreateEmptySingletonCluster() noexcept + { + return std::make_unique(m_next_sequence_counter++); + } /** Create an empty Cluster of the appropriate implementation for the specified (maximum) tx * count. */ std::unique_ptr CreateEmptyCluster(DepGraphIndex tx_count) noexcept { + if (tx_count >= SingletonClusterImpl::MIN_INTENDED_TX_COUNT && tx_count <= SingletonClusterImpl::MAX_TX_COUNT) { + return CreateEmptySingletonCluster(); + } if (tx_count >= GenericClusterImpl::MIN_INTENDED_TX_COUNT && tx_count <= GenericClusterImpl::MAX_TX_COUNT) { return CreateEmptyGenericCluster(); } @@ -805,6 +869,14 @@ size_t GenericClusterImpl::TotalMemoryUsage() const noexcept sizeof(std::unique_ptr); } +size_t SingletonClusterImpl::TotalMemoryUsage() const noexcept +{ + return // Memory usage of the allocated SingletonClusterImpl itself. + memusage::MallocUsage(sizeof(SingletonClusterImpl)) + + // Memory usage of the ClusterSet::m_clusters entry. + sizeof(std::unique_ptr); +} + uint64_t GenericClusterImpl::GetTotalTxSize() const noexcept { uint64_t ret{0}; @@ -823,11 +895,26 @@ DepGraphIndex GenericClusterImpl::AppendTransaction(GraphIndex graph_idx, FeePer return ret; } +DepGraphIndex SingletonClusterImpl::AppendTransaction(GraphIndex graph_idx, FeePerWeight feerate) noexcept +{ + Assume(!GetTxCount()); + m_graph_index = graph_idx; + m_feerate = feerate; + return 0; +} + void GenericClusterImpl::AddDependencies(SetType parents, DepGraphIndex child) noexcept { m_depgraph.AddDependencies(parents, child); } +void SingletonClusterImpl::AddDependencies(SetType parents, DepGraphIndex child) noexcept +{ + // Singletons cannot have any dependencies. + Assume(child == 0); + Assume(parents == SetType{} || parents == SetType::Fill(0)); +} + void GenericClusterImpl::ExtractTransactions(const std::function& visit_fn) noexcept { for (auto pos : m_linearization) { @@ -839,6 +926,14 @@ void GenericClusterImpl::ExtractTransactions(const std::function& visit_fn) noexcept +{ + if (GetTxCount()) { + visit_fn(0, m_graph_index, m_feerate, SetType{}); + m_graph_index = NO_GRAPH_INDEX; + } +} + int GenericClusterImpl::GetLevel(const TxGraphImpl& graph) const noexcept { // GetLevel() does not work for empty Clusters. @@ -856,6 +951,23 @@ int GenericClusterImpl::GetLevel(const TxGraphImpl& graph) const noexcept return -1; } +int SingletonClusterImpl::GetLevel(const TxGraphImpl& graph) const noexcept +{ + // GetLevel() does not work for empty Clusters. + if (!Assume(GetTxCount())) return -1; + + // Get the Entry in this Cluster. + const auto& entry = graph.m_entries[m_graph_index]; + // See if there is a level whose Locator matches this Cluster, if so return that level. + for (int level = 0; level < MAX_LEVELS; ++level) { + if (entry.m_locator[level].cluster == this) return level; + } + // Given that we started with an Entry that occurs in this Cluster, one of its Locators must + // point back to it. + assert(false); + return -1; +} + void TxGraphImpl::ClearLocator(int level, GraphIndex idx, bool oversized_tx) noexcept { auto& entry = m_entries[idx]; @@ -931,6 +1043,27 @@ void GenericClusterImpl::Updated(TxGraphImpl& graph, int level) noexcept } } +void SingletonClusterImpl::Updated(TxGraphImpl& graph, int level) noexcept +{ + // Don't do anything if this is empty. + if (GetTxCount() == 0) return; + + auto& entry = graph.m_entries[m_graph_index]; + // Discard any potential ChunkData prior to modifying the Cluster (as that could + // invalidate its ordering). + if (level == 0) graph.ClearChunkData(entry); + entry.m_locator[level].SetPresent(this, 0); + // If this is for the main graph (level = 0), compute its chunking and store its information in + // the Entry's m_main_lin_index and m_main_chunk_feerate. + if (level == 0 && IsAcceptable()) { + entry.m_main_lin_index = 0; + entry.m_main_chunk_feerate = m_feerate; + // Always use the special LinearizationIndex(-1), indicating singleton chunk at end of + // Cluster, here. + graph.CreateChunkData(m_graph_index, LinearizationIndex(-1)); + } +} + void GenericClusterImpl::GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept { for (auto i : m_linearization) { @@ -943,6 +1076,19 @@ void GenericClusterImpl::GetConflicts(const TxGraphImpl& graph, std::vector& out) const noexcept +{ + // Empty clusters have no conflicts. + if (GetTxCount() == 0) return; + + auto& entry = graph.m_entries[m_graph_index]; + // If the transaction in this Cluster also exists in a lower-level Cluster, then that Cluster + // conflicts. + if (entry.m_locator[0].IsPresent()) { + out.push_back(entry.m_locator[0].cluster); + } +} + std::vector TxGraphImpl::GetConflicts() const noexcept { Assume(GetTopLevel() == 1); @@ -973,7 +1119,7 @@ Cluster* GenericClusterImpl::CopyToStaging(TxGraphImpl& graph) const noexcept // Construct an empty Cluster. auto ret = graph.CreateEmptyGenericCluster(); auto ptr = ret.get(); - // Copy depgraph, mapping, and linearization/ + // Copy depgraph, mapping, and linearization. ptr->m_depgraph = m_depgraph; ptr->m_mapping = m_mapping; ptr->m_linearization = m_linearization; @@ -986,6 +1132,23 @@ Cluster* GenericClusterImpl::CopyToStaging(TxGraphImpl& graph) const noexcept return ptr; } +Cluster* SingletonClusterImpl::CopyToStaging(TxGraphImpl& graph) const noexcept +{ + // Construct an empty Cluster. + auto ret = graph.CreateEmptySingletonCluster(); + auto ptr = ret.get(); + // Copy data. + ptr->m_graph_index = m_graph_index; + ptr->m_feerate = m_feerate; + // Insert the new Cluster into the graph. + graph.InsertCluster(/*level=*/1, std::move(ret), m_quality); + // Update its Locators. + ptr->Updated(graph, /*level=*/1); + // Update memory usage. + graph.GetClusterSet(/*level=*/1).m_cluster_usage += ptr->TotalMemoryUsage(); + return ptr; +} + void GenericClusterImpl::ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept { // Iterate over the prefix of to_remove that applies to this cluster. @@ -1048,8 +1211,28 @@ void GenericClusterImpl::ApplyRemovals(TxGraphImpl& graph, int level, std::span< Updated(graph, level); } +void SingletonClusterImpl::ApplyRemovals(TxGraphImpl& graph, int level, std::span& to_remove) noexcept +{ + // We can only remove the one transaction this Cluster has. + Assume(!to_remove.empty()); + Assume(GetTxCount()); + Assume(to_remove.front() == m_graph_index); + // Pop all copies of m_graph_index from the front of to_remove (at least one, but there may be + // multiple). + do { + to_remove = to_remove.subspan(1); + } while (!to_remove.empty() && to_remove.front() == m_graph_index); + // Clear this cluster. + graph.ClearLocator(level, m_graph_index, m_quality == QualityLevel::OVERSIZED_SINGLETON); + m_graph_index = NO_GRAPH_INDEX; + graph.SetClusterQuality(level, m_quality, m_setindex, QualityLevel::NEEDS_SPLIT); + // No need to account for m_cluster_usage changes here, as SingletonClusterImpl has constant + // memory usage. +} + void GenericClusterImpl::Clear(TxGraphImpl& graph, int level) noexcept { + Assume(GetTxCount()); graph.GetClusterSet(level).m_cluster_usage -= TotalMemoryUsage(); for (auto i : m_linearization) { graph.ClearLocator(level, m_mapping[i], m_quality == QualityLevel::OVERSIZED_SINGLETON); @@ -1059,6 +1242,14 @@ void GenericClusterImpl::Clear(TxGraphImpl& graph, int level) noexcept m_mapping.clear(); } +void SingletonClusterImpl::Clear(TxGraphImpl& graph, int level) noexcept +{ + Assume(GetTxCount()); + graph.GetClusterSet(level).m_cluster_usage -= TotalMemoryUsage(); + graph.ClearLocator(level, m_graph_index, m_quality == QualityLevel::OVERSIZED_SINGLETON); + m_graph_index = NO_GRAPH_INDEX; +} + void GenericClusterImpl::MoveToMain(TxGraphImpl& graph) noexcept { for (auto i : m_linearization) { @@ -1076,6 +1267,20 @@ void GenericClusterImpl::MoveToMain(TxGraphImpl& graph) noexcept Updated(graph, /*level=*/0); } +void SingletonClusterImpl::MoveToMain(TxGraphImpl& graph) noexcept +{ + if (GetTxCount()) { + auto& entry = graph.m_entries[m_graph_index]; + entry.m_locator[1].SetMissing(); + } + auto quality = m_quality; + graph.GetClusterSet(/*level=*/1).m_cluster_usage -= TotalMemoryUsage(); + auto cluster = graph.ExtractCluster(/*level=*/1, quality, m_setindex); + graph.InsertCluster(/*level=*/0, std::move(cluster), quality); + graph.GetClusterSet(/*level=*/0).m_cluster_usage += TotalMemoryUsage(); + Updated(graph, /*level=*/0); +} + void GenericClusterImpl::Compact() noexcept { m_linearization.shrink_to_fit(); @@ -1083,6 +1288,11 @@ void GenericClusterImpl::Compact() noexcept m_depgraph.Compact(); } +void SingletonClusterImpl::Compact() noexcept +{ + // Nothing to compact; SingletonClusterImpl is constant size. +} + void GenericClusterImpl::AppendChunkFeerates(std::vector& ret) const noexcept { auto chunk_feerates = ChunkLinearization(m_depgraph, m_linearization); @@ -1090,6 +1300,13 @@ void GenericClusterImpl::AppendChunkFeerates(std::vector& ret) const no ret.insert(ret.end(), chunk_feerates.begin(), chunk_feerates.end()); } +void SingletonClusterImpl::AppendChunkFeerates(std::vector& ret) const noexcept +{ + if (GetTxCount()) { + ret.push_back(m_feerate); + } +} + uint64_t GenericClusterImpl::AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept { const LinearizationChunking linchunking(m_depgraph, m_linearization); @@ -1121,6 +1338,16 @@ uint64_t GenericClusterImpl::AppendTrimData(std::vector& ret, std::v return size; } +uint64_t SingletonClusterImpl::AppendTrimData(std::vector& ret, std::vector>& deps) const noexcept +{ + if (!GetTxCount()) return 0; + auto& entry = ret.emplace_back(); + entry.m_chunk_feerate = m_feerate; + entry.m_index = m_graph_index; + entry.m_tx_size = m_feerate.size; + return m_feerate.size; +} + bool GenericClusterImpl::Split(TxGraphImpl& graph, int level) noexcept { // This function can only be called when the Cluster needs splitting. @@ -1205,6 +1432,21 @@ bool GenericClusterImpl::Split(TxGraphImpl& graph, int level) noexcept return true; } +bool SingletonClusterImpl::Split(TxGraphImpl& graph, int level) noexcept +{ + Assume(NeedsSplitting()); + if (GetTxCount() == 0) { + // The cluster is now empty. + graph.GetClusterSet(level).m_cluster_usage -= TotalMemoryUsage(); + return true; + } else { + // Nothing changed. + graph.SetClusterQuality(level, m_quality, m_setindex, QualityLevel::OPTIMAL); + Updated(graph, level); + return false; + } +} + void GenericClusterImpl::Merge(TxGraphImpl& graph, int level, Cluster& other) noexcept { /** Vector to store the positions in this Cluster for each position in other. */ @@ -1238,6 +1480,13 @@ void GenericClusterImpl::Merge(TxGraphImpl& graph, int level, Cluster& other) no }); } +void SingletonClusterImpl::Merge(TxGraphImpl& graph, int level, Cluster& other_abstract) noexcept +{ + // Nothing can be merged into a singleton; it should have been converted to GenericClusterImpl + // first. + Assume(false); +} + void GenericClusterImpl::ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) noexcept { // This function is invoked by TxGraphImpl::ApplyDependencies after merging groups of Clusters @@ -1286,6 +1535,15 @@ void GenericClusterImpl::ApplyDependencies(TxGraphImpl& graph, int level, std::s Updated(graph, level); } +void SingletonClusterImpl::ApplyDependencies(TxGraphImpl& graph, int level, std::span> to_apply) noexcept +{ + // Nothing can actually be applied. + for (auto& [par, chl] : to_apply) { + Assume(par == m_graph_index); + Assume(chl == m_graph_index); + } +} + TxGraphImpl::~TxGraphImpl() noexcept { // If Refs outlive the TxGraphImpl they refer to, unlink them, so that their destructor does not @@ -1861,6 +2119,14 @@ std::pair GenericClusterImpl::Relinearize(TxGraphImpl& graph, in return {cost, improved}; } +std::pair SingletonClusterImpl::Relinearize(TxGraphImpl& graph, int level, uint64_t max_iters) noexcept +{ + // All singletons are optimal, oversized, or need splitting. Each of these precludes + // Relinearize from being called. + assert(false); + return {0, false}; +} + void TxGraphImpl::MakeAcceptable(Cluster& cluster, int level) noexcept { // Relinearize the Cluster if needed. @@ -1990,6 +2256,18 @@ void GenericClusterImpl::GetAncestorRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept +{ + Assume(GetTxCount()); + while (!args.empty()) { + if (args.front().first != this) break; + args = args.subspan(1); + } + const auto& entry = graph.m_entries[m_graph_index]; + Assume(entry.m_ref != nullptr); + output.push_back(entry.m_ref); +} + void GenericClusterImpl::GetDescendantRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept { /** The union of all descendants to be returned. */ @@ -2009,6 +2287,12 @@ void GenericClusterImpl::GetDescendantRefs(const TxGraphImpl& graph, std::span>& args, std::vector& output) noexcept +{ + // In a singleton cluster, the ancestors or descendants are always just the entire cluster. + GetAncestorRefs(graph, args, output); +} + bool GenericClusterImpl::GetClusterRefs(TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept { // Translate the transactions in the Cluster (in linearization order, starting at start_pos in @@ -2023,11 +2307,29 @@ bool GenericClusterImpl::GetClusterRefs(TxGraphImpl& graph, std::span range, LinearizationIndex start_pos) noexcept +{ + Assume(!range.empty()); + Assume(GetTxCount()); + Assume(start_pos == 0); + const auto& entry = graph.m_entries[m_graph_index]; + Assume(entry.m_ref != nullptr); + range[0] = entry.m_ref; + return true; +} + FeePerWeight GenericClusterImpl::GetIndividualFeerate(DepGraphIndex idx) noexcept { return FeePerWeight::FromFeeFrac(m_depgraph.FeeRate(idx)); } +FeePerWeight SingletonClusterImpl::GetIndividualFeerate(DepGraphIndex idx) noexcept +{ + Assume(GetTxCount()); + Assume(idx == 0); + return m_feerate; +} + void GenericClusterImpl::MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept { // Mark all transactions of a Cluster missing, needed when aborting staging, so that the @@ -2039,6 +2341,14 @@ void GenericClusterImpl::MakeStagingTransactionsMissing(TxGraphImpl& graph) noex } } +void SingletonClusterImpl::MakeStagingTransactionsMissing(TxGraphImpl& graph) noexcept +{ + if (GetTxCount()) { + auto& entry = graph.m_entries[m_graph_index]; + entry.m_locator[1].SetMissing(); + } +} + std::vector TxGraphImpl::GetAncestors(const Ref& arg, Level level_select) noexcept { // Return the empty vector if the Ref is empty. @@ -2347,6 +2657,14 @@ void GenericClusterImpl::SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx Updated(graph, level); } +void SingletonClusterImpl::SetFee(TxGraphImpl& graph, int level, DepGraphIndex idx, int64_t fee) noexcept +{ + Assume(GetTxCount()); + Assume(idx == 0); + m_feerate.fee = fee; + Updated(graph, level); +} + void TxGraphImpl::SetTransactionFee(const Ref& ref, int64_t fee) noexcept { // Don't do anything if the passed Ref is empty. @@ -2494,6 +2812,26 @@ void GenericClusterImpl::SanityCheck(const TxGraphImpl& graph, int level) const assert(m_done == m_depgraph.Positions()); } +void SingletonClusterImpl::SanityCheck(const TxGraphImpl& graph, int level) const +{ + // All singletons are optimal, oversized, or need splitting. + Assume(IsOptimal() || IsOversized() || NeedsSplitting()); + if (GetTxCount()) { + const auto& entry = graph.m_entries[m_graph_index]; + // Check that the Entry has a locator pointing back to this Cluster & position within it. + assert(entry.m_locator[level].cluster == this); + assert(entry.m_locator[level].index == 0); + // For main-level entries, check linearization position and chunk feerate. + if (level == 0 && IsAcceptable()) { + assert(entry.m_main_lin_index == 0); + assert(entry.m_main_chunk_feerate == m_feerate); + assert(entry.m_main_chunkindex_iterator != graph.m_main_chunkindex.end()); + auto& chunk_data = *entry.m_main_chunkindex_iterator; + assert(chunk_data.m_chunk_count == LinearizationIndex(-1)); + } + } +} + void TxGraphImpl::SanityCheck() const { /** Which GraphIndexes ought to occur in m_unlinked, based on m_entries. */