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            cranelift.push_middleware(Arc::new(middleware::InkMeter::new(
136                self.pricing.ink_header_cost,
137            )));
138            cranelift.push_middleware(Arc::new(middleware::DynamicMeter::new(
139                self.pricing.memory_fill_ink,
140                self.pricing.memory_copy_ink,
141            )));
142            cranelift.push_middleware(Arc::new(middleware::DepthChecker::new(
143                self.bounds.max_frame_size,
144            )));
145            cranelift.push_middleware(Arc::new(middleware::HeapBound::new()));
146        }
147
148        EngineBuilder::new(cranelift).into()
149    }
150
151    /// Create a wasmer Store from this config.
152    pub fn store(&self) -> Store {
153        Store::new(self.engine())
154    }
155}
156
157impl InitCache {
158    const ARBOS_TAG: u32 = 1;
159    const DEFAULT_LRU_CAPACITY: usize = 1024;
160
161    fn new() -> Self {
162        Self {
163            long_term: HashMap::new(),
164            long_term_size_bytes: 0,
165            long_term_counters: LongTermCounters::default(),
166            lru: HashMap::new(),
167            lru_capacity: Self::DEFAULT_LRU_CAPACITY,
168            lru_counters: LruCounters::default(),
169        }
170    }
171
172    /// Set the LRU cache capacity.
173    pub fn set_lru_capacity(capacity: u32) {
174        cache!().lru_capacity = capacity as usize;
175    }
176
177    /// Retrieve a cached module.
178    pub fn get(
179        module_hash: B256,
180        version: u16,
181        long_term_tag: u32,
182        debug: bool,
183    ) -> Option<(Module, Store)> {
184        let key = CacheKey::new(module_hash, version, debug);
185        let mut cache = cache!();
186
187        if let Some(item) = cache.long_term.get(&key) {
188            let data = item.data();
189            cache.long_term_counters.hits += 1;
190            return Some(data);
191        }
192        if long_term_tag == Self::ARBOS_TAG {
193            cache.long_term_counters.misses += 1;
194        }
195
196        if let Some(item) = cache.lru.get(&key).cloned() {
197            cache.lru_counters.hits += 1;
198            if long_term_tag == Self::ARBOS_TAG {
199                cache.long_term_size_bytes += item.entry_size_estimate_bytes;
200                cache.long_term.insert(key, item.clone());
201            }
202            return Some(item.data());
203        }
204        cache.lru_counters.misses += 1;
205
206        None
207    }
208
209    /// Insert a module into the cache.
210    pub fn insert(
211        module_hash: B256,
212        module: &[u8],
213        version: u16,
214        long_term_tag: u32,
215        debug: bool,
216    ) -> eyre::Result<(Module, Store)> {
217        let key = CacheKey::new(module_hash, version, debug);
218        let mut cache = cache!();
219
220        if let Some(item) = cache.long_term.get(&key) {
221            return Ok(item.data());
222        }
223        if let Some(item) = cache.lru.get(&key).cloned() {
224            if long_term_tag == Self::ARBOS_TAG {
225                cache.long_term_size_bytes += item.entry_size_estimate_bytes;
226                cache.long_term.insert(key, item.clone());
227            }
228            return Ok(item.data());
229        }
230        drop(cache);
231
232        let (module, engine, entry_size_estimate_bytes) =
233            deserialize_module(module, version, debug)?;
234        let item = CacheItem::new(module, engine, entry_size_estimate_bytes);
235        let data = item.data();
236
237        let mut cache = cache!();
238        if long_term_tag == Self::ARBOS_TAG {
239            cache.long_term_size_bytes += entry_size_estimate_bytes;
240            cache.long_term.insert(key, item);
241        } else {
242            // Simple eviction: if at capacity, remove an arbitrary entry
243            if cache.lru.len() >= cache.lru_capacity {
244                let first_key = cache.lru.keys().next().copied();
245                if let Some(k) = first_key {
246                    cache.lru.remove(&k);
247                }
248            }
249            cache.lru.insert(key, item);
250        }
251        Ok(data)
252    }
253
254    /// Evict a module from the long-term cache.
255    pub fn evict(module_hash: B256, version: u16, long_term_tag: u32, debug: bool) {
256        if long_term_tag != Self::ARBOS_TAG {
257            return;
258        }
259        let key = CacheKey::new(module_hash, version, debug);
260        let mut cache = cache!();
261        if let Some(item) = cache.long_term.remove(&key) {
262            cache.long_term_size_bytes -= item.entry_size_estimate_bytes;
263            cache.lru.insert(key, item);
264        }
265    }
266
267    /// Clear the long-term cache, moving items to LRU.
268    pub fn clear_long_term(long_term_tag: u32) {
269        if long_term_tag != Self::ARBOS_TAG {
270            return;
271        }
272        let mut cache = cache!();
273        let drained: Vec<_> = cache.long_term.drain().collect();
274        for (key, item) in drained {
275            cache.lru.insert(key, item);
276        }
277        cache.long_term_size_bytes = 0;
278    }
279
280    /// Get cache metrics, resetting counters.
281    pub fn get_metrics() -> CacheMetrics {
282        let mut cache = cache!();
283        let metrics = CacheMetrics {
284            lru: LruCacheMetrics {
285                size_bytes: cache.lru.len() as u64,
286                count: cache.lru.len() as u32,
287                hits: cache.lru_counters.hits,
288                misses: cache.lru_counters.misses,
289                does_not_fit: cache.lru_counters.does_not_fit,
290            },
291            long_term: LongTermCacheMetrics {
292                size_bytes: cache.long_term_size_bytes as u64,
293                count: cache.long_term.len() as u32,
294                hits: cache.long_term_counters.hits,
295                misses: cache.long_term_counters.misses,
296            },
297        };
298        cache.lru_counters = LruCounters::default();
299        cache.long_term_counters = LongTermCounters::default();
300        metrics
301    }
302
303    /// Clear the LRU cache.
304    pub fn clear_lru_cache() {
305        let mut cache = cache!();
306        cache.lru.clear();
307        cache.lru_counters = LruCounters::default();
308    }
309}