@@ -475,6 +475,7 @@ pub(crate) struct ShrinkCollect<'a, T: ShrinkCollectRefs<'a>> {
475
475
pub(crate) slot: Slot,
476
476
pub(crate) capacity: u64,
477
477
pub(crate) unrefed_pubkeys: Vec<&'a Pubkey>,
478
+ pub(crate) zero_lamport_single_ref_pubkeys: Vec<&'a Pubkey>,
478
479
pub(crate) alive_accounts: T,
479
480
/// total size in storage of all alive accounts
480
481
pub(crate) alive_total_bytes: usize,
@@ -524,6 +525,8 @@ struct LoadAccountsIndexForShrink<'a, T: ShrinkCollectRefs<'a>> {
524
525
alive_accounts: T,
525
526
/// pubkeys that were unref'd in the accounts index because they were dead
526
527
unrefed_pubkeys: Vec<&'a Pubkey>,
528
+ /// pubkeys that are the last remaining zero lamport instance of an account
529
+ zero_lamport_single_ref_pubkeys: Vec<&'a Pubkey>,
527
530
/// true if all alive accounts are zero lamport accounts
528
531
all_are_zero_lamports: bool,
529
532
/// index entries we need to hold onto to keep them from getting flushed
@@ -2011,6 +2014,7 @@ pub struct ShrinkStats {
2011
2014
dead_accounts: AtomicU64,
2012
2015
alive_accounts: AtomicU64,
2013
2016
accounts_loaded: AtomicU64,
2017
+ purged_zero_lamports: AtomicU64,
2014
2018
}
2015
2019
2016
2020
impl ShrinkStats {
@@ -2109,6 +2113,11 @@ impl ShrinkStats {
2109
2113
self.accounts_loaded.swap(0, Ordering::Relaxed) as i64,
2110
2114
i64
2111
2115
),
2116
+ (
2117
+ "purged_zero_lamports_count",
2118
+ self.purged_zero_lamports.swap(0, Ordering::Relaxed),
2119
+ i64
2120
+ ),
2112
2121
);
2113
2122
}
2114
2123
}
@@ -2309,6 +2318,13 @@ impl ShrinkAncientStats {
2309
2318
self.many_refs_old_alive.swap(0, Ordering::Relaxed),
2310
2319
i64
2311
2320
),
2321
+ (
2322
+ "purged_zero_lamports_count",
2323
+ self.shrink_stats
2324
+ .purged_zero_lamports
2325
+ .swap(0, Ordering::Relaxed),
2326
+ i64
2327
+ ),
2312
2328
);
2313
2329
}
2314
2330
}
@@ -3793,12 +3809,14 @@ impl AccountsDb {
3793
3809
let count = accounts.len();
3794
3810
let mut alive_accounts = T::with_capacity(count, slot_to_shrink);
3795
3811
let mut unrefed_pubkeys = Vec::with_capacity(count);
3812
+ let mut zero_lamport_single_ref_pubkeys = Vec::with_capacity(count);
3796
3813
3797
3814
let mut alive = 0;
3798
3815
let mut dead = 0;
3799
3816
let mut index = 0;
3800
3817
let mut all_are_zero_lamports = true;
3801
3818
let mut index_entries_being_shrunk = Vec::with_capacity(accounts.len());
3819
+ let latest_full_snapshot_slot = self.latest_full_snapshot_slot();
3802
3820
self.accounts_index.scan(
3803
3821
accounts.iter().map(|account| account.pubkey()),
3804
3822
|pubkey, slots_refs, entry| {
@@ -3817,6 +3835,21 @@ impl AccountsDb {
3817
3835
unrefed_pubkeys.push(pubkey);
3818
3836
result = AccountsIndexScanResult::Unref;
3819
3837
dead += 1;
3838
+ } else if stored_account.is_zero_lamport()
3839
+ && ref_count == 1
3840
+ && latest_full_snapshot_slot
3841
+ .map(|latest_full_snapshot_slot| {
3842
+ latest_full_snapshot_slot >= slot_to_shrink
3843
+ })
3844
+ .unwrap_or(true)
3845
+ {
3846
+ // only do this if our slot is prior to the latest full snapshot
3847
+ // we found a zero lamport account that is the only instance of this account. We can delete it completely.
3848
+ zero_lamport_single_ref_pubkeys.push(pubkey);
3849
+ self.add_uncleaned_pubkeys_after_shrink(
3850
+ slot_to_shrink,
3851
+ [*pubkey].into_iter(),
3852
+ );
3820
3853
} else {
3821
3854
// Hold onto the index entry arc so that it cannot be flushed.
3822
3855
// Since we are shrinking these entries, we need to disambiguate storage ids during this period and those only exist in the in-memory accounts index.
@@ -3839,6 +3872,7 @@ impl AccountsDb {
3839
3872
LoadAccountsIndexForShrink {
3840
3873
alive_accounts,
3841
3874
unrefed_pubkeys,
3875
+ zero_lamport_single_ref_pubkeys,
3842
3876
all_are_zero_lamports,
3843
3877
index_entries_being_shrunk,
3844
3878
}
@@ -3939,6 +3973,7 @@ impl AccountsDb {
3939
3973
let len = stored_accounts.len();
3940
3974
let alive_accounts_collect = Mutex::new(T::with_capacity(len, slot));
3941
3975
let unrefed_pubkeys_collect = Mutex::new(Vec::with_capacity(len));
3976
+ let zero_lamport_single_ref_pubkeys_collect = Mutex::new(Vec::with_capacity(len));
3942
3977
stats
3943
3978
.accounts_loaded
3944
3979
.fetch_add(len as u64, Ordering::Relaxed);
@@ -3955,6 +3990,7 @@ impl AccountsDb {
3955
3990
alive_accounts,
3956
3991
mut unrefed_pubkeys,
3957
3992
all_are_zero_lamports,
3993
+ mut zero_lamport_single_ref_pubkeys,
3958
3994
mut index_entries_being_shrunk,
3959
3995
} = self.load_accounts_index_for_shrink(stored_accounts, stats, slot);
3960
3996
@@ -3967,6 +4003,10 @@ impl AccountsDb {
3967
4003
.lock()
3968
4004
.unwrap()
3969
4005
.append(&mut unrefed_pubkeys);
4006
+ zero_lamport_single_ref_pubkeys_collect
4007
+ .lock()
4008
+ .unwrap()
4009
+ .append(&mut zero_lamport_single_ref_pubkeys);
3970
4010
index_entries_being_shrunk_outer
3971
4011
.lock()
3972
4012
.unwrap()
@@ -3979,6 +4019,9 @@ impl AccountsDb {
3979
4019
3980
4020
let alive_accounts = alive_accounts_collect.into_inner().unwrap();
3981
4021
let unrefed_pubkeys = unrefed_pubkeys_collect.into_inner().unwrap();
4022
+ let zero_lamport_single_ref_pubkeys = zero_lamport_single_ref_pubkeys_collect
4023
+ .into_inner()
4024
+ .unwrap();
3982
4025
3983
4026
index_read_elapsed.stop();
3984
4027
stats
@@ -4002,6 +4045,7 @@ impl AccountsDb {
4002
4045
slot,
4003
4046
capacity: *capacity,
4004
4047
unrefed_pubkeys,
4048
+ zero_lamport_single_ref_pubkeys,
4005
4049
alive_accounts,
4006
4050
alive_total_bytes,
4007
4051
total_starting_accounts: len,
@@ -4010,6 +4054,41 @@ impl AccountsDb {
4010
4054
}
4011
4055
}
4012
4056
4057
+ /// These accounts were found during shrink of `slot` to be slot_list=[slot] and ref_count == 1 and lamports = 0.
4058
+ /// This means this slot contained the only account data for this pubkey and it is zero lamport.
4059
+ /// Thus, we did NOT treat this as an alive account, so we did NOT copy the zero lamport account to the new
4060
+ /// storage. So, the account will no longer be alive or exist at `slot`.
4061
+ /// So, first, remove the ref count since this newly shrunk storage will no longer access it.
4062
+ /// Second, remove `slot` from the index entry's slot list. If the slot list is now empty, then the
4063
+ /// pubkey can be removed completely from the index.
4064
+ /// In parallel with this code (which is running in the bg), the same pubkey could be revived and written to
4065
+ /// as part of tx processing. In that case, the slot list will contain a slot in the write cache and the
4066
+ /// index entry will NOT be deleted.
4067
+ fn remove_zero_lamport_single_ref_accounts_after_shrink(
4068
+ &self,
4069
+ zero_lamport_single_ref_pubkeys: &[&Pubkey],
4070
+ slot: Slot,
4071
+ stats: &ShrinkStats,
4072
+ ) {
4073
+ stats.purged_zero_lamports.fetch_add(
4074
+ zero_lamport_single_ref_pubkeys.len() as u64,
4075
+ Ordering::Relaxed,
4076
+ );
4077
+
4078
+ // we have to unref before we `purge_keys_exact`. Otherwise, we could race with the foreground with tx processing
4079
+ // reviving this index entry and then we'd unref the revived version, which is a refcount bug.
4080
+ self.accounts_index.scan(
4081
+ zero_lamport_single_ref_pubkeys.iter().cloned(),
4082
+ |_pubkey, _slots_refs, _entry| AccountsIndexScanResult::Unref,
4083
+ Some(AccountsIndexScanResult::Unref),
4084
+ false,
4085
+ );
4086
+
4087
+ zero_lamport_single_ref_pubkeys.iter().for_each(|k| {
4088
+ _ = self.purge_keys_exact([&(**k, slot)].into_iter());
4089
+ });
4090
+ }
4091
+
4013
4092
/// common code from shrink and combine_ancient_slots
4014
4093
/// get rid of all original store_ids in the slot
4015
4094
pub(crate) fn remove_old_stores_shrink<'a, T: ShrinkCollectRefs<'a>>(
@@ -4020,7 +4099,19 @@ impl AccountsDb {
4020
4099
shrink_can_be_active: bool,
4021
4100
) {
4022
4101
let mut time = Measure::start("remove_old_stores_shrink");
4102
+
4103
+ // handle the zero lamport alive accounts before calling clean
4104
+ // We have to update the index entries for these zero lamport pubkeys before we remove the storage in `mark_dirty_dead_stores`
4105
+ // that contained the accounts.
4106
+ self.remove_zero_lamport_single_ref_accounts_after_shrink(
4107
+ &shrink_collect.zero_lamport_single_ref_pubkeys,
4108
+ shrink_collect.slot,
4109
+ stats,
4110
+ );
4111
+
4023
4112
// Purge old, overwritten storage entries
4113
+ // This has the side effect of dropping `shrink_in_progress`, which removes the old storage completely. The
4114
+ // index has to be correct before we drop the old storage.
4024
4115
let dead_storages = self.mark_dirty_dead_stores(
4025
4116
shrink_collect.slot,
4026
4117
// If all accounts are zero lamports, then we want to mark the entire OLD append vec as dirty.
@@ -10884,6 +10975,146 @@ pub mod tests {
10884
10975
assert_eq!(accounts.alive_account_count_in_slot(1), 0);
10885
10976
}
10886
10977
10978
+ #[test]
10979
+ fn test_remove_zero_lamport_single_ref_accounts_after_shrink() {
10980
+ for pass in 0..3 {
10981
+ let accounts = AccountsDb::new_single_for_tests();
10982
+ let pubkey_zero = Pubkey::from([1; 32]);
10983
+ let pubkey2 = Pubkey::from([2; 32]);
10984
+ let account = AccountSharedData::new(1, 0, AccountSharedData::default().owner());
10985
+ let zero_lamport_account =
10986
+ AccountSharedData::new(0, 0, AccountSharedData::default().owner());
10987
+ let slot = 1;
10988
+
10989
+ accounts.store_for_tests(
10990
+ slot,
10991
+ &[(&pubkey_zero, &zero_lamport_account), (&pubkey2, &account)],
10992
+ );
10993
+
10994
+ // Simulate rooting the zero-lamport account, writes it to storage
10995
+ accounts.calculate_accounts_delta_hash(slot);
10996
+ accounts.add_root_and_flush_write_cache(slot);
10997
+
10998
+ if pass > 0 {
10999
+ // store in write cache
11000
+ accounts.store_for_tests(slot + 1, &[(&pubkey_zero, &zero_lamport_account)]);
11001
+ if pass == 2 {
11002
+ // move to a storage (causing ref count to increase)
11003
+ accounts.calculate_accounts_delta_hash(slot + 1);
11004
+ accounts.add_root_and_flush_write_cache(slot + 1);
11005
+ }
11006
+ }
11007
+
11008
+ accounts.accounts_index.get_and_then(&pubkey_zero, |entry| {
11009
+ let expected_ref_count = if pass < 2 { 1 } else { 2 };
11010
+ assert_eq!(entry.unwrap().ref_count(), expected_ref_count, "{pass}");
11011
+ let expected_slot_list = if pass < 1 { 1 } else { 2 };
11012
+ assert_eq!(
11013
+ entry.unwrap().slot_list.read().unwrap().len(),
11014
+ expected_slot_list
11015
+ );
11016
+ (false, ())
11017
+ });
11018
+ accounts.accounts_index.get_and_then(&pubkey2, |entry| {
11019
+ assert!(entry.is_some());
11020
+ (false, ())
11021
+ });
11022
+
11023
+ let zero_lamport_single_ref_pubkeys = [&pubkey_zero];
11024
+ accounts.remove_zero_lamport_single_ref_accounts_after_shrink(
11025
+ &zero_lamport_single_ref_pubkeys,
11026
+ slot,
11027
+ &ShrinkStats::default(),
11028
+ );
11029
+
11030
+ accounts.accounts_index.get_and_then(&pubkey_zero, |entry| {
11031
+ if pass == 0 {
11032
+ // should not exist in index at all
11033
+ assert!(entry.is_none(), "{pass}");
11034
+ } else {
11035
+ // alive only in slot + 1
11036
+ assert_eq!(entry.unwrap().slot_list.read().unwrap().len(), 1);
11037
+ assert_eq!(
11038
+ entry
11039
+ .unwrap()
11040
+ .slot_list
11041
+ .read()
11042
+ .unwrap()
11043
+ .first()
11044
+ .map(|(s, _)| s)
11045
+ .cloned()
11046
+ .unwrap(),
11047
+ slot + 1
11048
+ );
11049
+ // refcount = 1 if we flushed the write cache for slot + 1
11050
+ let expected_ref_count = if pass < 2 { 0 } else { 1 };
11051
+ assert_eq!(
11052
+ entry.map(|e| e.ref_count()),
11053
+ Some(expected_ref_count),
11054
+ "{pass}"
11055
+ );
11056
+ }
11057
+ (false, ())
11058
+ });
11059
+
11060
+ accounts.accounts_index.get_and_then(&pubkey2, |entry| {
11061
+ assert!(entry.is_some(), "{pass}");
11062
+ (false, ())
11063
+ });
11064
+ }
11065
+ }
11066
+
11067
+ #[test]
11068
+ fn test_shrink_zero_lamport_single_ref_account() {
11069
+ solana_logger::setup();
11070
+
11071
+ // store a zero and non-zero lamport account
11072
+ // make sure clean marks the ref_count=1, zero lamport account dead and removes pubkey from index completely
11073
+ let accounts = AccountsDb::new_single_for_tests();
11074
+ let pubkey_zero = Pubkey::from([1; 32]);
11075
+ let pubkey2 = Pubkey::from([2; 32]);
11076
+ let account = AccountSharedData::new(1, 0, AccountSharedData::default().owner());
11077
+ let zero_lamport_account =
11078
+ AccountSharedData::new(0, 0, AccountSharedData::default().owner());
11079
+
11080
+ // Store a zero-lamport account and a non-zero lamport account
11081
+ accounts.store_for_tests(
11082
+ 1,
11083
+ &[(&pubkey_zero, &zero_lamport_account), (&pubkey2, &account)],
11084
+ );
11085
+
11086
+ // Simulate rooting the zero-lamport account, should be a
11087
+ // candidate for cleaning
11088
+ accounts.calculate_accounts_delta_hash(1);
11089
+ accounts.add_root_and_flush_write_cache(1);
11090
+
11091
+ // for testing, we need to cause shrink to think this will be productive.
11092
+ // The zero lamport account isn't dead, but it can become dead inside shrink.
11093
+ accounts
11094
+ .storage
11095
+ .get_slot_storage_entry(1)
11096
+ .unwrap()
11097
+ .alive_bytes
11098
+ .fetch_sub(aligned_stored_size(0), Ordering::Relaxed);
11099
+
11100
+ // Slot 1 should be cleaned, but
11101
+ // zero-lamport account should not be cleaned since last full snapshot root is before slot 1
11102
+ accounts.shrink_slot_forced(1);
11103
+
11104
+ assert!(accounts.storage.get_slot_storage_entry(1).is_some());
11105
+
11106
+ // the zero lamport account should be marked as dead
11107
+ assert_eq!(accounts.alive_account_count_in_slot(1), 1);
11108
+
11109
+ // zero lamport account should be dead in the index
11110
+ assert!(!accounts
11111
+ .accounts_index
11112
+ .contains_with(&pubkey_zero, None, None));
11113
+ // other account should still be alive
11114
+ assert!(accounts.accounts_index.contains_with(&pubkey2, None, None));
11115
+ assert!(accounts.storage.get_slot_storage_entry(1).is_some());
11116
+ }
11117
+
10887
11118
#[test]
10888
11119
fn test_clean_multiple_zero_lamport_decrements_index_ref_count() {
10889
11120
solana_logger::setup();
@@ -15910,6 +16141,8 @@ pub mod tests {
15910
16141
debug!("space: {space}, lamports: {lamports}, alive: {alive}, account_count: {account_count}, append_opposite_alive_account: {append_opposite_alive_account}, append_opposite_zero_lamport_account: {append_opposite_zero_lamport_account}, normal_account_count: {normal_account_count}");
15911
16142
let db = AccountsDb::new_single_for_tests();
15912
16143
let slot5 = 5;
16144
+ // don't do special zero lamport account handling
16145
+ db.set_latest_full_snapshot_slot(0);
15913
16146
let mut account = AccountSharedData::new(
15914
16147
lamports,
15915
16148
space,
0 commit comments