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            && self.is_tombstone() == other.is_tombstone()
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(0),
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(deployment_slot: Slot, register_fn: BuiltinFunctionRegisterer) -> Self {
285        let mut program = BuiltinProgram::new_builtin();
286        register_fn(&mut program, "entrypoint").unwrap();
287        Self {
288            deployment_slot,
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 effective_slot(&self) -> Slot {
351        match self.program {
352            ProgramCacheEntryType::Closed
353            | ProgramCacheEntryType::DelayVisibility
354            | ProgramCacheEntryType::FailedVerification(_)
355            | ProgramCacheEntryType::Builtin(_) => self.deployment_slot,
356            ProgramCacheEntryType::Unloaded(_) | ProgramCacheEntryType::Loaded(_) => self
357                .deployment_slot
358                .saturating_add(DELAY_VISIBILITY_SLOT_OFFSET),
359        }
360    }
361
362    pub fn update_access_slot(&self, slot: Slot) {
363        let _ = self.latest_access_slot.fetch_max(slot, Ordering::Relaxed);
364    }
365
366    /// Compute a retention score.
367    ///
368    /// Eviction uses an adapted GDSF scheme which incorporates frequency, recovery cost
369    /// (recompilation) and time-based decay.
370    ///
371    /// How hard should we try to retain this entry. Higher number -> retention more likely.
372    pub fn retention_score(&self) -> u64 {
373        let last_access = self.latest_access_slot.load(Ordering::Relaxed);
374        let recovery_cost = self.stats.compilation_time_ema.load(Ordering::Relaxed);
375        let frequency = self.stats.uses.load(Ordering::Relaxed);
376        retention_score(last_access, recovery_cost, frequency)
377    }
378
379    pub fn account_owner(&self) -> Pubkey {
380        self.account_owner.into()
381    }
382}
383
384/// See [`ProgramCacheEntry::retention_score`].
385pub(crate) const fn retention_score(last_access: u64, recovery_cost: u64, frequency: u64) -> u64 {
386    // Traditionally GDSF uses the following logic:
387    //
388    // on_access:
389    //   entry.frequency += 1
390    //   entry.H := cache.L + (entry.cost * entry.frequency) / entry.size
391    //
392    // on_eviction:
393    //   victim = pick_victim_minimizing_H()
394    //   cache.L := victim.H
395    //
396    // It achieves decay by virtue of L increasing over time (and therefore the “value” of
397    // stored score of each entry decreasing over time.) Entry recovery and frequency, as well
398    // as size are otherwise also accounted for by them inflating the overall score by a bit.
399    //
400    // We adapt this algorithm slightly: we already have a kind of `L` – access slot. It does
401    // not include the weight of the evicted entry as the original algorithm does, that is
402    // *probably* fine (the author has not done any empirical experiments to verify it it
403    // actually matters.)
404    //
405    // Additionally we ignore the size component altogether as irrelevant and instead of
406    // applying entry weight linearly, we use a `log_2`. We can't use plain `weight*frequency`
407    // as the most heavily used entries would never ever get evicted after just some runtime,
408    // even if they're no longer used. With `log_2` weight and frequency can contribute to
409    // up-to 128 slots of "bonus" towards their retention compared to rarely used peers.
410    //
411    // Feel free to adjust the specific formulae used.
412    let weight = (recovery_cost as u128).wrapping_mul(frequency as u128);
413    let weight_log = u128::BITS.wrapping_sub(weight.leading_zeros());
414    last_access.saturating_add(weight_log as u64)
415}
416
417#[cfg(test)]
418mod tests {
419    use {
420        crate::{
421            loaded_programs::tests::new_test_entry_with_usage, program_metrics::ProgramStatistics,
422        },
423        std::sync::atomic::{AtomicU64, Ordering},
424    };
425
426    #[test]
427    fn test_retention_score_decay_horizon() {
428        let stats = ProgramStatistics {
429            uses: AtomicU64::new(u64::MAX),
430            compilation_time_ema: AtomicU64::new(u64::MAX),
431            ..Default::default()
432        };
433        let program = new_test_entry_with_usage(0, stats);
434        program.update_access_slot(1);
435        assert!(
436            dbg!(program.retention_score()) <= 129,
437            "retention score should remain within sensible boundaries even for very frequently \
438             used entries."
439        );
440    }
441
442    #[test]
443    fn test_retention_score_frequency_preference() {
444        let stats = ProgramStatistics {
445            uses: AtomicU64::new(16),
446            compilation_time_ema: AtomicU64::new(1),
447            ..Default::default()
448        };
449        let program = new_test_entry_with_usage(10, stats);
450        program.update_access_slot(15);
451        let less_used_retention_score = program.retention_score();
452        program.stats.uses.fetch_max(1024, Ordering::Relaxed);
453        let more_used_retention_score = program.retention_score();
454        assert!(
455            less_used_retention_score > 15,
456            "frequency should count for entry retention score"
457        );
458        assert!(
459            dbg!(more_used_retention_score) > dbg!(less_used_retention_score),
460            "retention score should prefer evicting less used entry over the more used one if \
461             possible"
462        );
463    }
464
465    #[test]
466    fn test_retention_score_recovery_time_preference() {
467        let stats = ProgramStatistics {
468            uses: AtomicU64::new(1),
469            compilation_time_ema: AtomicU64::new(1000),
470            ..Default::default()
471        };
472        let program = new_test_entry_with_usage(10, stats);
473        program.update_access_slot(15);
474        let cheaper_to_compile_score = program.retention_score();
475        program
476            .stats
477            .compilation_time_ema
478            .fetch_max(2000, Ordering::Relaxed);
479        let more_expensive_to_compile_score = program.retention_score();
480        assert!(
481            cheaper_to_compile_score > 15,
482            "compile time should count for entry retention score"
483        );
484        assert!(
485            dbg!(more_expensive_to_compile_score) > dbg!(cheaper_to_compile_score),
486            "retention score should prefer evicting cheaper-to-compile entries"
487        );
488    }
489
490    #[test]
491    fn test_retention_weight_metric_does_not_outweight_smaller_metric() {
492        // Compilation time generally stays in the scale of 4 digits, while the uses counter can
493        // become many millions. Neither should overshadow other too much.
494        let stats = ProgramStatistics {
495            uses: AtomicU64::new(100_000_000),
496            compilation_time_ema: AtomicU64::new(1000),
497            ..Default::default()
498        };
499        let program = new_test_entry_with_usage(10, stats);
500        program.update_access_slot(15);
501        let previous_score = program.retention_score();
502        program
503            .stats
504            .compilation_time_ema
505            .fetch_max(2000, Ordering::Relaxed);
506        let new_score = program.retention_score();
507        assert!(
508            dbg!(previous_score) != dbg!(new_score),
509            "retention weight components shouldn't overshadow the other due to scale differences"
510        );
511    }
512}