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