@@ -124,7 +124,7 @@ pub struct MempoolStore {
124124 // and doesn't free the memory when an item is removed - it's only replaced with a tombstone.
125125 // Since TxMempoolEntry is relatively big (size_of = 350+ bytes), we'd waste a noticeable
126126 // amount of memory without boxing.)
127- pub txs_by_id : TrackedHashMap < Id < Transaction > , Tracked < Box < TxMempoolEntry > , StrictDropPolicy > > ,
127+ txs_by_id : TrackedHashMap < Id < Transaction > , Tracked < Box < TxMempoolEntry > , StrictDropPolicy > > ,
128128
129129 // Mempool entries sorted by descendant score.
130130 // We keep this index so that when the mempool grows full, we know which transactions are the
@@ -136,22 +136,22 @@ pub struct MempoolStore {
136136 // max(fee/size of entry's tx, fee/size with all descendants).
137137 // TODO if we wish to follow Bitcoin Core, "size" is not simply the encoded size, but
138138 // rather a value that takes into account witness and sigop data (see CTxMemPoolEntry::GetTxSize).
139- pub txs_by_descendant_score : TrackedTxIdMultiMap < DescendantScore > ,
139+ txs_by_descendant_score : TrackedTxIdMultiMap < DescendantScore > ,
140140
141141 // Mempool entries sorted by ancestor score.
142142 // This is used to select the most economically attractive transactions for block production.
143143 // The ancestor score of an entry is defined as
144144 // min(fee/size of entry's tx, fee/size with all ancestors).
145- pub txs_by_ancestor_score : TrackedTxIdMultiMap < AncestorScore > ,
145+ txs_by_ancestor_score : TrackedTxIdMultiMap < AncestorScore > ,
146146
147147 // Entries that have remained in the mempool for a long time (see DEFAULT_MEMPOOL_EXPIRY) are
148148 // evicted. To efficiently know which entries to evict, we store the mempool entries sorted by
149149 // their creation time, from earliest to latest.
150- pub txs_by_creation_time : TrackedTxIdMultiMap < Time > ,
150+ txs_by_creation_time : TrackedTxIdMultiMap < Time > ,
151151
152152 // We keep the information of which inputs are spent by entries currently in the mempool.
153153 // This allows us to recognize conflicts (double-spends) and handle them
154- pub spender_txs : Tracked < BTreeMap < TxDependency , Id < Transaction > > > ,
154+ spender_txs : Tracked < BTreeMap < TxDependency , Id < Transaction > > > ,
155155
156156 // Track transactions by internal unique sequence number. This is used to recover the order in
157157 // which the transactions have been inserted into the mempool, so they can be re-inserted in
@@ -243,6 +243,29 @@ impl MempoolStore {
243243 self . mem_tracker . get_usage ( )
244244 }
245245
246+ pub fn txs_by_id (
247+ & self ,
248+ ) -> & TrackedHashMap < Id < Transaction > , Tracked < Box < TxMempoolEntry > , StrictDropPolicy > > {
249+ & self . txs_by_id
250+ }
251+
252+ pub fn txs_by_descendant_score ( & self ) -> & TrackedTxIdMultiMap < DescendantScore > {
253+ & self . txs_by_descendant_score
254+ }
255+
256+ pub fn txs_by_ancestor_score ( & self ) -> & TrackedTxIdMultiMap < AncestorScore > {
257+ & self . txs_by_ancestor_score
258+ }
259+
260+ pub fn txs_by_creation_time ( & self ) -> & TrackedTxIdMultiMap < Time > {
261+ & self . txs_by_creation_time
262+ }
263+
264+ #[ cfg( test) ]
265+ pub fn seq_nos_by_tx ( & self ) -> & TrackedHashMap < Id < Transaction > , usize > {
266+ & self . seq_nos_by_tx
267+ }
268+
246269 pub fn assert_valid ( & self ) {
247270 #[ cfg( test) ]
248271 self . assert_valid_inner ( )
@@ -663,8 +686,87 @@ impl MempoolStore {
663686 let entry = self . get_existing_entry ( tx_id) ?;
664687 entry. collect_cluster ( self )
665688 }
689+
690+ /// For internal containers that have capacity, check if the capacity is excessive; shrink
691+ /// the container if it is.
692+ pub fn shrink_capacity_if_needed ( & mut self ) {
693+ // Note:
694+ // * Hashbrown tables never shrink their capacity automatically.
695+ // * According to the pseudo-test `estimate_max_tx_count_in_store`, the store with the default
696+ // size of 300Mb can fit over 230'000 txs of the smallest possible size. Due to how hashbrown
697+ // tables work (1/8 of all buckets should always be empty, and reallocation doubles the number
698+ // of buckets), `txs_by_id` and `seq_nos_by_tx` may end up with more than 500'000 buckets each.
699+ // Given that the bucket size in each table is 40 bytes (in non-test builds), this results in
700+ // roughly 20Mb of allocated memory per table, which will not go down even if the tables'
701+ // element counts become zero. Since table's entire allocation_size counts towards the
702+ // mempool size, this will effectively reduce the max mempool size by 40Mb.
703+ // * On the other hand, the mempool re-creates its store completely every time a new block
704+ // arrives, so the situation described above can only exist for a few minutes. Still,
705+ // it's better for the store not to depend on such a behavior of its owner code and
706+ // manage the capacities explicitly.
707+
708+ // Implementation notes:
709+ // * table's `capacity` doesn't count the tombstones, so in a degenerate case like the one
710+ // described above it's possible to have a table with a huge allocation size and small
711+ // capacity. So below we don't use capacity when deciding whether to shrink, and estimate
712+ // (roughly) the number of buckets instead.
713+ // * even though `shrink_to` accepts capacity, it'll compare the estimated number of buckets
714+ // (from the passed capacity) with the current one and reallocate/rehash the table if the
715+ // latter is bigger.
716+
717+ fn maybe_shrink < K , V > (
718+ table : & mut TrackedHashMap < K , V > ,
719+ mem_tracker : & mut MemUsageTracker ,
720+ table_name : & str ,
721+ ) where
722+ K : Eq + std:: hash:: Hash ,
723+ {
724+ let bucket_size = hash_map_bucket_size ( table) ;
725+ let bucket_count = hash_map_bucket_count_upper_bound ( table) ;
726+
727+ let max_bucket_count = table. len ( ) * HASH_TABLE_MAX_BUCKET_COUNT_FACTOR ;
728+ let adjusted_capacity = table. len ( ) * HASH_TABLE_ADJUSTED_CAPACITY_FACTOR ;
729+
730+ if bucket_count > max_bucket_count {
731+ let potentially_reclaimable_mem_size =
732+ ( bucket_count - adjusted_capacity) * bucket_size;
733+
734+ // Only bother shrinking if the win is noticeable.
735+ if potentially_reclaimable_mem_size >= HASH_TABLE_MIN_RECLAIMABLE_MEM_SIZE {
736+ log:: debug!( "Shrinking {table_name} to {adjusted_capacity}" ) ;
737+ mem_tracker. modify ( table, |table, _| table. shrink_to ( adjusted_capacity) ) ;
738+ }
739+ }
740+ }
741+
742+ maybe_shrink ( & mut self . txs_by_id , & mut self . mem_tracker , "txs_by_id" ) ;
743+ maybe_shrink (
744+ & mut self . seq_nos_by_tx ,
745+ & mut self . mem_tracker ,
746+ "seq_nos_by_tx" ,
747+ ) ;
748+ }
749+ }
750+
751+ pub fn hash_map_bucket_size < K , V > ( _: & StoreHashMap < K , V > ) -> usize {
752+ std:: mem:: size_of :: < ( K , V ) > ( )
753+ }
754+
755+ // Return the upper bound for the number of buckets in the map.
756+ pub fn hash_map_bucket_count_upper_bound < K , V > ( map : & StoreHashMap < K , V > ) -> usize
757+ where
758+ K : Eq + std:: hash:: Hash ,
759+ {
760+ // Note: the actual number of buckets will be smaller than this, because `allocation_size` also
761+ // includes control bytes and padding.
762+ map. allocation_size ( ) / hash_map_bucket_size ( map)
666763}
667764
765+ // Constants that determine whether store's hash tables should be shrunk and, if yes, to what capacity.
766+ pub const HASH_TABLE_MAX_BUCKET_COUNT_FACTOR : usize = 5 ;
767+ pub const HASH_TABLE_ADJUSTED_CAPACITY_FACTOR : usize = 2 ;
768+ pub const HASH_TABLE_MIN_RECLAIMABLE_MEM_SIZE : usize = 10_000 ;
769+
668770#[ cfg( test) ]
669771impl Drop for MempoolStore {
670772 fn drop ( & mut self ) {
0 commit comments