diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 923610c3b..d5d5f8812 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -279,6 +279,7 @@ mod executor_cache { pub evictions: HashMap, pub insertions: AtomicU64, pub replacements: AtomicU64, + pub one_hit_wonders: AtomicU64, } impl Stats { @@ -287,6 +288,7 @@ mod executor_cache { let misses = self.misses.load(Relaxed); let insertions = self.insertions.load(Relaxed); let replacements = self.replacements.load(Relaxed); + let one_hit_wonders = self.one_hit_wonders.load(Relaxed); let evictions: u64 = self.evictions.values().sum(); datapoint_info!( "bank-executor-cache-stats", @@ -296,10 +298,11 @@ mod executor_cache { ("evictions", evictions, i64), ("insertions", insertions, i64), ("replacements", replacements, i64), + ("one_hit_wonders", one_hit_wonders, i64), ); debug!( - "Executor Cache Stats -- Hits: {}, Misses: {}, Evictions: {}, Insertions: {}, Replacements: {}", - hits, misses, evictions, insertions, replacements, + "Executor Cache Stats -- Hits: {}, Misses: {}, Evictions: {}, Insertions: {}, Replacements: {}, One-Hit-Wonders: {}", + hits, misses, evictions, insertions, replacements, one_hit_wonders, ); if log_enabled!(log::Level::Trace) && !self.evictions.is_empty() { let mut evictions = self.evictions.iter().collect::>(); @@ -323,12 +326,13 @@ mod executor_cache { } } -const MAX_CACHED_EXECUTORS: usize = 100; // 10 MB assuming programs are around 100k +const MAX_CACHED_EXECUTORS: usize = 256; #[derive(Debug)] struct CachedExecutorsEntry { prev_epoch_count: u64, epoch_count: AtomicU64, executor: Arc, + hit_count: AtomicU64, } /// LFU Cache of executors with single-epoch memory of usage counts #[derive(Debug)] @@ -366,6 +370,7 @@ impl Clone for CachedExecutors { prev_epoch_count: entry.prev_epoch_count, epoch_count: AtomicU64::new(entry.epoch_count.load(Relaxed)), executor: entry.executor.clone(), + hit_count: AtomicU64::new(entry.hit_count.load(Relaxed)), }; (key, entry) }); @@ -390,6 +395,7 @@ impl CachedExecutors { prev_epoch_count: entry.epoch_count.load(Relaxed), epoch_count: AtomicU64::default(), executor: entry.executor.clone(), + hit_count: AtomicU64::new(entry.hit_count.load(Relaxed)), }; (key, entry) }); @@ -414,6 +420,7 @@ impl CachedExecutors { if let Some(entry) = self.executors.get(pubkey) { self.stats.hits.fetch_add(1, Relaxed); entry.epoch_count.fetch_add(1, Relaxed); + entry.hit_count.fetch_add(1, Relaxed); Some(entry.executor.clone()) } else { self.stats.misses.fetch_add(1, Relaxed); @@ -425,7 +432,7 @@ impl CachedExecutors { let mut new_executors: Vec<_> = executors .iter() .filter_map(|(key, executor)| { - if let Some(mut entry) = self.executors.remove(key) { + if let Some(mut entry) = self.remove(key) { self.stats.replacements.fetch_add(1, Relaxed); entry.executor = executor.clone(); let _ = self.executors.insert(**key, entry); @@ -457,7 +464,7 @@ impl CachedExecutors { .map(|least| *least.0) .collect::>(); for least_key in least_keys.drain(..) { - let _ = self.executors.remove(&least_key); + let _ = self.remove(&least_key); self.stats .evictions .entry(least_key) @@ -471,14 +478,21 @@ impl CachedExecutors { prev_epoch_count: 0, epoch_count: AtomicU64::new(primer_count), executor: executor.clone(), + hit_count: AtomicU64::new(1), }; let _ = self.executors.insert(*key, entry); } } } - fn remove(&mut self, pubkey: &Pubkey) { - let _ = self.executors.remove(pubkey); + fn remove(&mut self, pubkey: &Pubkey) -> Option { + let maybe_entry = self.executors.remove(pubkey); + if let Some(entry) = maybe_entry.as_ref() { + if entry.hit_count.load(Relaxed) == 1 { + self.stats.one_hit_wonders.fetch_add(1, Relaxed); + } + } + maybe_entry } fn clear(&mut self) { @@ -4181,7 +4195,7 @@ impl Bank { /// Remove an executor from the bank's cache fn remove_executor(&self, pubkey: &Pubkey) { let mut cache = self.cached_executors.write().unwrap(); - Arc::make_mut(&mut cache).remove(pubkey); + let _ = Arc::make_mut(&mut cache).remove(pubkey); } pub fn clear_executors(&self) { @@ -14509,6 +14523,39 @@ pub(crate) mod tests { assert!(cache.get(&entries[1].0).is_some()); } + #[test] + fn test_cached_executors_one_hit_wonder_counter() { + let mut cache = CachedExecutors::new(1, 0); + + let one_hit_wonder = Pubkey::new_unique(); + let popular = Pubkey::new_unique(); + let executor: Arc = Arc::new(TestExecutor {}); + + // make sure we're starting from where we think we are + assert_eq!(cache.stats.one_hit_wonders.load(Relaxed), 0); + + // add our one-hit-wonder + cache.put(&[(&one_hit_wonder, executor.clone())]); + assert_eq!(cache.executors[&one_hit_wonder].hit_count.load(Relaxed), 1); + // displace the one-hit-wonder with "popular program" + cache.put(&[(&popular, executor.clone())]); + assert_eq!(cache.executors[&popular].hit_count.load(Relaxed), 1); + + // one-hit-wonder counter incremented + assert_eq!(cache.stats.one_hit_wonders.load(Relaxed), 1); + + // make "popular program" popular + cache.get(&popular).unwrap(); + assert_eq!(cache.executors[&popular].hit_count.load(Relaxed), 2); + + // evict "popular program" + cache.put(&[(&one_hit_wonder, executor.clone())]); + assert_eq!(cache.executors[&one_hit_wonder].hit_count.load(Relaxed), 1); + + // one-hit-wonder counter not incremented + assert_eq!(cache.stats.one_hit_wonders.load(Relaxed), 1); + } + #[test] fn test_bank_executor_cache() { solana_logger::setup();