Skip to main content

torvyn_engine/
cache.rs

1//! Compiled component cache for fast instantiation.
2//!
3//! Caches [`CompiledComponent`] objects by [`ComponentTypeId`] to avoid
4//! recompilation. When the `wasmtime-backend` feature is enabled, also
5//! supports disk caching via Wasmtime's serialization.
6//!
7//! Per Doc 02, Section 2.4 and MR-06: `InstancePre` is available for
8//! Component Model and is used for pre-resolved import caching.
9
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13use parking_lot::RwLock;
14use sha2::{Digest, Sha256};
15
16use torvyn_types::ComponentTypeId;
17
18use crate::error::EngineError;
19use crate::traits::WasmEngine;
20use crate::types::CompiledComponent;
21
22/// Cache for compiled WebAssembly components.
23///
24/// Provides both in-memory and optional disk caching. Components are
25/// keyed by [`ComponentTypeId`] (SHA-256 of the binary).
26///
27/// Thread-safe: uses `RwLock` for concurrent read access.
28///
29/// # COLD PATH — all operations are during pipeline setup.
30///
31/// # Examples
32/// ```
33/// use torvyn_engine::CompiledComponentCache;
34///
35/// let cache = CompiledComponentCache::new(None);
36/// assert_eq!(cache.len(), 0);
37/// ```
38pub struct CompiledComponentCache {
39    /// In-memory cache of compiled components.
40    memory: RwLock<HashMap<ComponentTypeId, CompiledComponent>>,
41
42    /// Optional disk cache directory.
43    disk_dir: Option<PathBuf>,
44}
45
46impl CompiledComponentCache {
47    /// Create a new cache with an optional disk cache directory.
48    ///
49    /// # COLD PATH
50    pub fn new(disk_dir: Option<PathBuf>) -> Self {
51        Self {
52            memory: RwLock::new(HashMap::new()),
53            disk_dir,
54        }
55    }
56
57    /// Compute the [`ComponentTypeId`] for a component binary.
58    ///
59    /// Uses SHA-256 to produce a deterministic content hash.
60    ///
61    /// # COLD PATH
62    ///
63    /// # Examples
64    /// ```
65    /// use torvyn_engine::CompiledComponentCache;
66    ///
67    /// let id = CompiledComponentCache::compute_type_id(b"hello");
68    /// assert_eq!(id.as_bytes().len(), 32);
69    /// ```
70    pub fn compute_type_id(bytes: &[u8]) -> ComponentTypeId {
71        let mut hasher = Sha256::new();
72        hasher.update(bytes);
73        let hash: [u8; 32] = hasher.finalize().into();
74        ComponentTypeId::new(hash)
75    }
76
77    /// Look up a compiled component in the cache (memory first, then disk).
78    ///
79    /// # COLD PATH
80    ///
81    /// # Returns
82    /// - `Ok(Some(compiled))` if found in cache.
83    /// - `Ok(None)` if not in cache.
84    /// - `Err` if disk cache read failed fatally.
85    pub fn get<E: WasmEngine>(
86        &self,
87        type_id: &ComponentTypeId,
88        engine: &E,
89    ) -> Result<Option<CompiledComponent>, EngineError> {
90        // Check memory cache first (fast path).
91        {
92            let guard = self.memory.read();
93            if let Some(compiled) = guard.get(type_id) {
94                return Ok(Some(compiled.clone()));
95            }
96        }
97
98        // Check disk cache.
99        if let Some(ref dir) = self.disk_dir {
100            let path = disk_cache_path(dir, type_id);
101            if path.exists() {
102                if let Ok(bytes) = std::fs::read(&path) {
103                    // SAFETY: The cached bytes were produced by our own
104                    // serialize_component and stored in a directory we control.
105                    match unsafe { engine.deserialize_component(&bytes) } {
106                        Ok(Some(compiled)) => {
107                            // Promote to memory cache.
108                            let mut guard = self.memory.write();
109                            guard.insert(*type_id, compiled.clone());
110                            return Ok(Some(compiled));
111                        }
112                        Ok(None) => {
113                            // Incompatible cache entry — remove stale file.
114                            let _ = std::fs::remove_file(&path);
115                        }
116                        Err(_e) => {
117                            // Corrupt cache entry — remove.
118                            let _ = std::fs::remove_file(&path);
119                        }
120                    }
121                }
122            }
123        }
124
125        Ok(None)
126    }
127
128    /// Insert a compiled component into the cache.
129    ///
130    /// # COLD PATH
131    ///
132    /// Stores in memory. If disk caching is enabled, also writes to disk
133    /// (failures are non-fatal).
134    pub fn insert<E: WasmEngine>(
135        &self,
136        type_id: ComponentTypeId,
137        compiled: CompiledComponent,
138        engine: &E,
139    ) {
140        // Insert into memory cache.
141        {
142            let mut guard = self.memory.write();
143            guard.insert(type_id, compiled.clone());
144        }
145
146        // Write to disk cache (best-effort).
147        if let Some(ref dir) = self.disk_dir {
148            if let Ok(bytes) = engine.serialize_component(&compiled) {
149                let path = disk_cache_path(dir, &type_id);
150                if let Some(parent) = path.parent() {
151                    let _ = std::fs::create_dir_all(parent);
152                }
153                let _ = std::fs::write(&path, &bytes);
154            }
155        }
156    }
157
158    /// Returns the number of components in the memory cache.
159    pub fn len(&self) -> usize {
160        self.memory.read().len()
161    }
162
163    /// Returns `true` if the memory cache is empty.
164    pub fn is_empty(&self) -> bool {
165        self.memory.read().is_empty()
166    }
167
168    /// Clear the in-memory cache.
169    pub fn clear(&self) {
170        self.memory.write().clear();
171    }
172
173    /// Compile a component with caching.
174    ///
175    /// Checks the cache first. If not found, compiles the component
176    /// and inserts it into the cache.
177    ///
178    /// # COLD PATH
179    pub fn compile_or_get<E: WasmEngine>(
180        &self,
181        bytes: &[u8],
182        engine: &E,
183    ) -> Result<(ComponentTypeId, CompiledComponent), EngineError> {
184        let type_id = Self::compute_type_id(bytes);
185
186        // Check cache.
187        if let Some(compiled) = self.get(&type_id, engine)? {
188            return Ok((type_id, compiled));
189        }
190
191        // Cache miss — compile.
192        let compiled = engine.compile_component(bytes)?;
193
194        // Insert into cache.
195        self.insert(type_id, compiled.clone(), engine);
196
197        Ok((type_id, compiled))
198    }
199}
200
201/// Compute the disk cache file path for a given component type ID.
202///
203/// Uses a two-level directory structure: `{dir}/{first_2_hex}/{full_hex}.bin`
204fn disk_cache_path(dir: &Path, type_id: &ComponentTypeId) -> PathBuf {
205    let hex = format!("{type_id}");
206    let prefix = &hex[..2.min(hex.len())];
207    dir.join(prefix).join(format!("{hex}.bin"))
208}
209
210// ---------------------------------------------------------------------------
211// Tests
212// ---------------------------------------------------------------------------
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_compute_type_id_deterministic() {
220        let id1 = CompiledComponentCache::compute_type_id(b"hello world");
221        let id2 = CompiledComponentCache::compute_type_id(b"hello world");
222        assert_eq!(id1, id2);
223    }
224
225    #[test]
226    fn test_compute_type_id_different_inputs() {
227        let id1 = CompiledComponentCache::compute_type_id(b"hello");
228        let id2 = CompiledComponentCache::compute_type_id(b"world");
229        assert_ne!(id1, id2);
230    }
231
232    #[test]
233    fn test_cache_new_empty() {
234        let cache = CompiledComponentCache::new(None);
235        assert_eq!(cache.len(), 0);
236        assert!(cache.is_empty());
237    }
238
239    #[test]
240    fn test_cache_clear() {
241        let cache = CompiledComponentCache::new(None);
242        cache.clear();
243        assert!(cache.is_empty());
244    }
245
246    #[test]
247    fn test_disk_cache_path_format() {
248        let type_id = ComponentTypeId::new([0xab; 32]);
249        let path = disk_cache_path(Path::new("/tmp/cache"), &type_id);
250        let path_str = path.to_string_lossy();
251        assert!(path_str.contains("cache"));
252        assert!(path_str.ends_with(".bin"));
253    }
254}