Skip to main content

solana_program_runtime/
program_cache_entry.rs

1#[cfg(feature = "metrics")]
2use crate::program_metrics::LoadProgramMetrics;
3use {
4    crate::{
5        invoke_context::{BuiltinFunctionRegisterer, InvokeContext},
6        loaded_programs::ProgramRuntimeEnvironment,
7        program_metrics::ProgramStatistics,
8    },
9    solana_clock::Slot,
10    solana_pubkey::Pubkey,
11    solana_sbpf::{elf::Executable, program::BuiltinProgram, verifier::RequisiteVerifier},
12    solana_sdk_ids::{
13        bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader,
14    },
15    solana_svm_type_overrides::sync::{
16        Arc,
17        atomic::{AtomicU64, Ordering},
18    },
19};
20
21pub const DELAY_VISIBILITY_SLOT_OFFSET: Slot = 1;
22
23/// The owner of a programs accounts, thus the loader of a program
24#[derive(Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Debug)]
25pub enum ProgramCacheEntryOwner {
26    #[default]
27    NativeLoader,
28    LoaderV1,
29    LoaderV2,
30    LoaderV3,
31    LoaderV4,
32}
33
34impl TryFrom<&Pubkey> for ProgramCacheEntryOwner {
35    type Error = ();
36    fn try_from(loader_key: &Pubkey) -> Result<Self, ()> {
37        if native_loader::check_id(loader_key) {
38            Ok(ProgramCacheEntryOwner::NativeLoader)
39        } else if bpf_loader_deprecated::check_id(loader_key) {
40            Ok(ProgramCacheEntryOwner::LoaderV1)
41        } else if bpf_loader::check_id(loader_key) {
42            Ok(ProgramCacheEntryOwner::LoaderV2)
43        } else if bpf_loader_upgradeable::check_id(loader_key) {
44            Ok(ProgramCacheEntryOwner::LoaderV3)
45        } else if loader_v4::check_id(loader_key) {
46            Ok(ProgramCacheEntryOwner::LoaderV4)
47        } else {
48            Err(())
49        }
50    }
51}
52
53impl From<ProgramCacheEntryOwner> for Pubkey {
54    fn from(program_cache_entry_owner: ProgramCacheEntryOwner) -> Self {
55        match program_cache_entry_owner {
56            ProgramCacheEntryOwner::NativeLoader => native_loader::id(),
57            ProgramCacheEntryOwner::LoaderV1 => bpf_loader_deprecated::id(),
58            ProgramCacheEntryOwner::LoaderV2 => bpf_loader::id(),
59            ProgramCacheEntryOwner::LoaderV3 => bpf_loader_upgradeable::id(),
60            ProgramCacheEntryOwner::LoaderV4 => loader_v4::id(),
61        }
62    }
63}
64
65/*
66    The possible ProgramCacheEntryType transitions:
67
68    DelayVisibility is special in that it is never stored in the cache.
69    It is only returned by ProgramCacheForTxBatch::find() when a Loaded entry
70    is encountered which is not effective yet.
71
72    Builtin re/deployment:
73    - Empty => Builtin in TransactionBatchProcessor::add_builtin
74    - Builtin => Builtin in TransactionBatchProcessor::add_builtin
75
76    Un/re/deployment (with delay and cooldown):
77    - Empty / Closed => Unloaded in UpgradeableLoaderInstruction::DeployWithMaxDataLen
78    - Loaded / FailedVerification => Loaded in UpgradeableLoaderInstruction::Upgrade
79    - Loaded / FailedVerification => Closed in UpgradeableLoaderInstruction::Close
80
81    Loader migration:
82    - Closed => Closed (in the same slot)
83    - FailedVerification => FailedVerification (with different account_owner)
84    - Loaded => Loaded (with different account_owner)
85
86    Eviction and unloading (in the same slot):
87    - Unloaded => Loaded / FailedVerification in ProgramCache::assign_program
88    - Loaded => Unloaded in ProgramCache::unload_program_entry
89
90    At epoch boundary (when feature set and environment changes):
91    - Loaded => FailedVerification in Bank::_new_from_parent
92    - FailedVerification => Loaded in Bank::_new_from_parent
93
94    Through pruning:
95    - Closed / Unloaded / Loaded / Builtin => Empty in ProgramCache::prune (when on orphan fork or overshadowed on the rooted fork)
96    - FailedVerification / Unloaded / Loaded => Unloaded in ProgramCache::prune (when on outdated program runtime environment)
97*/
98
99/// Actual payload of [ProgramCacheEntry].
100#[derive(Default)]
101pub enum ProgramCacheEntryType {
102    /// Tombstone for programs which currently do not pass the verifier but could if the feature set changed.
103    FailedVerification(ProgramRuntimeEnvironment),
104    /// Tombstone for programs that were either explicitly closed or never deployed.
105    ///
106    /// It's also used for accounts belonging to program loaders, that don't actually contain program code (e.g. buffer accounts for LoaderV3 programs).
107    #[default]
108    Closed,
109    /// Tombstone for programs which have recently been modified but the new version is not visible yet.
110    DelayVisibility,
111    /// Valid program account, but not relocated, verified or compiled.
112    ///
113    /// It continues to track usage statistics even when the executable of the program is evicted from memory.
114    Unloaded(ProgramRuntimeEnvironment),
115    /// Verified program.
116    ///
117    /// It may or may not be JIT compiled.
118    Loaded(Executable<InvokeContext<'static, 'static>>),
119    /// A built-in program which is not stored on-chain but backed into and distributed with the validator
120    Builtin(BuiltinProgram<InvokeContext<'static, 'static>>),
121}
122
123impl std::fmt::Debug for ProgramCacheEntryType {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct(match self {
126            ProgramCacheEntryType::FailedVerification(_) => {
127                "ProgramCacheEntryType::FailedVerification"
128            }
129            ProgramCacheEntryType::Closed => "ProgramCacheEntryType::Closed",
130            ProgramCacheEntryType::DelayVisibility => "ProgramCacheEntryType::DelayVisibility",
131            ProgramCacheEntryType::Unloaded(_) => "ProgramCacheEntryType::Unloaded",
132            ProgramCacheEntryType::Loaded(_) => "ProgramCacheEntryType::Loaded",
133            ProgramCacheEntryType::Builtin(_) => "ProgramCacheEntryType::Builtin",
134        })
135        .finish()
136    }
137}
138
139impl ProgramCacheEntryType {
140    /// Returns a reference to its environment if it has one
141    pub fn get_environment(&self) -> Option<&ProgramRuntimeEnvironment> {
142        match self {
143            ProgramCacheEntryType::Loaded(program) => {
144                Some(ProgramRuntimeEnvironment::from_ref(program.get_loader()))
145            }
146            ProgramCacheEntryType::FailedVerification(env)
147            | ProgramCacheEntryType::Unloaded(env) => Some(env),
148            _ => None,
149        }
150    }
151}
152
153/// Holds a program version at a specific address and on a specific slot / fork.
154///
155/// It contains the actual program in [ProgramCacheEntryType] and a bunch of meta-data.
156#[derive(Default)]
157pub struct ProgramCacheEntry {
158    /// The program of this entry
159    pub program: ProgramCacheEntryType,
160    /// The loader of this entry
161    pub account_owner: ProgramCacheEntryOwner,
162    /// Slot in which the program was (re)deployed
163    pub deployment_slot: Slot,
164    /// How often this entry was used by a transaction
165    pub stats: Arc<ProgramStatistics>,
166    pub latest_access_slot: AtomicU64,
167}
168
169impl std::fmt::Debug for ProgramCacheEntry {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        f.debug_struct("ProgramCacheEntry")
172            .field("slot", &self.deployment_slot)
173            .field(
174                "env",
175                &self
176                    .program
177                    .get_environment()
178                    .map(|env| Arc::as_ptr(env))
179                    .unwrap_or(std::ptr::null()),
180            )
181            .field("type", &self.program)
182            .finish()
183    }
184}
185
186#[cfg(feature = "dev-context-only-utils")]
187impl PartialEq for ProgramCacheEntry {
188    fn eq(&self, other: &Self) -> bool {
189        self.deployment_slot == other.deployment_slot
190            && self.account_owner == other.account_owner
191            && std::mem::discriminant(&self.program) == std::mem::discriminant(&other.program)
192    }
193}
194
195impl ProgramCacheEntry {
196    /// Creates a loaded user program
197    pub fn load(
198        loader_key: &Pubkey,
199        program_runtime_environment: ProgramRuntimeEnvironment,
200        deployment_slot: Slot,
201        elf_bytes: &[u8],
202        #[cfg(feature = "metrics")] metrics: &mut LoadProgramMetrics,
203    ) -> Result<Self, Box<dyn std::error::Error>> {
204        let entry_stats = ProgramStatistics::default();
205        #[cfg(feature = "metrics")]
206        let load_elf_time = solana_svm_measure::measure::Measure::start("load_elf_time");
207        let executable = Executable::load(elf_bytes, Arc::clone(&*program_runtime_environment))?;
208
209        #[cfg(feature = "metrics")]
210        {
211            metrics.load_elf_us = load_elf_time.end_as_us();
212        }
213
214        #[cfg(feature = "metrics")]
215        let verify_code_time = solana_svm_measure::measure::Measure::start("verify_code_time");
216        executable.verify::<RequisiteVerifier>()?;
217        #[cfg(feature = "metrics")]
218        {
219            metrics.verify_code_us = verify_code_time.end_as_us();
220        }
221
222        #[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))]
223        {
224            let jit_compile_time = solana_svm_measure::measure::Measure::start("jit_compile_time");
225            executable.jit_compile()?;
226            let jit_compile_time = jit_compile_time.end_as_us();
227            entry_stats.jit_compiled(jit_compile_time);
228            #[cfg(feature = "metrics")]
229            {
230                metrics.jit_compile_us = jit_compile_time;
231            }
232        }
233
234        Ok(Self {
235            deployment_slot,
236            account_owner: ProgramCacheEntryOwner::try_from(loader_key).unwrap(),
237            program: ProgramCacheEntryType::Loaded(executable),
238            stats: entry_stats.into(),
239            latest_access_slot: AtomicU64::new(0),
240        })
241    }
242
243    pub fn new_unloaded(
244        deployment_slot: Slot,
245        account_owner: ProgramCacheEntryOwner,
246        program_runtime_environment: ProgramRuntimeEnvironment,
247    ) -> Self {
248        Self {
249            program: ProgramCacheEntryType::Unloaded(program_runtime_environment),
250            account_owner,
251            deployment_slot,
252            stats: Arc::default(),
253            latest_access_slot: AtomicU64::new(deployment_slot),
254        }
255    }
256
257    pub fn to_unloaded_in_env(&self, environment: ProgramRuntimeEnvironment) -> Option<Self> {
258        match &self.program {
259            ProgramCacheEntryType::Loaded(_)
260            | ProgramCacheEntryType::FailedVerification(_)
261            | ProgramCacheEntryType::Unloaded(_) => {}
262            ProgramCacheEntryType::Closed
263            | ProgramCacheEntryType::DelayVisibility
264            | ProgramCacheEntryType::Builtin(_) => {
265                return None;
266            }
267        }
268        Some(Self {
269            program: ProgramCacheEntryType::Unloaded(environment),
270            account_owner: self.account_owner,
271            deployment_slot: self.deployment_slot,
272            stats: Arc::clone(&self.stats),
273            latest_access_slot: AtomicU64::new(self.latest_access_slot.load(Ordering::Relaxed)),
274        })
275    }
276
277    pub fn to_unloaded(&self) -> Option<Self> {
278        self.to_unloaded_in_env(ProgramRuntimeEnvironment::clone(
279            self.program.get_environment()?,
280        ))
281    }
282
283    /// Creates a new built-in program
284    pub fn new_builtin(register_fn: BuiltinFunctionRegisterer) -> Self {
285        let mut program = BuiltinProgram::new_builtin();
286        register_fn(&mut program, "entrypoint").unwrap();
287        Self {
288            deployment_slot: 0, // Deployment slot is always zero
289            account_owner: ProgramCacheEntryOwner::NativeLoader,
290            program: ProgramCacheEntryType::Builtin(program),
291            stats: Arc::default(),
292            latest_access_slot: AtomicU64::new(0),
293        }
294    }
295
296    pub fn new_failed_verification_tombstone(
297        deployment_slot: Slot,
298        account_owner: ProgramCacheEntryOwner,
299        program_runtime_environment: ProgramRuntimeEnvironment,
300    ) -> Self {
301        Self {
302            program: ProgramCacheEntryType::FailedVerification(program_runtime_environment),
303            account_owner,
304            deployment_slot,
305            stats: Arc::default(),
306            latest_access_slot: AtomicU64::new(0),
307        }
308    }
309
310    pub fn new_closed_tombstone(
311        deployment_slot: Slot,
312        account_owner: ProgramCacheEntryOwner,
313    ) -> Self {
314        Self {
315            program: ProgramCacheEntryType::Closed,
316            account_owner,
317            deployment_slot,
318            stats: Arc::default(),
319            latest_access_slot: AtomicU64::new(0),
320        }
321    }
322
323    pub fn new_delay_visibility_tombstone(
324        deployment_slot: Slot,
325        account_owner: ProgramCacheEntryOwner,
326        stats: Arc<ProgramStatistics>,
327    ) -> Self {
328        Self {
329            program: ProgramCacheEntryType::DelayVisibility,
330            account_owner,
331            deployment_slot,
332            stats,
333            latest_access_slot: AtomicU64::new(0),
334        }
335    }
336
337    pub fn is_tombstone(&self) -> bool {
338        matches!(
339            self.program,
340            ProgramCacheEntryType::FailedVerification(_)
341                | ProgramCacheEntryType::Closed
342                | ProgramCacheEntryType::DelayVisibility
343        )
344    }
345
346    pub(crate) fn is_implicit_delay_visibility_tombstone(&self, slot: Slot) -> bool {
347        slot >= self.deployment_slot && slot < self.effective_slot()
348    }
349
350    pub fn is_unloaded(&self) -> bool {
351        matches!(self.program, ProgramCacheEntryType::Unloaded(_))
352    }
353
354    pub fn effective_slot(&self) -> Slot {
355        match self.program {
356            ProgramCacheEntryType::Closed
357            | ProgramCacheEntryType::DelayVisibility
358            | ProgramCacheEntryType::FailedVerification(_)
359            | ProgramCacheEntryType::Builtin(_) => self.deployment_slot,
360            ProgramCacheEntryType::Unloaded(_) | ProgramCacheEntryType::Loaded(_) => self
361                .deployment_slot
362                .saturating_add(DELAY_VISIBILITY_SLOT_OFFSET),
363        }
364    }
365
366    pub fn update_access_slot(&self, slot: Slot) {
367        let _ = self.latest_access_slot.fetch_max(slot, Ordering::Relaxed);
368    }
369
370    /// Compute a retention score.
371    ///
372    /// Eviction uses an adapted GDSF scheme which incorporates frequency, recovery cost
373    /// (recompilation) and time-based decay.
374    ///
375    /// How hard should we try to retain this entry. Higher number -> retention more likely.
376    pub fn retention_score(&self) -> u64 {
377        let last_access = self.latest_access_slot.load(Ordering::Relaxed);
378        let recovery_cost = self.stats.compilation_time_ema.load(Ordering::Relaxed);
379        let frequency = self.stats.uses.load(Ordering::Relaxed);
380        retention_score(last_access, recovery_cost, frequency)
381    }
382
383    pub fn account_owner(&self) -> Pubkey {
384        self.account_owner.into()
385    }
386}
387
388/// See [`ProgramCacheEntry::retention_score`].
389pub(crate) const fn retention_score(last_access: u64, recovery_cost: u64, frequency: u64) -> u64 {
390    // Traditionally GDSF uses the following logic:
391    //
392    // on_access:
393    //   entry.frequency += 1
394    //   entry.H := cache.L + (entry.cost * entry.frequency) / entry.size
395    //
396    // on_eviction:
397    //   victim = pick_victim_minimizing_H()
398    //   cache.L := victim.H
399    //
400    // It achieves decay by virtue of L increasing over time (and therefore the “value” of
401    // stored score of each entry decreasing over time.) Entry recovery and frequency, as well
402    // as size are otherwise also accounted for by them inflating the overall score by a bit.
403    //
404    // We adapt this algorithm slightly: we already have a kind of `L` – access slot. It does
405    // not include the weight of the evicted entry as the original algorithm does, that is
406    // *probably* fine (the author has not done any empirical experiments to verify it it
407    // actually matters.)
408    //
409    // Additionally we ignore the size component altogether as irrelevant and instead of
410    // applying entry weight linearly, we use a `log_2`. We can't use plain `weight*frequency`
411    // as the most heavily used entries would never ever get evicted after just some runtime,
412    // even if they're no longer used. With `log_2` weight and frequency can contribute to
413    // up-to 128 slots of "bonus" towards their retention compared to rarely used peers.
414    //
415    // Feel free to adjust the specific formulae used.
416    let weight = (recovery_cost as u128).wrapping_mul(frequency as u128);
417    let weight_log = u128::BITS.wrapping_sub(weight.leading_zeros());
418    last_access.saturating_add(weight_log as u64)
419}
420
421#[cfg(test)]
422mod tests {
423    use {
424        crate::{
425            loaded_programs::tests::new_test_entry_with_usage, program_metrics::ProgramStatistics,
426        },
427        std::sync::atomic::{AtomicU64, Ordering},
428    };
429
430    #[test]
431    fn test_retention_score_decay_horizon() {
432        let stats = ProgramStatistics {
433            uses: AtomicU64::new(u64::MAX),
434            compilation_time_ema: AtomicU64::new(u64::MAX),
435            ..Default::default()
436        };
437        let program = new_test_entry_with_usage(0, stats);
438        program.update_access_slot(1);
439        assert!(
440            dbg!(program.retention_score()) <= 129,
441            "retention score should remain within sensible boundaries even for very frequently \
442             used entries."
443        );
444    }
445
446    #[test]
447    fn test_retention_score_frequency_preference() {
448        let stats = ProgramStatistics {
449            uses: AtomicU64::new(16),
450            compilation_time_ema: AtomicU64::new(1),
451            ..Default::default()
452        };
453        let program = new_test_entry_with_usage(10, stats);
454        program.update_access_slot(15);
455        let less_used_retention_score = program.retention_score();
456        program.stats.uses.fetch_max(1024, Ordering::Relaxed);
457        let more_used_retention_score = program.retention_score();
458        assert!(
459            less_used_retention_score > 15,
460            "frequency should count for entry retention score"
461        );
462        assert!(
463            dbg!(more_used_retention_score) > dbg!(less_used_retention_score),
464            "retention score should prefer evicting less used entry over the more used one if \
465             possible"
466        );
467    }
468
469    #[test]
470    fn test_retention_score_recovery_time_preference() {
471        let stats = ProgramStatistics {
472            uses: AtomicU64::new(1),
473            compilation_time_ema: AtomicU64::new(1000),
474            ..Default::default()
475        };
476        let program = new_test_entry_with_usage(10, stats);
477        program.update_access_slot(15);
478        let cheaper_to_compile_score = program.retention_score();
479        program
480            .stats
481            .compilation_time_ema
482            .fetch_max(2000, Ordering::Relaxed);
483        let more_expensive_to_compile_score = program.retention_score();
484        assert!(
485            cheaper_to_compile_score > 15,
486            "compile time should count for entry retention score"
487        );
488        assert!(
489            dbg!(more_expensive_to_compile_score) > dbg!(cheaper_to_compile_score),
490            "retention score should prefer evicting cheaper-to-compile entries"
491        );
492    }
493
494    #[test]
495    fn test_retention_weight_metric_does_not_outweight_smaller_metric() {
496        // Compilation time generally stays in the scale of 4 digits, while the uses counter can
497        // become many millions. Neither should overshadow other too much.
498        let stats = ProgramStatistics {
499            uses: AtomicU64::new(100_000_000),
500            compilation_time_ema: AtomicU64::new(1000),
501            ..Default::default()
502        };
503        let program = new_test_entry_with_usage(10, stats);
504        program.update_access_slot(15);
505        let previous_score = program.retention_score();
506        program
507            .stats
508            .compilation_time_ema
509            .fetch_max(2000, Ordering::Relaxed);
510        let new_score = program.retention_score();
511        assert!(
512            dbg!(previous_score) != dbg!(new_score),
513            "retention weight components shouldn't overshadow the other due to scale differences"
514        );
515    }
516}