#[cfg(feature = "metrics")]
use crate::program_metrics::LoadProgramMetrics;
use {
crate::{
invoke_context::{BuiltinFunctionRegisterer, InvokeContext},
loaded_programs::ProgramRuntimeEnvironment,
program_metrics::ProgramStatistics,
},
solana_clock::Slot,
solana_pubkey::Pubkey,
solana_sbpf::{elf::Executable, program::BuiltinProgram, verifier::RequisiteVerifier},
solana_sdk_ids::{
bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader,
},
solana_svm_type_overrides::sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
pub const DELAY_VISIBILITY_SLOT_OFFSET: Slot = 1;
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
pub enum ProgramCacheEntryOwner {
#[default]
NativeLoader,
LoaderV1,
LoaderV2,
LoaderV3,
LoaderV4,
}
impl TryFrom<&Pubkey> for ProgramCacheEntryOwner {
type Error = ();
fn try_from(loader_key: &Pubkey) -> Result<Self, ()> {
if native_loader::check_id(loader_key) {
Ok(ProgramCacheEntryOwner::NativeLoader)
} else if bpf_loader_deprecated::check_id(loader_key) {
Ok(ProgramCacheEntryOwner::LoaderV1)
} else if bpf_loader::check_id(loader_key) {
Ok(ProgramCacheEntryOwner::LoaderV2)
} else if bpf_loader_upgradeable::check_id(loader_key) {
Ok(ProgramCacheEntryOwner::LoaderV3)
} else if loader_v4::check_id(loader_key) {
Ok(ProgramCacheEntryOwner::LoaderV4)
} else {
Err(())
}
}
}
impl From<ProgramCacheEntryOwner> for Pubkey {
fn from(program_cache_entry_owner: ProgramCacheEntryOwner) -> Self {
match program_cache_entry_owner {
ProgramCacheEntryOwner::NativeLoader => native_loader::id(),
ProgramCacheEntryOwner::LoaderV1 => bpf_loader_deprecated::id(),
ProgramCacheEntryOwner::LoaderV2 => bpf_loader::id(),
ProgramCacheEntryOwner::LoaderV3 => bpf_loader_upgradeable::id(),
ProgramCacheEntryOwner::LoaderV4 => loader_v4::id(),
}
}
}
#[derive(Default)]
pub enum ProgramCacheEntryType {
FailedVerification(ProgramRuntimeEnvironment),
#[default]
Closed,
DelayVisibility,
Unloaded(ProgramRuntimeEnvironment),
Loaded(Executable<InvokeContext<'static, 'static>>),
Builtin(BuiltinProgram<InvokeContext<'static, 'static>>),
}
impl std::fmt::Debug for ProgramCacheEntryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(match self {
ProgramCacheEntryType::FailedVerification(_) => {
"ProgramCacheEntryType::FailedVerification"
}
ProgramCacheEntryType::Closed => "ProgramCacheEntryType::Closed",
ProgramCacheEntryType::DelayVisibility => "ProgramCacheEntryType::DelayVisibility",
ProgramCacheEntryType::Unloaded(_) => "ProgramCacheEntryType::Unloaded",
ProgramCacheEntryType::Loaded(_) => "ProgramCacheEntryType::Loaded",
ProgramCacheEntryType::Builtin(_) => "ProgramCacheEntryType::Builtin",
})
.finish()
}
}
impl ProgramCacheEntryType {
pub fn get_environment(&self) -> Option<&ProgramRuntimeEnvironment> {
match self {
ProgramCacheEntryType::Loaded(program) => {
Some(ProgramRuntimeEnvironment::from_ref(program.get_loader()))
}
ProgramCacheEntryType::FailedVerification(env)
| ProgramCacheEntryType::Unloaded(env) => Some(env),
_ => None,
}
}
}
#[derive(Debug, Default)]
pub struct ProgramCacheEntry {
pub program: ProgramCacheEntryType,
pub account_owner: ProgramCacheEntryOwner,
pub account_size: usize,
pub deployment_slot: Slot,
pub effective_slot: Slot,
pub stats: Arc<ProgramStatistics>,
pub latest_access_slot: AtomicU64,
}
impl PartialEq for ProgramCacheEntry {
fn eq(&self, other: &Self) -> bool {
self.effective_slot == other.effective_slot
&& self.deployment_slot == other.deployment_slot
&& self.is_tombstone() == other.is_tombstone()
}
}
impl ProgramCacheEntry {
pub fn new(
loader_key: &Pubkey,
program_runtime_environment: ProgramRuntimeEnvironment,
deployment_slot: Slot,
effective_slot: Slot,
elf_bytes: &[u8],
account_size: usize,
#[cfg(feature = "metrics")] metrics: &mut LoadProgramMetrics,
) -> Result<Self, Box<dyn std::error::Error>> {
Self::new_internal(
loader_key,
program_runtime_environment,
deployment_slot,
effective_slot,
elf_bytes,
account_size,
#[cfg(feature = "metrics")]
metrics,
false,
)
}
pub unsafe fn reload(
loader_key: &Pubkey,
program_runtime_environment: ProgramRuntimeEnvironment,
deployment_slot: Slot,
effective_slot: Slot,
elf_bytes: &[u8],
account_size: usize,
#[cfg(feature = "metrics")] metrics: &mut LoadProgramMetrics,
) -> Result<Self, Box<dyn std::error::Error>> {
Self::new_internal(
loader_key,
program_runtime_environment,
deployment_slot,
effective_slot,
elf_bytes,
account_size,
#[cfg(feature = "metrics")]
metrics,
true,
)
}
fn new_internal(
loader_key: &Pubkey,
program_runtime_environment: ProgramRuntimeEnvironment,
deployment_slot: Slot,
effective_slot: Slot,
elf_bytes: &[u8],
account_size: usize,
#[cfg(feature = "metrics")] metrics: &mut LoadProgramMetrics,
reloading: bool,
) -> Result<Self, Box<dyn std::error::Error>> {
let entry_stats = ProgramStatistics::default();
#[cfg(feature = "metrics")]
let load_elf_time = solana_svm_measure::measure::Measure::start("load_elf_time");
let executable = Executable::load(elf_bytes, Arc::clone(&*program_runtime_environment))?;
#[cfg(feature = "metrics")]
{
metrics.load_elf_us = load_elf_time.end_as_us();
}
if !reloading {
#[cfg(feature = "metrics")]
let verify_code_time = solana_svm_measure::measure::Measure::start("verify_code_time");
executable.verify::<RequisiteVerifier>()?;
#[cfg(feature = "metrics")]
{
metrics.verify_code_us = verify_code_time.end_as_us();
}
}
#[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))]
{
let jit_compile_time = solana_svm_measure::measure::Measure::start("jit_compile_time");
executable.jit_compile()?;
let jit_compile_time = jit_compile_time.end_as_us();
entry_stats.jit_compiled(jit_compile_time);
#[cfg(feature = "metrics")]
{
metrics.jit_compile_us = jit_compile_time;
}
}
Ok(Self {
deployment_slot,
account_owner: ProgramCacheEntryOwner::try_from(loader_key).unwrap(),
account_size,
effective_slot,
program: ProgramCacheEntryType::Loaded(executable),
stats: entry_stats.into(),
latest_access_slot: AtomicU64::new(0),
})
}
pub fn to_unloaded(&self) -> Option<Self> {
match &self.program {
ProgramCacheEntryType::Loaded(_) => {}
ProgramCacheEntryType::FailedVerification(_)
| ProgramCacheEntryType::Closed
| ProgramCacheEntryType::DelayVisibility
| ProgramCacheEntryType::Unloaded(_)
| ProgramCacheEntryType::Builtin(_) => {
return None;
}
}
Some(Self {
program: ProgramCacheEntryType::Unloaded(self.program.get_environment()?.clone()),
account_owner: self.account_owner,
account_size: self.account_size,
deployment_slot: self.deployment_slot,
effective_slot: self.effective_slot,
stats: Arc::clone(&self.stats),
latest_access_slot: AtomicU64::new(self.latest_access_slot.load(Ordering::Relaxed)),
})
}
pub fn new_builtin(
deployment_slot: Slot,
account_size: usize,
register_fn: BuiltinFunctionRegisterer,
) -> Self {
let mut program = BuiltinProgram::new_builtin();
register_fn(&mut program, "entrypoint").unwrap();
Self {
deployment_slot,
account_owner: ProgramCacheEntryOwner::NativeLoader,
account_size,
effective_slot: deployment_slot,
program: ProgramCacheEntryType::Builtin(program),
stats: Arc::default(),
latest_access_slot: AtomicU64::new(0),
}
}
pub fn new_tombstone(
slot: Slot,
account_owner: ProgramCacheEntryOwner,
reason: ProgramCacheEntryType,
) -> Self {
Self::new_tombstone_with_stats(slot, account_owner, reason, Arc::default())
}
pub fn new_tombstone_with_stats(
slot: Slot,
account_owner: ProgramCacheEntryOwner,
reason: ProgramCacheEntryType,
stats: Arc<ProgramStatistics>,
) -> Self {
let tombstone = Self {
program: reason,
account_owner,
account_size: 0,
deployment_slot: slot,
effective_slot: slot,
stats,
latest_access_slot: AtomicU64::new(0),
};
debug_assert!(tombstone.is_tombstone());
tombstone
}
pub fn is_tombstone(&self) -> bool {
matches!(
self.program,
ProgramCacheEntryType::FailedVerification(_)
| ProgramCacheEntryType::Closed
| ProgramCacheEntryType::DelayVisibility
)
}
pub(crate) fn is_implicit_delay_visibility_tombstone(&self, slot: Slot) -> bool {
!matches!(self.program, ProgramCacheEntryType::Builtin(_))
&& self.effective_slot.saturating_sub(self.deployment_slot)
== DELAY_VISIBILITY_SLOT_OFFSET
&& slot >= self.deployment_slot
&& slot < self.effective_slot
}
pub fn update_access_slot(&self, slot: Slot) {
let _ = self.latest_access_slot.fetch_max(slot, Ordering::Relaxed);
}
pub fn retention_score(&self) -> u64 {
let last_access = self.latest_access_slot.load(Ordering::Relaxed);
let recovery_cost = self.stats.compilation_time_ema.load(Ordering::Relaxed);
let frequency = self.stats.uses.load(Ordering::Relaxed);
retention_score(last_access, recovery_cost, frequency)
}
pub fn account_owner(&self) -> Pubkey {
self.account_owner.into()
}
}
pub(crate) const fn retention_score(last_access: u64, recovery_cost: u64, frequency: u64) -> u64 {
let weight = (recovery_cost as u128).wrapping_mul(frequency as u128);
let weight_log = u128::BITS.wrapping_sub(weight.leading_zeros());
last_access.saturating_add(weight_log as u64)
}
#[cfg(test)]
mod tests {
use {
crate::{
loaded_programs::tests::new_test_entry_with_usage, program_metrics::ProgramStatistics,
},
std::sync::atomic::{AtomicU64, Ordering},
};
#[test]
fn test_retention_score_decay_horizon() {
let stats = ProgramStatistics {
uses: AtomicU64::new(u64::MAX),
compilation_time_ema: AtomicU64::new(u64::MAX),
..Default::default()
};
let program = new_test_entry_with_usage(0, 0, stats);
program.update_access_slot(1);
assert!(
dbg!(program.retention_score()) <= 129,
"retention score should remain within sensible boundaries even for very frequently \
used entries."
);
}
#[test]
fn test_retention_score_frequency_preference() {
let stats = ProgramStatistics {
uses: AtomicU64::new(16),
compilation_time_ema: AtomicU64::new(1),
..Default::default()
};
let program = new_test_entry_with_usage(10, 11, stats);
program.update_access_slot(15);
let less_used_retention_score = program.retention_score();
program.stats.uses.fetch_max(1024, Ordering::Relaxed);
let more_used_retention_score = program.retention_score();
assert!(
less_used_retention_score > 15,
"frequency should count for entry retention score"
);
assert!(
dbg!(more_used_retention_score) > dbg!(less_used_retention_score),
"retention score should prefer evicting less used entry over the more used one if \
possible"
);
}
#[test]
fn test_retention_score_recovery_time_preference() {
let stats = ProgramStatistics {
uses: AtomicU64::new(1),
compilation_time_ema: AtomicU64::new(1000),
..Default::default()
};
let program = new_test_entry_with_usage(10, 11, stats);
program.update_access_slot(15);
let cheaper_to_compile_score = program.retention_score();
program
.stats
.compilation_time_ema
.fetch_max(2000, Ordering::Relaxed);
let more_expensive_to_compile_score = program.retention_score();
assert!(
cheaper_to_compile_score > 15,
"compile time should count for entry retention score"
);
assert!(
dbg!(more_expensive_to_compile_score) > dbg!(cheaper_to_compile_score),
"retention score should prefer evicting cheaper-to-compile entries"
);
}
#[test]
fn test_retention_weight_metric_does_not_outweight_smaller_metric() {
let stats = ProgramStatistics {
uses: AtomicU64::new(100_000_000),
compilation_time_ema: AtomicU64::new(1000),
..Default::default()
};
let program = new_test_entry_with_usage(10, 11, stats);
program.update_access_slot(15);
let previous_score = program.retention_score();
program
.stats
.compilation_time_ema
.fetch_max(2000, Ordering::Relaxed);
let new_score = program.retention_score();
assert!(
dbg!(previous_score) != dbg!(new_score),
"retention weight components shouldn't overshadow the other due to scale differences"
);
}
}