Skip to main content

javm/
image_cache.rs

1//! Image bytecode predecode cache.
2//!
3//! Predecoding a `javm_exec::PvmProgram` is expensive (basic-block
4//! analysis, gas cost computation, instruction predecoding); doing it
5//! per CALL would dominate the per-invocation cost. The cache is
6//! keyed by Image content_hash so identical Images share a single
7//! `Predecoded` (the bytecode is content-addressed; identical content
8//! always produces identical decoded state).
9//!
10//! Stage 3 stores predecoded `PvmProgram` directly. A future
11//! optimization can swap in JIT-compiled bytes for the same key and
12//! serve both paths from one cache.
13
14use std::collections::HashMap;
15use std::sync::Arc;
16
17use javm_cap::CapHash;
18use javm_exec::{PvmProgram, gas_cost::DEFAULT_MEM_CYCLES};
19
20use crate::error::VmError;
21
22/// Map from `Image::content_hash` to a parsed `PvmProgram`. The
23/// `PvmProgram` is wrapped in `Arc` so the same predecoded body can be
24/// referenced from multiple in-flight InstanceEntries (siblings) and
25/// from concurrent threads (the kernel may eventually be
26/// multi-threaded).
27#[derive(Default, Debug)]
28pub struct ImageCache {
29    entries: HashMap<CapHash, Arc<PvmProgram>>,
30}
31
32impl ImageCache {
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Number of cached images.
38    pub fn len(&self) -> usize {
39        self.entries.len()
40    }
41
42    pub fn is_empty(&self) -> bool {
43        self.entries.is_empty()
44    }
45
46    /// Look up by content hash. `None` if not yet cached.
47    pub fn get(&self, content_hash: &CapHash) -> Option<Arc<PvmProgram>> {
48        self.entries.get(content_hash).cloned()
49    }
50
51    /// Cache a precomputed program under the given content hash.
52    pub fn insert(&mut self, content_hash: CapHash, program: Arc<PvmProgram>) {
53        self.entries.insert(content_hash, program);
54    }
55
56    /// Look up or compute: if the image's content_hash is in the
57    /// cache, return the cached program; otherwise parse `code`,
58    /// `bitmask`, `jump_table` into a `PvmProgram`, cache it, and
59    /// return it.
60    pub fn get_or_decode(
61        &mut self,
62        content_hash: CapHash,
63        code: Vec<u8>,
64        bitmask: Vec<u8>,
65        jump_table: Vec<u32>,
66    ) -> Result<Arc<PvmProgram>, VmError> {
67        if let Some(prog) = self.entries.get(&content_hash) {
68            return Ok(prog.clone());
69        }
70        let prog = PvmProgram::new(code, bitmask, jump_table, DEFAULT_MEM_CYCLES)
71            .map_err(|e| VmError::InvalidBytecode(format!("{:?}", e)))?;
72        let arc = Arc::new(prog);
73        self.entries.insert(content_hash, arc.clone());
74        Ok(arc)
75    }
76
77    /// Drop all cached programs. Used at block boundaries by the
78    /// chain orchestrator if it wants to free memory.
79    pub fn clear(&mut self) {
80        self.entries.clear();
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    fn trivial_prog() -> (Vec<u8>, Vec<u8>, Vec<u32>) {
89        // Single trap instruction (opcode 0); bitmask marks it as an
90        // instruction start.
91        (vec![0u8], vec![1u8], vec![])
92    }
93
94    #[test]
95    fn miss_then_hit() {
96        let mut cache = ImageCache::new();
97        let h = [1u8; 32];
98        assert!(cache.get(&h).is_none());
99
100        let (code, bm, jt) = trivial_prog();
101        let p = cache.get_or_decode(h, code, bm, jt).unwrap();
102        assert_eq!(cache.len(), 1);
103        assert!(Arc::ptr_eq(&p, &cache.get(&h).unwrap()));
104    }
105
106    #[test]
107    fn get_or_decode_reuses_existing_entry() {
108        let mut cache = ImageCache::new();
109        let h = [2u8; 32];
110        let (c1, b1, j1) = trivial_prog();
111        let (c2, b2, j2) = trivial_prog();
112        let p1 = cache.get_or_decode(h, c1, b1, j1).unwrap();
113        let p2 = cache.get_or_decode(h, c2, b2, j2).unwrap();
114        // Same Arc — the cache returned the cached entry, not a
115        // freshly-decoded one.
116        assert!(Arc::ptr_eq(&p1, &p2));
117        assert_eq!(cache.len(), 1);
118    }
119
120    #[test]
121    fn clear_drops_entries() {
122        let mut cache = ImageCache::new();
123        let (c, b, j) = trivial_prog();
124        cache.get_or_decode([3u8; 32], c, b, j).unwrap();
125        assert_eq!(cache.len(), 1);
126        cache.clear();
127        assert!(cache.is_empty());
128    }
129
130    #[test]
131    fn invalid_bytecode_returns_err() {
132        let mut cache = ImageCache::new();
133        // bitmask len mismatch with code: PvmProgram::new returns
134        // `ProgramError::BitmaskLenMismatch`.
135        let res = cache.get_or_decode([4u8; 32], vec![0u8, 0u8], vec![1u8], vec![]);
136        assert!(matches!(res, Err(VmError::InvalidBytecode(_))));
137    }
138}