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}