use alloc::boxed::Box; use crate::unwind_rule::UnwindRule; const CACHE_ENTRY_COUNT: usize = 509; pub struct RuleCache { entries: Box<[Option>; CACHE_ENTRY_COUNT]>, stats: CacheStats, } impl RuleCache { pub fn new() -> Self { Self { entries: Box::new([None; CACHE_ENTRY_COUNT]), stats: CacheStats::new(), } } pub fn lookup(&mut self, address: u64, modules_generation: u16) -> CacheResult { let slot = (address % (CACHE_ENTRY_COUNT as u64)) as u16; match &self.entries[slot as usize] { None => { self.stats.miss_empty_slot_count += 1; } Some(entry) => { if entry.modules_generation == modules_generation { if entry.address == address { self.stats.hit_count += 1; return CacheResult::Hit(entry.unwind_rule); } else { self.stats.miss_wrong_address_count += 1; } } else { self.stats.miss_wrong_modules_count += 1; } } } CacheResult::Miss(CacheHandle { slot, address, modules_generation, }) } pub fn insert(&mut self, handle: CacheHandle, unwind_rule: R) { let CacheHandle { slot, address, modules_generation, } = handle; self.entries[slot as usize] = Some(CacheEntry { address, modules_generation, unwind_rule, }); } /// Returns a snapshot of the cache usage statistics. pub fn stats(&self) -> CacheStats { self.stats } } pub enum CacheResult { Miss(CacheHandle), Hit(R), } pub struct CacheHandle { slot: u16, address: u64, modules_generation: u16, } const _: () = assert!( CACHE_ENTRY_COUNT as u64 <= u16::MAX as u64, "u16 should be sufficient to store the cache slot index" ); #[derive(Clone, Copy, Debug)] struct CacheEntry { address: u64, modules_generation: u16, unwind_rule: R, } /// Statistics about the effectiveness of the rule cache. #[derive(Default, Debug, Clone, Copy)] pub struct CacheStats { /// The number of successful cache hits. pub hit_count: u64, /// The number of cache misses that were due to an empty slot. pub miss_empty_slot_count: u64, /// The number of cache misses that were due to a filled slot whose module /// generation didn't match the unwinder's current module generation. /// (This means that either the unwinder's modules have changed since the /// rule in this slot was stored, or the same cache is used with multiple /// unwinders and the unwinders are stomping on each other's cache slots.) pub miss_wrong_modules_count: u64, /// The number of cache misses that were due to cache slot collisions of /// different addresses. pub miss_wrong_address_count: u64, } impl CacheStats { /// Create a new instance. pub fn new() -> Self { Default::default() } /// The number of total lookups. pub fn total(&self) -> u64 { self.hits() + self.misses() } /// The number of total hits. pub fn hits(&self) -> u64 { self.hit_count } /// The number of total misses. pub fn misses(&self) -> u64 { self.miss_empty_slot_count + self.miss_wrong_modules_count + self.miss_wrong_address_count } } #[cfg(test)] mod tests { use crate::{aarch64::UnwindRuleAarch64, x86_64::UnwindRuleX86_64}; use super::*; // Ensure that the size of Option> doesn't change by accident. #[test] fn test_cache_entry_size() { assert_eq!( core::mem::size_of::>>(), 16 ); assert_eq!( core::mem::size_of::>>(), 24 // <-- larger than we'd like ); } }