arb_stylus/
cache.rs

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