Commit ade65e7
perf(mempool): make insertion cost independent of mempool size
Replacing the priority index was not enough. `Mempool::insert_entry` called
`recompute_all_metadata`, which walked every entry, and then `total_vsize()`,
which folded every entry again. With the index already fixed, insertion still
measured an exponent of 2.17 -- 1.057 s to fill 3,200 transactions.
Three changes remove what was left.
The metadata refresh is incremental. Linking one transaction into the spend
graph changes package totals for its transitive ancestors, itself, and its
transitive descendants, and for nothing else: an entry outside that set gains no
new ancestor because it is not a descendant of the seed, and gains no new
descendant because every new path runs through the seed, which would have put it
among the seed's ancestors. `insert_entry` and `remove_entries` now recompute
exactly that closure, whose size is bounded by the ancestor and descendant
policy limits rather than by the pool.
The closure is taken after the entry enters the spend indexes on an insertion,
because a transaction can arrive after something that already spends its
outputs; and before the removal on a removal, because a removed entry's
ancestors cannot be walked once it is gone.
`total_vsize` and `aggregate_fees` are running sums. Both were folds over the
whole pool, and `insert_entry` consults `total_vsize()` on every acceptance, so
that fold alone made insertion quadratic.
Measured end to end, against the same fixture as the previous commit:
200 tx 2.572 ms -> 373.0 us 6.9x
800 tx 51.27 ms -> 1.760 ms 29.1x
3,200 tx 1.057 s -> 7.079 ms 149.3x
12,800 tx unmeasurable -> 37.87 ms
51,200 tx unmeasurable -> 211.5 ms
Per transaction 1.87 us -> 4.13 us across 256x the entries; measured exponent
1.24 over the final leg, which is n log n for the fill.
`prioritise` had a defect this fixes on the way. It applied its fee delta to
each descendant's `ancestor_fee` by hand and never reindexed those descendants,
so a descendant kept the priority key it had before its ancestor was bumped --
defeating `prioritisetransaction` for exactly the packages it aims at. The three
delta loops are now one call to the same refresh an insertion does.
`recompute_all_metadata` is retained under `cfg(test)` as the oracle. Nothing in
the pool reaches it, so production cannot drift away from it.
Nine mutations, all killed on their named tests. Two survived the first pass:
`aggregate_fees` guards itself with a debug_assert, but a guard only fires when
something calls it, and no test called it after a removal or a fee bump. Both
paths could lose their bookkeeping silently. Covered now, and compared against
an independent fold so the check survives a release build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent 5a78f96 commit ade65e7
3 files changed
Lines changed: 509 additions & 57 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
43 | 46 | | |
44 | 47 | | |
45 | 48 | | |
| |||
0 commit comments