Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
Do not evict unloaded programs (#31465)
Browse files Browse the repository at this point in the history
* Do not evict unloaded programs

* cleanup
  • Loading branch information
pgarg66 authored May 4, 2023
1 parent 22d4c6a commit 96e170b
Show file tree
Hide file tree
Showing 2 changed files with 18 additions and 137 deletions.
151 changes: 16 additions & 135 deletions program-runtime/src/loaded_programs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use {
pubkey::Pubkey, saturating_add_assign,
},
std::{
cmp,
collections::HashMap,
fmt::{Debug, Formatter},
sync::{
Expand All @@ -26,7 +25,6 @@ use {
};

const MAX_LOADED_ENTRY_COUNT: usize = 256;
const MAX_UNLOADED_ENTRY_COUNT: usize = 1024;

/// Relationship between two fork IDs
#[derive(Copy, Clone, PartialEq)]
Expand Down Expand Up @@ -444,63 +442,33 @@ impl LoadedPrograms {
)
}

/// Evicts programs which were used infrequently
pub fn sort_and_evict(&mut self, shrink_to: PercentageInteger) {
let mut num_loaded: usize = 0;
let mut num_unloaded: usize = 0;
// Find eviction candidates and sort by their type and usage counters.
// Sorted result will have the following order:
// Loaded entries with ascending order of their usage count
// Unloaded entries with ascending order of their usage count
let (ordering, sorted_candidates): (Vec<u32>, Vec<(Pubkey, Arc<LoadedProgram>)>) = self
/// Unloads programs which were used infrequently
pub fn sort_and_unload(&mut self, shrink_to: PercentageInteger) {
let sorted_candidates: Vec<(Pubkey, Arc<LoadedProgram>)> = self
.entries
.iter()
.flat_map(|(id, list)| {
list.iter()
.filter_map(move |program| match program.program {
LoadedProgramType::LegacyV0(_)
| LoadedProgramType::LegacyV1(_)
| LoadedProgramType::Typed(_) => Some((0, (*id, program.clone()))),
| LoadedProgramType::Typed(_) => Some((*id, program.clone())),
#[cfg(test)]
LoadedProgramType::TestLoaded => Some((0, (*id, program.clone()))),
LoadedProgramType::Unloaded => Some((1, (*id, program.clone()))),
LoadedProgramType::FailedVerification
LoadedProgramType::TestLoaded => Some((*id, program.clone())),
LoadedProgramType::Unloaded
| LoadedProgramType::FailedVerification
| LoadedProgramType::Closed
| LoadedProgramType::DelayVisibility
| LoadedProgramType::Builtin(_, _) => None,
})
})
.sorted_by_cached_key(|(order, (_id, program))| {
(*order, program.usage_counter.load(Ordering::Relaxed))
})
.unzip();

for order in ordering {
match order {
0 => num_loaded = num_loaded.saturating_add(1),
1 => num_unloaded = num_unloaded.saturating_add(1),
_ => unreachable!(),
}
}
.sorted_by_cached_key(|(_id, program)| program.usage_counter.load(Ordering::Relaxed))
.collect();

let num_to_unload = num_loaded.saturating_sub(shrink_to.apply_to(MAX_LOADED_ENTRY_COUNT));
let num_to_unload = sorted_candidates
.len()
.saturating_sub(shrink_to.apply_to(MAX_LOADED_ENTRY_COUNT));
self.unload_program_entries(sorted_candidates.iter().take(num_to_unload));

let num_unloaded_to_evict = num_unloaded
.saturating_add(num_to_unload)
.saturating_sub(shrink_to.apply_to(MAX_UNLOADED_ENTRY_COUNT));
let (newly_unloaded_programs, sorted_candidates) = sorted_candidates.split_at(num_loaded);
let num_old_unloaded_to_evict = cmp::min(sorted_candidates.len(), num_unloaded_to_evict);
self.remove_program_entries(sorted_candidates.iter().take(num_old_unloaded_to_evict));

let num_newly_unloaded_to_evict =
num_unloaded_to_evict.saturating_sub(sorted_candidates.len());
self.remove_program_entries(
newly_unloaded_programs
.iter()
.take(num_newly_unloaded_to_evict),
);

self.remove_programs_with_no_entries();
}

Expand All @@ -511,20 +479,6 @@ impl LoadedPrograms {
}
}

fn remove_program_entries<'a>(
&mut self,
remove: impl Iterator<Item = &'a (Pubkey, Arc<LoadedProgram>)>,
) {
for (id, program) in remove {
if let Some(entries) = self.entries.get_mut(id) {
let index = entries.iter().position(|entry| entry == program);
if let Some(index) = index {
entries.swap_remove(index);
}
}
}
}

fn remove_expired_entries(&mut self, current_slot: Slot) {
for entry in self.entries.values_mut() {
entry.retain(|program| {
Expand Down Expand Up @@ -759,9 +713,9 @@ mod tests {

// Evicting to 2% should update cache with
// * 5 active entries
// * 20 unloaded entries
// * 33 unloaded entries (3 active programs will get unloaded)
// * 30 tombstones (tombstones are not evicted)
cache.sort_and_evict(Percentage::from(2));
cache.sort_and_unload(Percentage::from(2));
// Check that every program is still in the cache.
programs.iter().for_each(|entry| {
assert!(cache.entries.get(&entry.0).is_some());
Expand Down Expand Up @@ -799,83 +753,10 @@ mod tests {
});

assert_eq!(num_loaded, 5);
assert_eq!(num_unloaded, 20);
assert_eq!(num_unloaded, 33);
assert_eq!(num_tombstones, 30);
}

#[test]
fn test_eviction_unload_underflow() {
// Test: Eviction of unloaded programs requires eviction of newly unloaded programs.
// 1. Load 26 programs
// 2. Insert 1 unloaded program
// Eviction will unload 21 programs.
// 2 unloaded programs need to be evicted. So 1 old and 1 new unloaded program will be evicted.

let mut cache = LoadedPrograms::default();

let program1 = Pubkey::new_unique();
let num_total_programs = 26;
(0..num_total_programs).for_each(|i| {
cache.replenish(
program1,
new_test_loaded_program_with_usage(i, i + 2, AtomicU64::new(i)),
);
});

let program2 = Pubkey::new_unique();
insert_unloaded_program(&mut cache, program2, 26);

let num_loaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, LoadedProgramType::TestLoaded)
});
let num_unloaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, LoadedProgramType::Unloaded)
});
let num_tombstones = num_matching_entries(&cache, |program_type| {
matches!(
program_type,
LoadedProgramType::DelayVisibility
| LoadedProgramType::FailedVerification
| LoadedProgramType::Closed
)
});

assert_eq!(num_loaded, 26);
assert_eq!(num_unloaded, 1);
assert_eq!(num_tombstones, 0);

// Test that program2 exists in the cache. It'll get removed after eviction.
assert!(cache.entries.get(&program2).is_some());

// Evicting to 2% should update cache with
// * 5 active entries
// * 20 unloaded entries
// * 0 tombstones
cache.sort_and_evict(Percentage::from(2));

let num_loaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, LoadedProgramType::TestLoaded)
});
let num_unloaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, LoadedProgramType::Unloaded)
});
let num_tombstones = num_matching_entries(&cache, |program_type| {
matches!(
program_type,
LoadedProgramType::DelayVisibility
| LoadedProgramType::FailedVerification
| LoadedProgramType::Closed
)
});

assert_eq!(num_loaded, 5);
assert_eq!(num_unloaded, 20);
assert_eq!(num_tombstones, 0);

// Test that program2 has been removed after eviction.
assert!(cache.entries.get(&program2).is_none());
}

#[test]
fn test_usage_count_of_unloaded_program() {
let mut cache = LoadedPrograms::default();
Expand All @@ -890,7 +771,7 @@ mod tests {
});

// This will unload the program deployed at slot 0, with usage count = 10
cache.sort_and_evict(Percentage::from(2));
cache.sort_and_unload(Percentage::from(2));

let num_unloaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, LoadedProgramType::Unloaded)
Expand Down
4 changes: 2 additions & 2 deletions runtime/src/bank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4714,11 +4714,11 @@ impl Bank {

execution_time.stop();

const EVICT_CACHE_TO_PERCENTAGE: u8 = 90;
const SHRINK_LOADED_PROGRAMS_TO_PERCENTAGE: u8 = 90;
self.loaded_programs_cache
.write()
.unwrap()
.sort_and_evict(Percentage::from(EVICT_CACHE_TO_PERCENTAGE));
.sort_and_unload(Percentage::from(SHRINK_LOADED_PROGRAMS_TO_PERCENTAGE));

debug!(
"check: {}us load: {}us execute: {}us txs_len={}",
Expand Down

0 comments on commit 96e170b

Please sign in to comment.