Skip to content

Commit b2df21b

Browse files
committed
Merge #17925: Improve UpdateTransactionsFromBlock with Epochs
bd5a026 Make UpdateTransactionsFromBlock use Epochs (Jeremy Rubin) 2ccb7cc Add Epoch Guards to CTXMemPoolEntry and CTxMemPool (Jeremy Rubin) Pull request description: UpdateTransactionsFromBlock is called during a re-org. When a re-org occurs, all of the transactions in the mempool may be descendants from a transaction which is in the pre-reorg block. This can cause us to propagate updates, worst case, to every transaction in the mempool. Because we construct a `setEntries setChildren`, which is backed by a `std::set`, it is possible that this algorithm is `O(N log N)`. By using an Epoch visitor pattern, we can limit this to `O(N)` worst case behavior. Epochs are also less resource intensive than almost any set option (e.g., hash set) because they are allocation free. This PR is related to bitcoin/bitcoin#17268, it is a small subset of the changes which have been refactored slightly to ease review. If this PR gets review & merge, I will follow up with more PRs (similar to #17268) to improve the mempool ACKs for top commit: sdaftuar: ACK bd5a026 adamjonas: Just to summarize for those looking to review - as of bd5a026 there are 3 ACKs (@sdaftuar, @ariard, and @hebasto) and one "looks good" from @ajtowns with no NACKs or any show-stopping concerns raised. ajtowns: ACK bd5a026 (code review) ariard: Code review ACK bd5a026 hebasto: ACK bd5a026, modulo some nits and a typo. Tree-SHA512: f0d2291085019ffb4e1119edeb9f4a89c1a572d1cb5b4bdf5743dd0152e721e1935f5155dcae84e1e5bda5ffdf6224c488c1e200bd33bedca9f5ca22d5f5139f
2 parents 365c83e + bd5a026 commit b2df21b

File tree

2 files changed

+87
-14
lines changed

2 files changed

+87
-14
lines changed

src/txmempool.cpp

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ CTxMemPoolEntry::CTxMemPoolEntry(const CTransactionRef& _tx, const CAmount& _nFe
2323
int64_t _nTime, unsigned int _entryHeight,
2424
bool _spendsCoinbase, int64_t _sigOpsCost, LockPoints lp)
2525
: tx(_tx), nFee(_nFee), nTxWeight(GetTransactionWeight(*tx)), nUsageSize(RecursiveDynamicUsage(tx)), nTime(_nTime), entryHeight(_entryHeight),
26-
spendsCoinbase(_spendsCoinbase), sigOpCost(_sigOpsCost), lockPoints(lp)
26+
spendsCoinbase(_spendsCoinbase), sigOpCost(_sigOpsCost), lockPoints(lp), m_epoch(0)
2727
{
2828
nCountWithDescendants = 1;
2929
nSizeWithDescendants = GetTxSize();
@@ -122,8 +122,6 @@ void CTxMemPool::UpdateTransactionsFromBlock(const std::vector<uint256> &vHashes
122122
// setMemPoolChildren will be updated, an assumption made in
123123
// UpdateForDescendants.
124124
for (const uint256 &hash : reverse_iterate(vHashesToUpdate)) {
125-
// we cache the in-mempool children to avoid duplicate updates
126-
setEntries setChildren;
127125
// calculate children from mapNextTx
128126
txiter it = mapTx.find(hash);
129127
if (it == mapTx.end()) {
@@ -132,17 +130,21 @@ void CTxMemPool::UpdateTransactionsFromBlock(const std::vector<uint256> &vHashes
132130
auto iter = mapNextTx.lower_bound(COutPoint(hash, 0));
133131
// First calculate the children, and update setMemPoolChildren to
134132
// include them, and update their setMemPoolParents to include this tx.
135-
for (; iter != mapNextTx.end() && iter->first->hash == hash; ++iter) {
136-
const uint256 &childHash = iter->second->GetHash();
137-
txiter childIter = mapTx.find(childHash);
138-
assert(childIter != mapTx.end());
139-
// We can skip updating entries we've encountered before or that
140-
// are in the block (which are already accounted for).
141-
if (setChildren.insert(childIter).second && !setAlreadyIncluded.count(childHash)) {
142-
UpdateChild(it, childIter, true);
143-
UpdateParent(childIter, it, true);
133+
// we cache the in-mempool children to avoid duplicate updates
134+
{
135+
const auto epoch = GetFreshEpoch();
136+
for (; iter != mapNextTx.end() && iter->first->hash == hash; ++iter) {
137+
const uint256 &childHash = iter->second->GetHash();
138+
txiter childIter = mapTx.find(childHash);
139+
assert(childIter != mapTx.end());
140+
// We can skip updating entries we've encountered before or that
141+
// are in the block (which are already accounted for).
142+
if (!visited(childIter) && !setAlreadyIncluded.count(childHash)) {
143+
UpdateChild(it, childIter, true);
144+
UpdateParent(childIter, it, true);
145+
}
144146
}
145-
}
147+
} // release epoch guard for UpdateForDescendants
146148
UpdateForDescendants(it, mapMemPoolDescendantsToUpdate, setAlreadyIncluded);
147149
}
148150
}
@@ -325,7 +327,7 @@ void CTxMemPoolEntry::UpdateAncestorState(int64_t modifySize, CAmount modifyFee,
325327
}
326328

327329
CTxMemPool::CTxMemPool(CBlockPolicyEstimator* estimator)
328-
: nTransactionsUpdated(0), minerPolicyEstimator(estimator)
330+
: nTransactionsUpdated(0), minerPolicyEstimator(estimator), m_epoch(0), m_has_epoch_guard(false)
329331
{
330332
_clear(); //lock free clear
331333

@@ -1105,4 +1107,23 @@ void CTxMemPool::SetIsLoaded(bool loaded)
11051107
m_is_loaded = loaded;
11061108
}
11071109

1110+
1111+
CTxMemPool::EpochGuard CTxMemPool::GetFreshEpoch() const
1112+
{
1113+
return EpochGuard(*this);
1114+
}
1115+
CTxMemPool::EpochGuard::EpochGuard(const CTxMemPool& in) : pool(in)
1116+
{
1117+
assert(!pool.m_has_epoch_guard);
1118+
++pool.m_epoch;
1119+
pool.m_has_epoch_guard = true;
1120+
}
1121+
1122+
CTxMemPool::EpochGuard::~EpochGuard()
1123+
{
1124+
// prevents stale results being used
1125+
++pool.m_epoch;
1126+
pool.m_has_epoch_guard = false;
1127+
}
1128+
11081129
SaltedTxidHasher::SaltedTxidHasher() : k0(GetRand(std::numeric_limits<uint64_t>::max())), k1(GetRand(std::numeric_limits<uint64_t>::max())) {}

src/txmempool.h

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ class CTxMemPoolEntry
129129
int64_t GetSigOpCostWithAncestors() const { return nSigOpCostWithAncestors; }
130130

131131
mutable size_t vTxHashesIdx; //!< Index in mempool's vTxHashes
132+
mutable uint64_t m_epoch; //!< epoch when last touched, useful for graph algorithms
132133
};
133134

134135
// Helpers for modifying CTxMemPool::mapTx, which is a boost multi_index.
@@ -453,6 +454,8 @@ class CTxMemPool
453454
mutable int64_t lastRollingFeeUpdate;
454455
mutable bool blockSinceLastRollingFeeBump;
455456
mutable double rollingMinimumFeeRate; //!< minimum fee to get into the pool, decreases exponentially
457+
mutable uint64_t m_epoch;
458+
mutable bool m_has_epoch_guard;
456459

457460
void trackPackageRemoved(const CFeeRate& rate) EXCLUSIVE_LOCKS_REQUIRED(cs);
458461

@@ -736,6 +739,55 @@ class CTxMemPool
736739
* removal.
737740
*/
738741
void removeUnchecked(txiter entry, MemPoolRemovalReason reason) EXCLUSIVE_LOCKS_REQUIRED(cs);
742+
public:
743+
/** EpochGuard: RAII-style guard for using epoch-based graph traversal algorithms.
744+
* When walking ancestors or descendants, we generally want to avoid
745+
* visiting the same transactions twice. Some traversal algorithms use
746+
* std::set (or setEntries) to deduplicate the transaction we visit.
747+
* However, use of std::set is algorithmically undesirable because it both
748+
* adds an asymptotic factor of O(log n) to traverals cost and triggers O(n)
749+
* more dynamic memory allocations.
750+
* In many algorithms we can replace std::set with an internal mempool
751+
* counter to track the time (or, "epoch") that we began a traversal, and
752+
* check + update a per-transaction epoch for each transaction we look at to
753+
* determine if that transaction has not yet been visited during the current
754+
* traversal's epoch.
755+
* Algorithms using std::set can be replaced on a one by one basis.
756+
* Both techniques are not fundamentally incomaptible across the codebase.
757+
* Generally speaking, however, the remaining use of std::set for mempool
758+
* traversal should be viewed as a TODO for replacement with an epoch based
759+
* traversal, rather than a preference for std::set over epochs in that
760+
* algorithm.
761+
*/
762+
class EpochGuard {
763+
const CTxMemPool& pool;
764+
public:
765+
EpochGuard(const CTxMemPool& in);
766+
~EpochGuard();
767+
};
768+
// N.B. GetFreshEpoch modifies mutable state via the EpochGuard construction
769+
// (and later destruction)
770+
EpochGuard GetFreshEpoch() const EXCLUSIVE_LOCKS_REQUIRED(cs);
771+
772+
/** visited marks a CTxMemPoolEntry as having been traversed
773+
* during the lifetime of the most recently created EpochGuard
774+
* and returns false if we are the first visitor, true otherwise.
775+
*
776+
* An EpochGuard must be held when visited is called or an assert will be
777+
* triggered.
778+
*
779+
*/
780+
bool visited(txiter it) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
781+
assert(m_has_epoch_guard);
782+
bool ret = it->m_epoch >= m_epoch;
783+
it->m_epoch = std::max(it->m_epoch, m_epoch);
784+
return ret;
785+
}
786+
787+
bool visited(Optional<txiter> it) const EXCLUSIVE_LOCKS_REQUIRED(cs) {
788+
assert(m_has_epoch_guard);
789+
return !it || visited(*it);
790+
}
739791
};
740792

741793
/**

0 commit comments

Comments
 (0)