use {
crate::{
invoke_context::InvokeContext,
loading_task::LoadingTaskWaiter,
program_cache_entry::{ProgramCacheEntry, ProgramCacheEntryType, retention_score},
program_metrics::{EMA_SCALE, ProgramCacheStats},
},
log::error,
percentage::PercentageInteger,
solana_clock::{Epoch, Slot},
solana_pubkey::Pubkey,
solana_sbpf::program::BuiltinProgram,
solana_svm_type_overrides::{
rand::{Rng, rng},
sync::{Arc, Mutex, RwLock, atomic::Ordering},
thread,
},
std::{
collections::{HashMap, hash_map::Entry},
sync::Weak,
},
};
#[repr(transparent)]
#[derive(Clone, Debug)]
pub struct ProgramRuntimeEnvironment(Arc<BuiltinProgram<InvokeContext<'static, 'static>>>);
impl std::hash::Hash for ProgramRuntimeEnvironment {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
Arc::<BuiltinProgram<InvokeContext<'static, 'static>>>::as_ptr(&self.0).hash(state);
}
}
impl PartialEq for ProgramRuntimeEnvironment {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Eq for ProgramRuntimeEnvironment {}
impl std::ops::Deref for ProgramRuntimeEnvironment {
type Target = Arc<BuiltinProgram<InvokeContext<'static, 'static>>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ProgramRuntimeEnvironment {
pub fn from(inner: BuiltinProgram<InvokeContext<'static, 'static>>) -> Self {
Self(Arc::new(inner))
}
pub const fn from_ref<'a>(
inner: &'a Arc<BuiltinProgram<InvokeContext<'static, 'static>>>,
) -> &'a Self {
unsafe { std::mem::transmute(inner) }
}
}
pub struct ProgramRuntimeEnvironments {
execution: ProgramRuntimeEnvironment,
deployment: ProgramRuntimeEnvironment,
}
impl ProgramRuntimeEnvironments {
pub fn new(
execution: ProgramRuntimeEnvironment,
deployment: ProgramRuntimeEnvironment,
) -> Self {
Self {
execution,
deployment,
}
}
pub fn get_env_for_execution(&self) -> &ProgramRuntimeEnvironment {
&self.execution
}
pub fn get_env_for_deployment(&self) -> &ProgramRuntimeEnvironment {
&self.deployment
}
#[cfg(feature = "dev-context-only-utils")]
pub fn mock() -> Self {
Self {
execution: get_mock_program_runtime_environment(),
deployment: get_mock_program_runtime_environment(),
}
}
}
#[cfg(feature = "dev-context-only-utils")]
pub fn get_mock_program_runtime_environment() -> ProgramRuntimeEnvironment {
static MOCK_ENVIRONMENT: std::sync::OnceLock<ProgramRuntimeEnvironment> =
std::sync::OnceLock::<ProgramRuntimeEnvironment>::new();
MOCK_ENVIRONMENT
.get_or_init(|| ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()))
.clone()
}
pub const MAX_LOADED_ENTRY_COUNT: usize = 512;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum BlockRelation {
Ancestor,
Equal,
Descendant,
Unrelated,
Unknown,
}
pub trait ForkGraph {
fn relationship(&self, a: Slot, b: Slot) -> BlockRelation;
}
#[derive(Debug, Default)]
pub struct EpochBoundaryPreparation {
pub upcoming_epoch: Epoch,
pub upcoming_environment: Option<ProgramRuntimeEnvironment>,
pub programs_to_recompile: Vec<(Pubkey, Arc<ProgramCacheEntry>)>,
}
impl EpochBoundaryPreparation {
pub fn new(epoch: Epoch) -> Self {
Self {
upcoming_epoch: epoch,
upcoming_environment: None,
programs_to_recompile: Vec::default(),
}
}
pub fn get_upcoming_environment_for_epoch(
&self,
epoch: Epoch,
) -> Option<ProgramRuntimeEnvironment> {
if epoch == self.upcoming_epoch {
return self.upcoming_environment.clone();
}
None
}
pub fn reroot(&mut self, epoch: Epoch) -> Option<ProgramRuntimeEnvironment> {
if epoch == self.upcoming_epoch
&& let Some(upcoming_environment) = self.upcoming_environment.take()
{
self.programs_to_recompile.clear();
return Some(upcoming_environment);
}
None
}
}
#[derive(Debug)]
pub(crate) enum IndexImplementation {
V1 {
entries: HashMap<Pubkey, Vec<Arc<ProgramCacheEntry>>>,
loading_entries: Mutex<HashMap<Pubkey, (Slot, thread::ThreadId)>>,
},
}
pub struct ProgramCache<FG: ForkGraph> {
pub(crate) index: IndexImplementation,
pub latest_root_slot: Slot,
pub stats: ProgramCacheStats,
pub fork_graph: Option<Weak<RwLock<FG>>>,
pub loading_task_waiter: Arc<LoadingTaskWaiter>,
}
impl<FG: ForkGraph> std::fmt::Debug for ProgramCache<FG> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProgramCache")
.field("root slot", &self.latest_root_slot)
.field("stats", &self.stats)
.field("index", &self.index)
.finish()
}
}
#[derive(Clone, Debug, Default)]
pub struct ProgramCacheForTxBatch {
entries: HashMap<Pubkey, Arc<ProgramCacheEntry>>,
modified_entries: HashMap<Pubkey, Arc<ProgramCacheEntry>>,
slot: Slot,
pub hit_max_limit: bool,
pub loaded_missing: bool,
pub merged_modified: bool,
}
impl ProgramCacheForTxBatch {
pub fn new(slot: Slot) -> Self {
Self {
entries: HashMap::new(),
modified_entries: HashMap::new(),
slot,
hit_max_limit: false,
loaded_missing: false,
merged_modified: false,
}
}
pub fn replenish(
&mut self,
key: Pubkey,
entry: Arc<ProgramCacheEntry>,
) -> (bool, Arc<ProgramCacheEntry>) {
(self.entries.insert(key, entry.clone()).is_some(), entry)
}
pub fn store_modified_entry(&mut self, key: Pubkey, entry: Arc<ProgramCacheEntry>) {
self.modified_entries.insert(key, entry);
}
pub fn drain_modified_entries(&mut self) -> HashMap<Pubkey, Arc<ProgramCacheEntry>> {
std::mem::take(&mut self.modified_entries)
}
pub fn find(&self, key: &Pubkey) -> Option<Arc<ProgramCacheEntry>> {
self.modified_entries
.get(key)
.or_else(|| self.entries.get(key))
.map(|entry| {
if entry.is_implicit_delay_visibility_tombstone(self.slot) {
Arc::new(ProgramCacheEntry::new_tombstone_with_stats(
entry.deployment_slot,
entry.account_owner,
ProgramCacheEntryType::DelayVisibility,
Arc::clone(&entry.stats),
))
} else {
entry.clone()
}
})
}
pub fn slot(&self) -> Slot {
self.slot
}
pub fn set_slot_for_tests(&mut self, slot: Slot) {
self.slot = slot;
}
pub fn merge(&mut self, modified_entries: &HashMap<Pubkey, Arc<ProgramCacheEntry>>) {
modified_entries.iter().for_each(|(key, entry)| {
self.merged_modified = true;
self.replenish(*key, entry.clone());
})
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
pub enum ProgramCacheMatchCriteria {
DeployedOnOrAfterSlot(Slot),
Tombstone,
NoCriteria,
}
impl<FG: ForkGraph> ProgramCache<FG> {
pub fn new(root_slot: Slot) -> Self {
Self {
index: IndexImplementation::V1 {
entries: HashMap::new(),
loading_entries: Mutex::new(HashMap::new()),
},
latest_root_slot: root_slot,
stats: ProgramCacheStats::default(),
fork_graph: None,
loading_task_waiter: Arc::new(LoadingTaskWaiter::default()),
}
}
pub fn set_fork_graph(&mut self, fork_graph: Weak<RwLock<FG>>) {
self.fork_graph = Some(fork_graph);
}
pub fn assign_program(
&mut self,
program_runtime_environment: &ProgramRuntimeEnvironment,
key: Pubkey,
_last_modification_slot: Slot,
entry: Arc<ProgramCacheEntry>,
) -> bool {
debug_assert!(!matches!(
&entry.program,
ProgramCacheEntryType::DelayVisibility
));
fn is_current_env(
program_runtime_environment: &ProgramRuntimeEnvironment,
env_opt: Option<&ProgramRuntimeEnvironment>,
) -> bool {
env_opt
.map(|env| env == program_runtime_environment)
.unwrap_or(true)
}
match &mut self.index {
IndexImplementation::V1 { entries, .. } => {
let slot_versions = &mut entries.entry(key).or_default();
let insertion_point = slot_versions.binary_search_by(|at| {
at.effective_slot
.cmp(&entry.effective_slot)
.then(at.deployment_slot.cmp(&entry.deployment_slot))
.then(
is_current_env(
program_runtime_environment,
at.program.get_environment(),
)
.cmp(&is_current_env(
program_runtime_environment,
entry.program.get_environment(),
)),
)
});
match insertion_point {
Ok(index) => {
let existing = slot_versions.get_mut(index).unwrap();
match (&existing.program, &entry.program) {
(
ProgramCacheEntryType::Builtin(_),
ProgramCacheEntryType::Builtin(_),
)
| (
ProgramCacheEntryType::Unloaded(_),
ProgramCacheEntryType::Loaded(_),
) => {}
(ProgramCacheEntryType::Closed, ProgramCacheEntryType::Closed)
if existing.account_owner != entry.account_owner => {}
_ => {
error!(
"ProgramCache::assign_program() failed key={key:?} \
existing={slot_versions:?} entry={entry:?}"
);
debug_assert!(false, "Unexpected replacement of an entry");
self.stats.replacements.fetch_add(1, Ordering::Relaxed);
return true;
}
}
entry.stats.merge_from(&existing.stats);
*existing = Arc::clone(&entry);
self.stats.reloads.fetch_add(1, Ordering::Relaxed);
}
Err(index) => {
self.stats.insertions.fetch_add(1, Ordering::Relaxed);
slot_versions.insert(index, Arc::clone(&entry));
}
}
slot_versions.retain(|existing| {
existing.deployment_slot != entry.deployment_slot
|| existing
.program
.get_environment()
.zip(entry.program.get_environment())
.map(|(a, b)| a != b)
.unwrap_or(false)
|| existing == &entry
});
}
}
false
}
pub fn prune_by_deployment_slot(&mut self, slot: Slot) {
match &mut self.index {
IndexImplementation::V1 { entries, .. } => {
for second_level in entries.values_mut() {
second_level.retain(|entry| entry.deployment_slot != slot);
}
self.remove_programs_with_no_entries();
}
}
}
pub fn prune(
&mut self,
new_root_slot: Slot,
upcoming_environment: Option<ProgramRuntimeEnvironment>,
fork_graph: &FG,
) {
match &mut self.index {
IndexImplementation::V1 { entries, .. } => {
for second_level in entries.values_mut() {
let mut first_ancestor_found = false;
let mut first_ancestor_env = None;
*second_level = second_level
.iter()
.rev()
.filter(|entry| {
let relation =
fork_graph.relationship(entry.deployment_slot, new_root_slot);
if entry.deployment_slot >= new_root_slot {
matches!(relation, BlockRelation::Equal | BlockRelation::Descendant)
} else if matches!(relation, BlockRelation::Ancestor)
|| entry.deployment_slot <= self.latest_root_slot
{
if !first_ancestor_found {
first_ancestor_found = true;
first_ancestor_env = entry.program.get_environment();
return true;
}
if let Some(entry_env) = entry.program.get_environment()
&& let Some(env) = first_ancestor_env
&& entry_env != env
{
return true;
}
self.stats.prunes_orphan.fetch_add(1, Ordering::Relaxed);
false
} else {
self.stats.prunes_orphan.fetch_add(1, Ordering::Relaxed);
false
}
})
.filter(|entry| {
if let Some(upcoming_environment) = upcoming_environment.as_ref()
&& !Self::matches_environment(entry, upcoming_environment)
{
self.stats
.prunes_environment
.fetch_add(1, Ordering::Relaxed);
return false;
}
true
})
.cloned()
.collect();
second_level.reverse();
}
}
}
self.remove_programs_with_no_entries();
debug_assert!(self.latest_root_slot <= new_root_slot);
self.latest_root_slot = new_root_slot;
}
fn matches_environment(
entry: &Arc<ProgramCacheEntry>,
program_runtime_environment: &ProgramRuntimeEnvironment,
) -> bool {
let Some(environment) = entry.program.get_environment() else {
return true;
};
environment == program_runtime_environment
}
fn matches_criteria(
program: &Arc<ProgramCacheEntry>,
criteria: &ProgramCacheMatchCriteria,
) -> bool {
match criteria {
ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(slot) => {
program.deployment_slot >= *slot
}
ProgramCacheMatchCriteria::Tombstone => program.is_tombstone(),
ProgramCacheMatchCriteria::NoCriteria => true,
}
}
pub fn extract(
&self,
search_for: &mut Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)>,
loaded_programs_for_tx_batch: &mut ProgramCacheForTxBatch,
program_runtime_environment_for_execution: &ProgramRuntimeEnvironment,
increment_usage_counter: bool,
count_hits_and_misses: bool,
) -> Option<Pubkey> {
debug_assert!(self.fork_graph.is_some());
let fork_graph = self.fork_graph.as_ref().unwrap().upgrade().unwrap();
let locked_fork_graph = fork_graph.read().unwrap();
let mut cooperative_loading_task = None;
match &self.index {
IndexImplementation::V1 {
entries,
loading_entries,
} => {
search_for.retain(|(key, match_criteria, _slot)| {
if let Some(second_level) = entries.get(key) {
let mut filter_by_deployment_slot = None;
for entry in second_level.iter().rev() {
let required_deployment_slot =
filter_by_deployment_slot.unwrap_or(entry.deployment_slot);
if required_deployment_slot != entry.deployment_slot {
continue;
}
let entry_in_same_branch = entry.deployment_slot
<= self.latest_root_slot
|| matches!(
locked_fork_graph.relationship(
entry.deployment_slot,
loaded_programs_for_tx_batch.slot
),
BlockRelation::Equal | BlockRelation::Ancestor
);
if entry_in_same_branch {
let entry_is_effective =
loaded_programs_for_tx_batch.slot >= entry.effective_slot;
let entry_to_return = if entry_is_effective {
if !Self::matches_environment(
entry,
program_runtime_environment_for_execution,
) {
filter_by_deployment_slot = filter_by_deployment_slot
.or(Some(entry.deployment_slot));
continue;
}
if !Self::matches_criteria(entry, match_criteria) {
break;
}
if let ProgramCacheEntryType::Unloaded(_environment) =
&entry.program
{
break;
}
entry.clone()
} else if entry.is_implicit_delay_visibility_tombstone(
loaded_programs_for_tx_batch.slot,
) {
Arc::new(ProgramCacheEntry::new_tombstone_with_stats(
entry.deployment_slot,
entry.account_owner,
ProgramCacheEntryType::DelayVisibility,
Arc::clone(&entry.stats),
))
} else {
continue;
};
entry_to_return
.update_access_slot(loaded_programs_for_tx_batch.slot);
if increment_usage_counter {
entry_to_return.stats.uses.fetch_add(1, Ordering::Relaxed);
}
loaded_programs_for_tx_batch
.entries
.insert(*key, entry_to_return);
return false;
}
}
}
if cooperative_loading_task.is_none() {
let mut loading_entries = loading_entries.lock().unwrap();
let entry = loading_entries.entry(*key);
if let Entry::Vacant(entry) = entry {
entry.insert((
loaded_programs_for_tx_batch.slot,
thread::current().id(),
));
cooperative_loading_task = Some(*key);
}
}
true
});
}
}
drop(locked_fork_graph);
if count_hits_and_misses {
self.stats
.misses
.fetch_add(search_for.len() as u64, Ordering::Relaxed);
self.stats.hits.fetch_add(
loaded_programs_for_tx_batch.entries.len() as u64,
Ordering::Relaxed,
);
}
cooperative_loading_task
}
pub fn finish_cooperative_loading_task(
&mut self,
program_runtime_environment: &ProgramRuntimeEnvironment,
current_slot: Slot,
key: Pubkey,
last_modification_slot: Slot,
loaded_program: Arc<ProgramCacheEntry>,
) -> bool {
match &mut self.index {
IndexImplementation::V1 {
loading_entries, ..
} => {
let loading_thread = loading_entries.get_mut().unwrap().remove(&key);
debug_assert_eq!(loading_thread, Some((current_slot, thread::current().id())));
if loaded_program.deployment_slot > self.latest_root_slot
&& !matches!(
self.fork_graph
.as_ref()
.unwrap()
.upgrade()
.unwrap()
.read()
.unwrap()
.relationship(loaded_program.deployment_slot, current_slot),
BlockRelation::Equal | BlockRelation::Ancestor
)
{
self.stats.lost_insertions.fetch_add(1, Ordering::Relaxed);
}
let was_occupied = self.assign_program(
program_runtime_environment,
key,
last_modification_slot,
loaded_program,
);
self.loading_task_waiter.notify();
was_occupied
}
}
}
pub fn merge(
&mut self,
program_runtime_environment: &ProgramRuntimeEnvironment,
current_slot: Slot,
modified_entries: &HashMap<Pubkey, Arc<ProgramCacheEntry>>,
) {
modified_entries.iter().for_each(|(key, entry)| {
self.assign_program(
program_runtime_environment,
*key,
current_slot,
entry.clone(),
);
})
}
pub fn get_flattened_entries(&self) -> Vec<(Pubkey, Slot, Arc<ProgramCacheEntry>)> {
match &self.index {
IndexImplementation::V1 { entries, .. } => entries
.iter()
.flat_map(|(id, second_level)| {
second_level
.iter()
.filter_map(move |program| match program.program {
ProgramCacheEntryType::Loaded(_) => Some((*id, 0, program.clone())),
_ => None,
})
})
.collect(),
}
}
#[cfg(feature = "dev-context-only-utils")]
pub fn get_flattened_entries_for_tests(&self) -> Vec<(Pubkey, Arc<ProgramCacheEntry>)> {
match &self.index {
IndexImplementation::V1 { entries, .. } => entries
.iter()
.flat_map(|(id, second_level)| {
second_level.iter().map(|program| (*id, program.clone()))
})
.collect(),
}
}
pub fn get_slot_versions_for_tests(&self, key: &Pubkey) -> &[Arc<ProgramCacheEntry>] {
match &self.index {
IndexImplementation::V1 { entries, .. } => entries
.get(key)
.map(|second_level| second_level.as_ref())
.unwrap_or(&[]),
}
}
pub fn sort_and_unload(&mut self, shrink_to: PercentageInteger) {
let mut sorted_candidates = self.get_flattened_entries();
sorted_candidates.sort_by_cached_key(|(_id, _last_modification_slot, program)| {
program.stats.uses.load(Ordering::Relaxed)
});
let num_to_unload = sorted_candidates
.len()
.saturating_sub(shrink_to.apply_to(MAX_LOADED_ENTRY_COUNT));
for (program, last_modification_slot, entry) in sorted_candidates.iter().take(num_to_unload)
{
self.unload_program_entry(*program, *last_modification_slot, entry);
}
}
pub fn evict_using_random_selection(&mut self, shrink_to: PercentageInteger, now: Slot) {
let mut candidates = self.get_flattened_entries();
let mut rng = rng();
self.stats
.water_level
.store(candidates.len() as u64, Ordering::Relaxed);
let num_to_unload = candidates
.len()
.saturating_sub(shrink_to.apply_to(MAX_LOADED_ENTRY_COUNT));
let mut sample_entry = |candidates: &Vec<(Pubkey, u64, Arc<ProgramCacheEntry>)>| {
#[cfg(feature = "shuttle-test")]
let index = rng.gen_range(0..candidates.len());
#[cfg(not(feature = "shuttle-test"))]
let index = rng.random_range(0..candidates.len());
let usage_counter = candidates
.get(index)
.expect("Failed to get cached entry")
.2
.retention_score();
(index, usage_counter)
};
const MAX_ADDITIONAL_SAMPLES: usize = 3;
let avoid_evicting_above_score = retention_score(now, 500 * EMA_SCALE, 500);
for _ in 0..num_to_unload {
let (mut index, mut score) = sample_entry(&candidates);
for _ in 0..MAX_ADDITIONAL_SAMPLES {
let (sample_index, sample_score) = sample_entry(&candidates);
if score > sample_score {
index = sample_index;
score = sample_score;
}
if score < avoid_evicting_above_score {
break;
}
}
let (id, last_modification_slot, entry) = candidates.swap_remove(index);
self.unload_program_entry(id, last_modification_slot, &entry);
}
}
pub fn remove_programs(&mut self, keys: impl Iterator<Item = Pubkey>) {
match &mut self.index {
IndexImplementation::V1 { entries, .. } => {
for k in keys {
entries.remove(&k);
}
}
}
}
fn unload_program_entry(
&mut self,
id: Pubkey,
_last_modification_slot: Slot,
remove_entry: &Arc<ProgramCacheEntry>,
) {
match &mut self.index {
IndexImplementation::V1 { entries, .. } => {
let second_level = entries.get_mut(&id).expect("Cache lookup failed");
let candidate = second_level
.iter_mut()
.find(|entry| entry == &remove_entry)
.expect("Program entry not found");
if let Some(unloaded) = candidate.to_unloaded() {
if candidate.stats.uses.load(Ordering::Relaxed) == 1 {
self.stats.one_hit_wonders.fetch_add(1, Ordering::Relaxed);
}
self.stats
.evictions
.entry(id)
.and_modify(|c| *c = c.saturating_add(1))
.or_insert(1);
*candidate = Arc::new(unloaded);
}
}
}
}
fn remove_programs_with_no_entries(&mut self) {
match &mut self.index {
IndexImplementation::V1 { entries, .. } => {
let num_programs_before_removal = entries.len();
entries.retain(|_key, second_level| !second_level.is_empty());
if entries.len() < num_programs_before_removal {
self.stats.empty_entries.fetch_add(
num_programs_before_removal.saturating_sub(entries.len()) as u64,
Ordering::Relaxed,
);
}
}
}
}
}
#[cfg(feature = "frozen-abi")]
impl solana_frozen_abi::abi_example::AbiExample for ProgramCacheEntry {
fn example() -> Self {
Self::default()
}
}
#[cfg(feature = "frozen-abi")]
impl<FG: ForkGraph> solana_frozen_abi::abi_example::AbiExample for ProgramCache<FG> {
fn example() -> Self {
Self::new(Slot::default())
}
}
#[cfg(test)]
pub(crate) mod tests {
use {
crate::{
loaded_programs::{
BlockRelation, ForkGraph, ProgramCache, ProgramCacheForTxBatch,
ProgramCacheMatchCriteria, ProgramRuntimeEnvironment,
get_mock_program_runtime_environment,
},
program_cache_entry::{
DELAY_VISIBILITY_SLOT_OFFSET, ProgramCacheEntry, ProgramCacheEntryOwner,
ProgramCacheEntryType,
},
program_metrics::ProgramStatistics,
},
assert_matches::assert_matches,
percentage::Percentage,
solana_clock::Slot,
solana_pubkey::Pubkey,
solana_sbpf::{elf::Executable, program::BuiltinProgram},
std::{
fs::File,
io::Read,
ops::ControlFlow,
sync::{
Arc, RwLock,
atomic::{AtomicU64, Ordering},
},
},
test_case::{test_case, test_matrix},
};
fn new_test_entry(deployment_slot: Slot, effective_slot: Slot) -> Arc<ProgramCacheEntry> {
new_test_entry_with_usage(
deployment_slot,
effective_slot,
ProgramStatistics::default(),
)
}
fn new_loaded_entry(env: ProgramRuntimeEnvironment) -> ProgramCacheEntryType {
let mut elf = Vec::new();
File::open("../programs/bpf_loader/test_elfs/out/noop_aligned.so")
.unwrap()
.read_to_end(&mut elf)
.unwrap();
let executable = Executable::load(&elf, Arc::clone(&*env)).unwrap();
ProgramCacheEntryType::Loaded(executable)
}
pub(crate) fn new_test_entry_with_usage(
deployment_slot: Slot,
effective_slot: Slot,
stats: ProgramStatistics,
) -> Arc<ProgramCacheEntry> {
Arc::new(ProgramCacheEntry {
program: new_loaded_entry(get_mock_program_runtime_environment()),
account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot,
effective_slot,
stats: Arc::new(stats),
latest_access_slot: AtomicU64::new(deployment_slot),
})
}
fn new_test_builtin_entry(
deployment_slot: Slot,
effective_slot: Slot,
) -> Arc<ProgramCacheEntry> {
Arc::new(ProgramCacheEntry {
program: ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
account_owner: ProgramCacheEntryOwner::NativeLoader,
account_size: 0,
deployment_slot,
effective_slot,
stats: Arc::default(),
latest_access_slot: AtomicU64::default(),
})
}
fn set_tombstone<FG: ForkGraph>(
cache: &mut ProgramCache<FG>,
key: Pubkey,
current_slot: Slot,
reason: ProgramCacheEntryType,
) -> Arc<ProgramCacheEntry> {
let env = get_mock_program_runtime_environment();
let program = Arc::new(ProgramCacheEntry::new_tombstone(
current_slot,
ProgramCacheEntryOwner::LoaderV2,
reason,
));
cache.assign_program(&env, key, current_slot, program.clone());
program
}
fn insert_unloaded_entry<FG: ForkGraph>(
cache: &mut ProgramCache<FG>,
key: Pubkey,
current_slot: Slot,
) -> Arc<ProgramCacheEntry> {
let env = get_mock_program_runtime_environment();
let loaded = new_test_entry_with_usage(
current_slot,
current_slot.saturating_add(1),
ProgramStatistics::default(),
);
let unloaded = Arc::new(loaded.to_unloaded().expect("Failed to unload the program"));
cache.assign_program(&env, key, current_slot, unloaded.clone());
unloaded
}
fn num_matching_entries<P, FG>(cache: &ProgramCache<FG>, predicate: P) -> usize
where
P: Fn(&ProgramCacheEntryType) -> bool,
FG: ForkGraph,
{
cache
.get_flattened_entries_for_tests()
.iter()
.filter(|(_key, program)| predicate(&program.program))
.count()
}
fn program_deploy_test_helper(
cache: &mut ProgramCache<TestForkGraph>,
program: Pubkey,
deployment_slots: Vec<Slot>,
usage_counters: Vec<u64>,
programs: &mut Vec<(Pubkey, Slot, u64)>,
) {
let env = get_mock_program_runtime_environment();
deployment_slots
.iter()
.enumerate()
.for_each(|(i, deployment_slot)| {
let usage_counter = *usage_counters.get(i).unwrap_or(&0);
let stats = ProgramStatistics {
uses: usage_counter.into(),
..Default::default()
};
cache.assign_program(
&env,
program,
*deployment_slot,
new_test_entry_with_usage(
*deployment_slot,
(*deployment_slot).saturating_add(2),
stats,
),
);
programs.push((program, *deployment_slot, usage_counter));
});
let env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock());
for slot in 21..31 {
set_tombstone(
cache,
program,
slot,
ProgramCacheEntryType::FailedVerification(env.clone()),
);
}
for slot in 31..41 {
insert_unloaded_entry(cache, program, slot);
}
}
#[test]
fn test_random_eviction() {
let mut programs = vec![];
let mut cache = ProgramCache::<TestForkGraph>::new(0);
program_deploy_test_helper(
&mut cache,
Pubkey::new_unique(),
vec![0, 10, 20],
vec![4, 5, 25],
&mut programs,
);
program_deploy_test_helper(
&mut cache,
Pubkey::new_unique(),
vec![5, 11],
vec![0, 2],
&mut programs,
);
program_deploy_test_helper(
&mut cache,
Pubkey::new_unique(),
vec![0, 5, 15],
vec![100, 3, 20],
&mut programs,
);
let num_loaded_expected = 8;
let num_unloaded_expected = 30;
let num_tombstones_expected = 30;
programs.sort_by_key(|(_id, _slot, usage_count)| *usage_count);
let num_loaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::Loaded(_))
});
let num_unloaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::Unloaded(_))
});
let num_tombstones = num_matching_entries(&cache, |program_type| {
matches!(
program_type,
ProgramCacheEntryType::DelayVisibility
| ProgramCacheEntryType::FailedVerification(_)
| ProgramCacheEntryType::Closed
)
});
assert_eq!(num_loaded, num_loaded_expected);
assert_eq!(num_unloaded, num_unloaded_expected);
assert_eq!(num_tombstones, num_tombstones_expected);
let eviction_pct = 1;
let num_loaded_expected =
Percentage::from(eviction_pct).apply_to(crate::loaded_programs::MAX_LOADED_ENTRY_COUNT);
let num_unloaded_expected = num_unloaded_expected + num_loaded - num_loaded_expected;
cache.evict_using_random_selection(Percentage::from(eviction_pct), 21);
let num_loaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::Loaded(_))
});
let num_unloaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::Unloaded(_))
});
let num_tombstones = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::FailedVerification(_))
});
assert_eq!(num_loaded, num_loaded_expected);
assert_eq!(num_unloaded, num_unloaded_expected);
assert_eq!(num_tombstones, num_tombstones_expected);
}
#[test]
fn test_eviction() {
let mut programs = vec![];
let mut cache = ProgramCache::<TestForkGraph>::new(0);
program_deploy_test_helper(
&mut cache,
Pubkey::new_unique(),
vec![0, 10, 20],
vec![4, 5, 25],
&mut programs,
);
program_deploy_test_helper(
&mut cache,
Pubkey::new_unique(),
vec![5, 11],
vec![0, 2],
&mut programs,
);
program_deploy_test_helper(
&mut cache,
Pubkey::new_unique(),
vec![0, 5, 15],
vec![100, 3, 20],
&mut programs,
);
let num_loaded_expected = 8;
let num_unloaded_expected = 30;
let num_tombstones_expected = 30;
programs.sort_by_key(|(_id, _slot, usage_count)| *usage_count);
let num_loaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::Loaded(_))
});
let num_unloaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::Unloaded(_))
});
let num_tombstones = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::FailedVerification(_))
});
assert_eq!(num_loaded, num_loaded_expected);
assert_eq!(num_unloaded, num_unloaded_expected);
assert_eq!(num_tombstones, num_tombstones_expected);
let eviction_pct = 1;
let num_loaded_expected =
Percentage::from(eviction_pct).apply_to(crate::loaded_programs::MAX_LOADED_ENTRY_COUNT);
let num_unloaded_expected = num_unloaded_expected + num_loaded - num_loaded_expected;
cache.sort_and_unload(Percentage::from(eviction_pct));
let entries = cache.get_flattened_entries_for_tests();
programs.iter().for_each(|entry| {
assert!(entries.iter().any(|(key, _entry)| key == &entry.0));
});
let unloaded = entries
.iter()
.filter_map(|(key, program)| {
matches!(program.program, ProgramCacheEntryType::Unloaded(_))
.then_some((*key, program.stats.uses.load(Ordering::Relaxed)))
})
.collect::<Vec<(Pubkey, u64)>>();
for index in 0..3 {
let expected = programs.get(index).expect("Missing program");
assert!(unloaded.contains(&(expected.0, expected.2)));
}
let num_loaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::Loaded(_))
});
let num_unloaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::Unloaded(_))
});
let num_tombstones = num_matching_entries(&cache, |program_type| {
matches!(
program_type,
ProgramCacheEntryType::DelayVisibility
| ProgramCacheEntryType::FailedVerification(_)
| ProgramCacheEntryType::Closed
)
});
assert_eq!(num_loaded, num_loaded_expected);
assert_eq!(num_unloaded, num_unloaded_expected);
assert_eq!(num_tombstones, num_tombstones_expected);
}
#[test]
fn test_usage_count_of_unloaded_program() {
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let env = get_mock_program_runtime_environment();
let program = Pubkey::new_unique();
let evict_to_pct = 2;
let cache_capacity_after_shrink =
Percentage::from(evict_to_pct).apply_to(crate::loaded_programs::MAX_LOADED_ENTRY_COUNT);
let num_total_programs = (cache_capacity_after_shrink + 1) as u64;
(0..num_total_programs).for_each(|i| {
let stats = ProgramStatistics {
uses: (i + 10).into(),
..Default::default()
};
let entry = new_test_entry_with_usage(i, i + 2, stats);
cache.assign_program(&env, program, i, entry);
});
cache.sort_and_unload(Percentage::from(evict_to_pct));
let num_unloaded = num_matching_entries(&cache, |program_type| {
matches!(program_type, ProgramCacheEntryType::Unloaded(_))
});
assert_eq!(num_unloaded, 1);
cache
.get_flattened_entries_for_tests()
.iter()
.for_each(|(_key, program)| {
if matches!(program.program, ProgramCacheEntryType::Unloaded(_)) {
assert_eq!(program.stats.uses.load(Ordering::Relaxed), 10);
assert_eq!(program.deployment_slot, 0);
assert_eq!(program.effective_slot, 2);
}
});
cache.assign_program(
&env,
program,
0,
new_test_entry_with_usage(0, 2, ProgramStatistics::default()),
);
cache
.get_flattened_entries_for_tests()
.iter()
.for_each(|(_key, program)| {
if matches!(program.program, ProgramCacheEntryType::Unloaded(_))
&& program.deployment_slot == 0
&& program.effective_slot == 2
{
assert_eq!(program.stats.uses.load(Ordering::Relaxed), 10);
}
});
}
#[test]
fn test_fuzz_assign_program_order() {
use rand::prelude::SliceRandom;
const EXPECTED_ENTRIES: [(u64, u64); 7] =
[(1, 2), (5, 5), (5, 6), (5, 10), (9, 10), (10, 10), (3, 12)];
let mut rng = rand::rng();
let program_id = Pubkey::new_unique();
let env = get_mock_program_runtime_environment();
for _ in 0..1000 {
let mut entries = EXPECTED_ENTRIES.to_vec();
entries.shuffle(&mut rng);
let mut cache = ProgramCache::<TestForkGraph>::new(0);
for (deployment_slot, effective_slot) in entries {
let entry = Arc::new(ProgramCacheEntry {
program: new_loaded_entry(ProgramRuntimeEnvironment::from(
BuiltinProgram::new_mock(),
)), account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot,
effective_slot,
stats: Arc::default(),
latest_access_slot: AtomicU64::new(deployment_slot),
});
assert!(!cache.assign_program(&env, program_id, deployment_slot, entry));
}
for ((deployment_slot, effective_slot), entry) in EXPECTED_ENTRIES
.iter()
.zip(cache.get_slot_versions_for_tests(&program_id).iter())
{
assert_eq!(entry.deployment_slot, *deployment_slot);
assert_eq!(entry.effective_slot, *effective_slot);
}
}
}
#[test_matrix(
(
ProgramCacheEntryType::Closed,
ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()),
new_loaded_entry(get_mock_program_runtime_environment()),
),
(
ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()),
ProgramCacheEntryType::Closed,
ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
new_loaded_entry(get_mock_program_runtime_environment()),
ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
)
)]
#[test_matrix(
(
ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
),
(
ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()),
ProgramCacheEntryType::Closed,
ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
)
)]
#[test_matrix(
(ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),),
(
ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()),
ProgramCacheEntryType::Closed,
ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
new_loaded_entry(get_mock_program_runtime_environment()),
)
)]
#[should_panic(expected = "Unexpected replacement of an entry")]
fn test_assign_program_failure(old: ProgramCacheEntryType, new: ProgramCacheEntryType) {
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let env = get_mock_program_runtime_environment();
let program_id = Pubkey::new_unique();
assert!(!cache.assign_program(
&env,
program_id,
10,
Arc::new(ProgramCacheEntry {
program: old,
account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot: 10,
effective_slot: 11,
stats: Arc::default(),
latest_access_slot: AtomicU64::default(),
}),
));
cache.assign_program(
&env,
program_id,
10,
Arc::new(ProgramCacheEntry {
program: new,
account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot: 10,
effective_slot: 11,
stats: Arc::default(),
latest_access_slot: AtomicU64::default(),
}),
);
}
#[test_case(
ProgramCacheEntryType::Unloaded(ProgramRuntimeEnvironment::from(
BuiltinProgram::new_mock()
)),
new_loaded_entry(get_mock_program_runtime_environment())
)]
#[test_case(
ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock())
)]
fn test_assign_program_success(old: ProgramCacheEntryType, new: ProgramCacheEntryType) {
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let env = get_mock_program_runtime_environment();
let program_id = Pubkey::new_unique();
assert!(!cache.assign_program(
&env,
program_id,
10,
Arc::new(ProgramCacheEntry {
program: old,
account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot: 10,
effective_slot: 11,
stats: Arc::default(),
latest_access_slot: AtomicU64::default(),
}),
));
assert!(!cache.assign_program(
&env,
program_id,
10,
Arc::new(ProgramCacheEntry {
program: new,
account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot: 10,
effective_slot: 11,
stats: Arc::default(),
latest_access_slot: AtomicU64::default(),
}),
));
}
#[test]
fn test_assign_program_removes_entries_in_same_slot() {
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let env = get_mock_program_runtime_environment();
let program_id = Pubkey::new_unique();
let closed_other_slot = Arc::new(ProgramCacheEntry {
program: ProgramCacheEntryType::Closed,
account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot: 9,
effective_slot: 9,
stats: Arc::default(),
latest_access_slot: AtomicU64::default(),
});
let closed_current_slot = Arc::new(ProgramCacheEntry {
program: ProgramCacheEntryType::Closed,
account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot: 10,
effective_slot: 10,
stats: Arc::default(),
latest_access_slot: AtomicU64::default(),
});
let loaded_entry_current_env = Arc::new(ProgramCacheEntry {
program: ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot: 10,
effective_slot: 11,
stats: Arc::default(),
latest_access_slot: AtomicU64::default(),
});
let loaded_entry_upcoming_env = Arc::new(ProgramCacheEntry {
program: ProgramCacheEntryType::Unloaded(ProgramRuntimeEnvironment::from(
BuiltinProgram::new_mock(),
)),
account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot: 10,
effective_slot: 11,
stats: Arc::default(),
latest_access_slot: AtomicU64::default(),
});
assert!(!cache.assign_program(&env, program_id, 9, closed_other_slot.clone()));
assert!(!cache.assign_program(&env, program_id, 10, closed_current_slot));
assert!(!cache.assign_program(&env, program_id, 10, loaded_entry_upcoming_env.clone()));
assert!(!cache.assign_program(&env, program_id, 10, loaded_entry_current_env.clone()));
assert_eq!(
cache.get_slot_versions_for_tests(&program_id),
&[
closed_other_slot,
loaded_entry_current_env,
loaded_entry_upcoming_env
]
);
}
#[test]
fn test_tombstone() {
let env = get_mock_program_runtime_environment();
let tombstone = ProgramCacheEntry::new_tombstone(
0,
ProgramCacheEntryOwner::LoaderV2,
ProgramCacheEntryType::FailedVerification(env.clone()),
);
assert_matches!(
tombstone.program,
ProgramCacheEntryType::FailedVerification(_)
);
assert!(tombstone.is_tombstone());
assert_eq!(tombstone.deployment_slot, 0);
assert_eq!(tombstone.effective_slot, 0);
let tombstone = ProgramCacheEntry::new_tombstone(
100,
ProgramCacheEntryOwner::LoaderV2,
ProgramCacheEntryType::Closed,
);
assert_matches!(tombstone.program, ProgramCacheEntryType::Closed);
assert!(tombstone.is_tombstone());
assert_eq!(tombstone.deployment_slot, 100);
assert_eq!(tombstone.effective_slot, 100);
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let program1 = Pubkey::new_unique();
let tombstone = set_tombstone(
&mut cache,
program1,
10,
ProgramCacheEntryType::FailedVerification(env.clone()),
);
let slot_versions = cache.get_slot_versions_for_tests(&program1);
assert_eq!(slot_versions.len(), 1);
assert!(slot_versions.first().unwrap().is_tombstone());
assert_eq!(tombstone.deployment_slot, 10);
assert_eq!(tombstone.effective_slot, 10);
let program2 = Pubkey::new_unique();
cache.assign_program(&env, program2, 50, new_test_builtin_entry(50, 51));
let slot_versions = cache.get_slot_versions_for_tests(&program2);
assert_eq!(slot_versions.len(), 1);
assert!(!slot_versions.first().unwrap().is_tombstone());
let tombstone = set_tombstone(
&mut cache,
program2,
60,
ProgramCacheEntryType::FailedVerification(env),
);
let slot_versions = cache.get_slot_versions_for_tests(&program2);
assert_eq!(slot_versions.len(), 2);
assert!(!slot_versions.first().unwrap().is_tombstone());
assert!(slot_versions.get(1).unwrap().is_tombstone());
assert!(tombstone.is_tombstone());
assert_eq!(tombstone.deployment_slot, 60);
assert_eq!(tombstone.effective_slot, 60);
}
struct TestForkGraph {
relation: BlockRelation,
}
impl ForkGraph for TestForkGraph {
fn relationship(&self, _a: Slot, _b: Slot) -> BlockRelation {
self.relation
}
}
#[test]
fn test_prune_empty() {
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let fork_graph = Arc::new(RwLock::new(TestForkGraph {
relation: BlockRelation::Unrelated,
}));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
cache.prune(0, None, &fork_graph.read().unwrap());
assert!(cache.get_flattened_entries_for_tests().is_empty());
cache.prune(10, None, &fork_graph.read().unwrap());
assert!(cache.get_flattened_entries_for_tests().is_empty());
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let fork_graph = Arc::new(RwLock::new(TestForkGraph {
relation: BlockRelation::Ancestor,
}));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
cache.prune(0, None, &fork_graph.read().unwrap());
assert!(cache.get_flattened_entries_for_tests().is_empty());
cache.prune(10, None, &fork_graph.read().unwrap());
assert!(cache.get_flattened_entries_for_tests().is_empty());
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let fork_graph = Arc::new(RwLock::new(TestForkGraph {
relation: BlockRelation::Descendant,
}));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
cache.prune(0, None, &fork_graph.read().unwrap());
assert!(cache.get_flattened_entries_for_tests().is_empty());
cache.prune(10, None, &fork_graph.read().unwrap());
assert!(cache.get_flattened_entries_for_tests().is_empty());
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let fork_graph = Arc::new(RwLock::new(TestForkGraph {
relation: BlockRelation::Unknown,
}));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
cache.prune(0, None, &fork_graph.read().unwrap());
assert!(cache.get_flattened_entries_for_tests().is_empty());
cache.prune(10, None, &fork_graph.read().unwrap());
assert!(cache.get_flattened_entries_for_tests().is_empty());
}
#[test]
fn test_prune_different_env() {
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let env = get_mock_program_runtime_environment();
let fork_graph = Arc::new(RwLock::new(TestForkGraph {
relation: BlockRelation::Ancestor,
}));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
let program1 = Pubkey::new_unique();
cache.assign_program(&env, program1, 10, new_test_entry(10, 10));
let new_env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock());
let upcoming_environment = Some(new_env.clone());
let updated_program = Arc::new(ProgramCacheEntry {
program: new_loaded_entry(new_env.clone()),
deployment_slot: 20,
effective_slot: 20,
..Default::default()
});
cache.assign_program(
&env,
program1,
updated_program.deployment_slot,
updated_program.clone(),
);
assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 2);
cache.prune(21, None, &fork_graph.read().unwrap());
assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 2);
cache.prune(22, upcoming_environment, &fork_graph.read().unwrap());
assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 1);
let entry = cache
.get_slot_versions_for_tests(&program1)
.first()
.expect("Failed to get the program")
.clone();
assert_eq!(entry, updated_program);
}
#[derive(Default)]
struct TestForkGraphSpecific {
forks: Vec<Vec<Slot>>,
}
impl TestForkGraphSpecific {
fn insert_fork(&mut self, fork: &[Slot]) {
let mut fork = fork.to_vec();
fork.sort();
self.forks.push(fork)
}
}
impl ForkGraph for TestForkGraphSpecific {
fn relationship(&self, a: Slot, b: Slot) -> BlockRelation {
match self.forks.iter().try_for_each(|fork| {
let relation = fork
.iter()
.position(|x| *x == a)
.and_then(|a_pos| {
fork.iter().position(|x| *x == b).and_then(|b_pos| {
(a_pos == b_pos)
.then_some(BlockRelation::Equal)
.or_else(|| (a_pos < b_pos).then_some(BlockRelation::Ancestor))
.or(Some(BlockRelation::Descendant))
})
})
.unwrap_or(BlockRelation::Unrelated);
if relation != BlockRelation::Unrelated {
return ControlFlow::Break(relation);
}
ControlFlow::Continue(())
}) {
ControlFlow::Break(relation) => relation,
_ => BlockRelation::Unrelated,
}
}
}
fn get_entries_to_load(
cache: &ProgramCache<TestForkGraphSpecific>,
loading_slot: Slot,
keys: &[Pubkey],
) -> Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> {
let fork_graph = cache.fork_graph.as_ref().unwrap().upgrade().unwrap();
let locked_fork_graph = fork_graph.read().unwrap();
let entries = cache.get_flattened_entries_for_tests();
keys.iter()
.filter_map(|key| {
entries
.iter()
.rev()
.find(|(program_id, entry)| {
program_id == key
&& matches!(
locked_fork_graph.relationship(entry.deployment_slot, loading_slot),
BlockRelation::Equal | BlockRelation::Ancestor,
)
})
.map(|(program_id, entry)| {
(
*program_id,
ProgramCacheMatchCriteria::NoCriteria,
entry.deployment_slot,
)
})
})
.collect()
}
fn match_slot(
extracted: &ProgramCacheForTxBatch,
program: &Pubkey,
deployment_slot: Slot,
working_slot: Slot,
) -> bool {
assert_eq!(extracted.slot, working_slot);
extracted
.entries
.get(program)
.map(|entry| entry.deployment_slot == deployment_slot)
.unwrap_or(false)
}
fn match_missing(
missing: &[(Pubkey, ProgramCacheMatchCriteria, Slot)],
program: &Pubkey,
expected_result: bool,
) -> bool {
missing.iter().any(|(key, _, _)| key == program) == expected_result
}
#[test]
fn test_fork_extract_and_prune() {
let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
let env = get_mock_program_runtime_environment();
let mut fork_graph = TestForkGraphSpecific::default();
fork_graph.insert_fork(&[0, 10, 20, 22]);
fork_graph.insert_fork(&[0, 5, 11, 15, 16, 18, 19, 21, 23]);
fork_graph.insert_fork(&[0, 5, 11, 25, 27]);
let fork_graph = Arc::new(RwLock::new(fork_graph));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
let program1 = Pubkey::new_unique();
cache.assign_program(&env, program1, 0, new_test_entry(0, 1));
cache.assign_program(&env, program1, 10, new_test_entry(10, 11));
cache.assign_program(&env, program1, 20, new_test_entry(20, 21));
let program2 = Pubkey::new_unique();
cache.assign_program(&env, program2, 5, new_test_entry(5, 6));
cache.assign_program(
&env,
program2,
11,
new_test_entry(11, 11 + DELAY_VISIBILITY_SLOT_OFFSET),
);
let program3 = Pubkey::new_unique();
cache.assign_program(&env, program3, 25, new_test_entry(25, 26));
let program4 = Pubkey::new_unique();
cache.assign_program(&env, program4, 0, new_test_entry(0, 1));
cache.assign_program(&env, program4, 5, new_test_entry(5, 6));
cache.assign_program(
&env,
program4,
15,
new_test_entry(15, 15 + DELAY_VISIBILITY_SLOT_OFFSET),
);
let mut missing =
get_entries_to_load(&cache, 22, &[program1, program2, program3, program4]);
assert!(match_missing(&missing, &program2, false));
assert!(match_missing(&missing, &program3, false));
let mut extracted = ProgramCacheForTxBatch::new(22);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 20, 22));
assert!(match_slot(&extracted, &program4, 0, 22));
let mut missing =
get_entries_to_load(&cache, 15, &[program1, program2, program3, program4]);
assert!(match_missing(&missing, &program3, false));
let mut extracted = ProgramCacheForTxBatch::new(15);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 15));
assert!(match_slot(&extracted, &program2, 11, 15));
let tombstone = extracted
.find(&program4)
.expect("Failed to find the tombstone");
assert_matches!(tombstone.program, ProgramCacheEntryType::DelayVisibility);
assert_eq!(tombstone.deployment_slot, 15);
let mut missing =
get_entries_to_load(&cache, 18, &[program1, program2, program3, program4]);
assert!(match_missing(&missing, &program3, false));
let mut extracted = ProgramCacheForTxBatch::new(18);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 18));
assert!(match_slot(&extracted, &program2, 11, 18));
assert!(match_slot(&extracted, &program4, 15, 18));
let mut missing =
get_entries_to_load(&cache, 23, &[program1, program2, program3, program4]);
assert!(match_missing(&missing, &program3, false));
let mut extracted = ProgramCacheForTxBatch::new(23);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 23));
assert!(match_slot(&extracted, &program2, 11, 23));
assert!(match_slot(&extracted, &program4, 15, 23));
let mut missing =
get_entries_to_load(&cache, 11, &[program1, program2, program3, program4]);
assert!(match_missing(&missing, &program3, false));
let mut extracted = ProgramCacheForTxBatch::new(11);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 11));
let tombstone = extracted
.find(&program2)
.expect("Failed to find the tombstone");
assert_matches!(tombstone.program, ProgramCacheEntryType::DelayVisibility);
assert_eq!(tombstone.deployment_slot, 11);
assert!(match_slot(&extracted, &program4, 5, 11));
cache.prune(5, None, &fork_graph.read().unwrap());
let mut missing =
get_entries_to_load(&cache, 21, &[program1, program2, program3, program4]);
assert!(match_missing(&missing, &program3, false));
let mut extracted = ProgramCacheForTxBatch::new(21);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 21));
assert!(match_slot(&extracted, &program2, 11, 21));
assert!(match_slot(&extracted, &program4, 15, 21));
let mut missing =
get_entries_to_load(&cache, 27, &[program1, program2, program3, program4]);
let mut extracted = ProgramCacheForTxBatch::new(27);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 27));
assert!(match_slot(&extracted, &program2, 11, 27));
assert!(match_slot(&extracted, &program3, 25, 27));
assert!(match_slot(&extracted, &program4, 5, 27));
cache.prune(15, None, &fork_graph.read().unwrap());
let mut missing =
get_entries_to_load(&cache, 23, &[program1, program2, program3, program4]);
assert!(match_missing(&missing, &program3, false));
let mut extracted = ProgramCacheForTxBatch::new(23);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 23));
assert!(match_slot(&extracted, &program2, 11, 23));
assert!(match_slot(&extracted, &program4, 15, 23));
}
#[test]
fn test_extract_using_deployment_slot() {
let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
let env = get_mock_program_runtime_environment();
let mut fork_graph = TestForkGraphSpecific::default();
fork_graph.insert_fork(&[0, 10, 20, 22]);
fork_graph.insert_fork(&[0, 5, 11, 12, 15, 16, 18, 19, 21, 23]);
fork_graph.insert_fork(&[0, 5, 11, 25, 27]);
let fork_graph = Arc::new(RwLock::new(fork_graph));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
let program1 = Pubkey::new_unique();
cache.assign_program(&env, program1, 0, new_test_entry(0, 1));
cache.assign_program(&env, program1, 20, new_test_entry(20, 21));
let program2 = Pubkey::new_unique();
cache.assign_program(&env, program2, 5, new_test_entry(5, 6));
cache.assign_program(&env, program2, 11, new_test_entry(11, 12));
let program3 = Pubkey::new_unique();
cache.assign_program(&env, program3, 25, new_test_entry(25, 26));
let mut missing = get_entries_to_load(&cache, 12, &[program1, program2, program3]);
assert!(match_missing(&missing, &program3, false));
let mut extracted = ProgramCacheForTxBatch::new(12);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 12));
assert!(match_slot(&extracted, &program2, 11, 12));
let mut missing = get_entries_to_load(&cache, 12, &[program1, program2, program3]);
missing.get_mut(0).unwrap().1 = ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5);
missing.get_mut(1).unwrap().1 = ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5);
assert!(match_missing(&missing, &program3, false));
let mut extracted = ProgramCacheForTxBatch::new(12);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_missing(&missing, &program1, true));
assert!(match_slot(&extracted, &program2, 11, 12));
}
#[test]
fn test_extract_unloaded() {
let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
let env = get_mock_program_runtime_environment();
let mut fork_graph = TestForkGraphSpecific::default();
fork_graph.insert_fork(&[0, 10, 20, 22]);
fork_graph.insert_fork(&[0, 5, 11, 15, 16, 19, 21, 23]);
fork_graph.insert_fork(&[0, 5, 11, 25, 27]);
let fork_graph = Arc::new(RwLock::new(fork_graph));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
let program1 = Pubkey::new_unique();
cache.assign_program(&env, program1, 0, new_test_entry(0, 1));
cache.assign_program(&env, program1, 20, new_test_entry(20, 21));
let program2 = Pubkey::new_unique();
cache.assign_program(&env, program2, 5, new_test_entry(5, 6));
cache.assign_program(&env, program2, 11, new_test_entry(11, 12));
let program3 = Pubkey::new_unique();
let _ = insert_unloaded_entry(&mut cache, program3, 25);
cache.assign_program(
&env,
program3,
20,
Arc::new(
new_test_entry(20, 21)
.to_unloaded()
.expect("Failed to create unloaded program"),
),
);
let mut missing = get_entries_to_load(&cache, 19, &[program1, program2, program3]);
assert!(match_missing(&missing, &program3, false));
let mut extracted = ProgramCacheForTxBatch::new(19);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 19));
assert!(match_slot(&extracted, &program2, 11, 19));
let mut missing = get_entries_to_load(&cache, 27, &[program1, program2, program3]);
let mut extracted = ProgramCacheForTxBatch::new(27);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 27));
assert!(match_slot(&extracted, &program2, 11, 27));
assert!(match_missing(&missing, &program3, true));
let mut missing = get_entries_to_load(&cache, 22, &[program1, program2, program3]);
assert!(match_missing(&missing, &program2, false));
let mut extracted = ProgramCacheForTxBatch::new(22);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 20, 22));
assert!(match_missing(&missing, &program3, true));
}
#[test]
fn test_extract_different_environment() {
let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
let env = get_mock_program_runtime_environment();
let other_env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock());
let mut fork_graph = TestForkGraphSpecific::default();
fork_graph.insert_fork(&[0, 10, 20, 22]);
let fork_graph = Arc::new(RwLock::new(fork_graph));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
let program1 = Pubkey::new_unique();
cache.assign_program(
&env,
program1,
10,
Arc::new(ProgramCacheEntry::new_tombstone(
10,
ProgramCacheEntryOwner::LoaderV3,
ProgramCacheEntryType::Closed,
)),
);
cache.assign_program(&env, program1, 20, new_test_entry(20, 21));
let mut missing = get_entries_to_load(&cache, 22, &[program1]);
let mut extracted = ProgramCacheForTxBatch::new(22);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 20, 22));
let mut missing = get_entries_to_load(&cache, 22, &[program1]);
let mut extracted = ProgramCacheForTxBatch::new(22);
cache.extract(&mut missing, &mut extracted, &other_env, true, true);
assert!(match_missing(&missing, &program1, true));
}
#[test]
fn test_extract_nonexistent() {
let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
let env = get_mock_program_runtime_environment();
let fork_graph = TestForkGraphSpecific::default();
let fork_graph = Arc::new(RwLock::new(fork_graph));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
let program1 = Pubkey::new_unique();
let mut missing = vec![(program1, ProgramCacheMatchCriteria::NoCriteria, 0)];
let mut extracted = ProgramCacheForTxBatch::new(0);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_missing(&missing, &program1, true));
}
#[test]
fn test_unloaded() {
let mut cache = ProgramCache::<TestForkGraph>::new(0);
let env = get_mock_program_runtime_environment();
for program_cache_entry_type in [
ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()),
ProgramCacheEntryType::Closed,
ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()),
ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),
] {
let entry = Arc::new(ProgramCacheEntry {
program: program_cache_entry_type,
account_owner: ProgramCacheEntryOwner::LoaderV2,
account_size: 0,
deployment_slot: 0,
effective_slot: 0,
stats: Arc::default(),
latest_access_slot: AtomicU64::default(),
});
assert!(entry.to_unloaded().is_none());
let program_id = Pubkey::new_unique();
cache.assign_program(&env, program_id, entry.deployment_slot, entry.clone());
cache.unload_program_entry(program_id, entry.deployment_slot, &entry);
assert_eq!(cache.get_slot_versions_for_tests(&program_id).len(), 1);
assert!(cache.stats.evictions.is_empty());
}
let stats = ProgramStatistics {
uses: 3.into(),
..Default::default()
};
let entry = new_test_entry_with_usage(1, 2, stats);
let unloaded_entry = entry.to_unloaded().unwrap();
assert_eq!(unloaded_entry.deployment_slot, 1);
assert_eq!(unloaded_entry.effective_slot, 2);
assert_eq!(unloaded_entry.latest_access_slot.load(Ordering::Relaxed), 1);
assert_eq!(unloaded_entry.stats.uses.load(Ordering::Relaxed), 3);
let program_id = Pubkey::new_unique();
cache.assign_program(&env, program_id, entry.deployment_slot, entry.clone());
cache.unload_program_entry(program_id, entry.deployment_slot, &entry);
assert!(cache.stats.evictions.contains_key(&program_id));
}
#[test]
fn test_fork_prune_find_first_ancestor() {
let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
let env = get_mock_program_runtime_environment();
let mut fork_graph = TestForkGraphSpecific::default();
fork_graph.insert_fork(&[0, 10, 20]);
fork_graph.insert_fork(&[0, 5]);
let fork_graph = Arc::new(RwLock::new(fork_graph));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
let program1 = Pubkey::new_unique();
cache.assign_program(&env, program1, 0, new_test_entry(0, 1));
cache.assign_program(&env, program1, 5, new_test_entry(5, 6));
cache.prune(10, None, &fork_graph.read().unwrap());
let mut missing = get_entries_to_load(&cache, 20, &[program1]);
let mut extracted = ProgramCacheForTxBatch::new(20);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert_eq!(
extracted
.find(&program1)
.expect("Did not find the program")
.deployment_slot,
0
);
}
#[test]
fn test_prune_by_deployment_slot() {
let mut cache = ProgramCache::<TestForkGraphSpecific>::new(0);
let env = get_mock_program_runtime_environment();
let mut fork_graph = TestForkGraphSpecific::default();
fork_graph.insert_fork(&[0, 10, 20]);
fork_graph.insert_fork(&[0, 5, 6]);
let fork_graph = Arc::new(RwLock::new(fork_graph));
cache.set_fork_graph(Arc::downgrade(&fork_graph));
let program1 = Pubkey::new_unique();
cache.assign_program(&env, program1, 0, new_test_entry(0, 1));
cache.assign_program(&env, program1, 5, new_test_entry(5, 6));
let program2 = Pubkey::new_unique();
cache.assign_program(&env, program2, 10, new_test_entry(10, 11));
let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]);
let mut extracted = ProgramCacheForTxBatch::new(20);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 20));
assert!(match_slot(&extracted, &program2, 10, 20));
let mut missing = get_entries_to_load(&cache, 6, &[program1, program2]);
assert!(match_missing(&missing, &program2, false));
let mut extracted = ProgramCacheForTxBatch::new(6);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 5, 6));
cache.prune_by_deployment_slot(5);
let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]);
let mut extracted = ProgramCacheForTxBatch::new(20);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 20));
assert!(match_slot(&extracted, &program2, 10, 20));
let mut missing = get_entries_to_load(&cache, 6, &[program1, program2]);
assert!(match_missing(&missing, &program2, false));
let mut extracted = ProgramCacheForTxBatch::new(6);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 6));
cache.prune_by_deployment_slot(10);
let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]);
assert!(match_missing(&missing, &program2, false));
let mut extracted = ProgramCacheForTxBatch::new(20);
cache.extract(&mut missing, &mut extracted, &env, true, true);
assert!(match_slot(&extracted, &program1, 0, 20));
}
#[test]
fn test_usable_entries_for_slot() {
ProgramCache::<TestForkGraph>::new(0);
let tombstone = Arc::new(ProgramCacheEntry::new_tombstone(
0,
ProgramCacheEntryOwner::LoaderV2,
ProgramCacheEntryType::Closed,
));
assert!(ProgramCache::<TestForkGraph>::matches_criteria(
&tombstone,
&ProgramCacheMatchCriteria::NoCriteria
));
assert!(ProgramCache::<TestForkGraph>::matches_criteria(
&tombstone,
&ProgramCacheMatchCriteria::Tombstone
));
assert!(ProgramCache::<TestForkGraph>::matches_criteria(
&tombstone,
&ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0)
));
assert!(!ProgramCache::<TestForkGraph>::matches_criteria(
&tombstone,
&ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(1)
));
let program = new_test_entry(0, 1);
assert!(ProgramCache::<TestForkGraph>::matches_criteria(
&program,
&ProgramCacheMatchCriteria::NoCriteria
));
assert!(!ProgramCache::<TestForkGraph>::matches_criteria(
&program,
&ProgramCacheMatchCriteria::Tombstone
));
assert!(ProgramCache::<TestForkGraph>::matches_criteria(
&program,
&ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0)
));
assert!(!ProgramCache::<TestForkGraph>::matches_criteria(
&program,
&ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(1)
));
let program = Arc::new(new_test_entry_with_usage(
0,
1,
ProgramStatistics::default(),
));
assert!(ProgramCache::<TestForkGraph>::matches_criteria(
&program,
&ProgramCacheMatchCriteria::NoCriteria
));
assert!(!ProgramCache::<TestForkGraph>::matches_criteria(
&program,
&ProgramCacheMatchCriteria::Tombstone
));
assert!(ProgramCache::<TestForkGraph>::matches_criteria(
&program,
&ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0)
));
assert!(!ProgramCache::<TestForkGraph>::matches_criteria(
&program,
&ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(1)
));
}
}