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#[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#[derive(Default)]
100pub enum ProgramCacheEntryType {
101 FailedVerification(ProgramRuntimeEnvironment),
103 #[default]
107 Closed,
108 DelayVisibility,
110 Unloaded(ProgramRuntimeEnvironment),
114 Loaded(Executable<InvokeContext<'static, 'static>>),
118 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 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#[derive(Debug, Default)]
156pub struct ProgramCacheEntry {
157 pub program: ProgramCacheEntryType,
159 pub account_owner: ProgramCacheEntryOwner,
161 pub account_size: usize,
163 pub deployment_slot: Slot,
165 pub effective_slot: Slot,
167 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 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, )
203 }
204
205 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, )
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 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 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
395pub(crate) const fn retention_score(last_access: u64, recovery_cost: u64, frequency: u64) -> u64 {
397 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 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}