arb_stylus/
cache.rs

1use alloy_primitives::B256;
2use parking_lot::Mutex;
3use std::collections::HashMap;
4use wasmer::{Engine, Module, Store};
5
6use crate::config::CompileConfig;
7
8lazy_static::lazy_static! {
9    static ref INIT_CACHE: Mutex<InitCache> = Mutex::new(InitCache::new());
10}
11
12macro_rules! cache {
13    () => {
14        INIT_CACHE.lock()
15    };
16}
17
18/// Counters for LRU cache hit/miss tracking.
19#[derive(Debug, Default)]
20pub struct LruCounters {
21    pub hits: u32,
22    pub misses: u32,
23    pub does_not_fit: u32,
24}
25
26/// Counters for long-term cache hit/miss tracking.
27#[derive(Debug, Default)]
28pub struct LongTermCounters {
29    pub hits: u32,
30    pub misses: u32,
31}
32
33/// Two-tier module cache: LRU for hot modules, long-term for ArbOS-pinned modules.
34pub struct InitCache {
35    long_term: HashMap<CacheKey, CacheItem>,
36    long_term_size_bytes: usize,
37    long_term_counters: LongTermCounters,
38
39    lru: HashMap<CacheKey, CacheItem>,
40    lru_capacity: usize,
41    lru_counters: LruCounters,
42}
43
44#[derive(Clone, Copy, Hash, PartialEq, Eq)]
45struct CacheKey {
46    module_hash: B256,
47    version: u16,
48    debug: bool,
49}
50
51impl CacheKey {
52    fn new(module_hash: B256, version: u16, debug: bool) -> Self {
53        Self {
54            module_hash,
55            version,
56            debug,
57        }
58    }
59}
60
61#[derive(Clone)]
62struct CacheItem {
63    module: Module,
64    engine: Engine,
65    entry_size_estimate_bytes: usize,
66}
67
68impl CacheItem {
69    fn new(module: Module, engine: Engine, entry_size_estimate_bytes: usize) -> Self {
70        Self {
71            module,
72            engine,
73            entry_size_estimate_bytes,
74        }
75    }
76
77    fn data(&self) -> (Module, Store) {
78        (self.module.clone(), Store::new(self.engine.clone()))
79    }
80}
81
82/// LRU cache metrics.
83#[derive(Debug, Default)]
84pub struct LruCacheMetrics {
85    pub size_bytes: u64,
86    pub count: u32,
87    pub hits: u32,
88    pub misses: u32,
89    pub does_not_fit: u32,
90}
91
92/// Long-term cache metrics.
93#[derive(Debug, Default)]
94pub struct LongTermCacheMetrics {
95    pub size_bytes: u64,
96    pub count: u32,
97    pub hits: u32,
98    pub misses: u32,
99}
100
101/// Combined cache metrics.
102#[derive(Debug, Default)]
103pub struct CacheMetrics {
104    pub lru: LruCacheMetrics,
105    pub long_term: LongTermCacheMetrics,
106}
107
108/// Deserialize a WASM module from compiled bytes.
109pub fn deserialize_module(
110    module: &[u8],
111    version: u16,
112    debug: bool,
113) -> eyre::Result<(Module, Engine, usize)> {
114    let compile = CompileConfig::version(version, debug);
115    let engine = compile.engine();
116    let module = unsafe { Module::deserialize_unchecked(&engine, module)? };
117    let asm_size_estimate_bytes = module.serialize()?.len();
118    let entry_size_estimate_bytes = asm_size_estimate_bytes + 128;
119    Ok((module, engine, entry_size_estimate_bytes))
120}
121
122impl CompileConfig {
123    /// Create a wasmer Engine with the configured middleware.
124    pub fn engine(&self) -> Engine {
125        use std::sync::Arc;
126        use wasmer::{sys::EngineBuilder, CompilerConfig, Cranelift, CraneliftOptLevel};
127
128        use crate::middleware;
129
130        let mut cranelift = Cranelift::new();
131        cranelift.opt_level(CraneliftOptLevel::Speed);
132        cranelift.canonicalize_nans(true);
133
134        if self.pricing.ink_header_cost > 0 {
135            // Middleware order:
136            //   StartMover -> InkMeter -> DynamicMeter -> DepthChecker -> HeapBound
137            cranelift.push_middleware(Arc::new(middleware::StartMover::new(self.debug.debug_info)));
138            cranelift.push_middleware(Arc::new(middleware::InkMeter::new(
139                self.pricing.ink_header_cost,
140            )));
141            cranelift.push_middleware(Arc::new(middleware::DynamicMeter::new(
142                self.pricing.memory_fill_ink,
143                self.pricing.memory_copy_ink,
144            )));
145            cranelift.push_middleware(Arc::new(middleware::DepthChecker::new(
146                self.bounds.max_frame_size,
147                self.bounds.max_frame_contention,
148            )));
149            cranelift.push_middleware(Arc::new(middleware::HeapBound::new()));
150        }
151
152        EngineBuilder::new(cranelift).into()
153    }
154
155    /// Create a wasmer Store from this config.
156    pub fn store(&self) -> Store {
157        Store::new(self.engine())
158    }
159}
160
161impl InitCache {
162    const ARBOS_TAG: u32 = 1;
163    const DEFAULT_LRU_CAPACITY: usize = 1024;
164
165    fn new() -> Self {
166        Self {
167            long_term: HashMap::new(),
168            long_term_size_bytes: 0,
169            long_term_counters: LongTermCounters::default(),
170            lru: HashMap::new(),
171            lru_capacity: Self::DEFAULT_LRU_CAPACITY,
172            lru_counters: LruCounters::default(),
173        }
174    }
175
176    /// Set the LRU cache capacity.
177    pub fn set_lru_capacity(capacity: u32) {
178        cache!().lru_capacity = capacity as usize;
179    }
180
181    /// Retrieve a cached module.
182    pub fn get(
183        module_hash: B256,
184        version: u16,
185        long_term_tag: u32,
186        debug: bool,
187    ) -> Option<(Module, Store)> {
188        let key = CacheKey::new(module_hash, version, debug);
189        let mut cache = cache!();
190
191        if let Some(item) = cache.long_term.get(&key) {
192            let data = item.data();
193            cache.long_term_counters.hits += 1;
194            return Some(data);
195        }
196        if long_term_tag == Self::ARBOS_TAG {
197            cache.long_term_counters.misses += 1;
198        }
199
200        if let Some(item) = cache.lru.get(&key).cloned() {
201            cache.lru_counters.hits += 1;
202            if long_term_tag == Self::ARBOS_TAG {
203                cache.long_term_size_bytes += item.entry_size_estimate_bytes;
204                cache.long_term.insert(key, item.clone());
205            }
206            return Some(item.data());
207        }
208        cache.lru_counters.misses += 1;
209
210        None
211    }
212
213    /// Insert a module into the cache.
214    pub fn insert(
215        module_hash: B256,
216        module: &[u8],
217        version: u16,
218        long_term_tag: u32,
219        debug: bool,
220    ) -> eyre::Result<(Module, Store)> {
221        let key = CacheKey::new(module_hash, version, debug);
222        let mut cache = cache!();
223
224        if let Some(item) = cache.long_term.get(&key) {
225            return Ok(item.data());
226        }
227        if let Some(item) = cache.lru.get(&key).cloned() {
228            if long_term_tag == Self::ARBOS_TAG {
229                cache.long_term_size_bytes += item.entry_size_estimate_bytes;
230                cache.long_term.insert(key, item.clone());
231            }
232            return Ok(item.data());
233        }
234        drop(cache);
235
236        let (module, engine, entry_size_estimate_bytes) =
237            deserialize_module(module, version, debug)?;
238        let item = CacheItem::new(module, engine, entry_size_estimate_bytes);
239        let data = item.data();
240
241        let mut cache = cache!();
242        if long_term_tag == Self::ARBOS_TAG {
243            cache.long_term_size_bytes += entry_size_estimate_bytes;
244            cache.long_term.insert(key, item);
245        } else {
246            // Simple eviction: if at capacity, remove an arbitrary entry
247            if cache.lru.len() >= cache.lru_capacity {
248                let first_key = cache.lru.keys().next().copied();
249                if let Some(k) = first_key {
250                    cache.lru.remove(&k);
251                }
252            }
253            cache.lru.insert(key, item);
254        }
255        Ok(data)
256    }
257
258    /// Evict a module from the long-term cache.
259    pub fn evict(module_hash: B256, version: u16, long_term_tag: u32, debug: bool) {
260        if long_term_tag != Self::ARBOS_TAG {
261            return;
262        }
263        let key = CacheKey::new(module_hash, version, debug);
264        let mut cache = cache!();
265        if let Some(item) = cache.long_term.remove(&key) {
266            cache.long_term_size_bytes -= item.entry_size_estimate_bytes;
267            cache.lru.insert(key, item);
268        }
269    }
270
271    /// Clear the long-term cache, moving items to LRU.
272    pub fn clear_long_term(long_term_tag: u32) {
273        if long_term_tag != Self::ARBOS_TAG {
274            return;
275        }
276        let mut cache = cache!();
277        let drained: Vec<_> = cache.long_term.drain().collect();
278        for (key, item) in drained {
279            cache.lru.insert(key, item);
280        }
281        cache.long_term_size_bytes = 0;
282    }
283
284    /// Get cache metrics, resetting counters.
285    pub fn get_metrics() -> CacheMetrics {
286        let mut cache = cache!();
287        let metrics = CacheMetrics {
288            lru: LruCacheMetrics {
289                size_bytes: cache.lru.len() as u64,
290                count: cache.lru.len() as u32,
291                hits: cache.lru_counters.hits,
292                misses: cache.lru_counters.misses,
293                does_not_fit: cache.lru_counters.does_not_fit,
294            },
295            long_term: LongTermCacheMetrics {
296                size_bytes: cache.long_term_size_bytes as u64,
297                count: cache.long_term.len() as u32,
298                hits: cache.long_term_counters.hits,
299                misses: cache.long_term_counters.misses,
300            },
301        };
302        cache.lru_counters = LruCounters::default();
303        cache.long_term_counters = LongTermCounters::default();
304        metrics
305    }
306
307    /// Clear the LRU cache.
308    pub fn clear_lru_cache() {
309        let mut cache = cache!();
310        cache.lru.clear();
311        cache.lru_counters = LruCounters::default();
312    }
313}