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)]
101pub enum ProgramCacheEntryType {
102 FailedVerification(ProgramRuntimeEnvironment),
104 #[default]
108 Closed,
109 DelayVisibility,
111 Unloaded(ProgramRuntimeEnvironment),
115 Loaded(Executable<InvokeContext<'static, 'static>>),
119 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 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#[derive(Default)]
157pub struct ProgramCacheEntry {
158 pub program: ProgramCacheEntryType,
160 pub account_owner: ProgramCacheEntryOwner,
162 pub deployment_slot: Slot,
164 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 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 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 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
384pub(crate) const fn retention_score(last_access: u64, recovery_cost: u64, frequency: u64) -> u64 {
386 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 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}