Skip to main content

lashlang/runtime/
cache.rs

1use crate::{
2    LASHLANG_COMPILER_VERSION, LASHLANG_VM_ABI_VERSION, LashlangSurface, LinkError, LinkedModule,
3    ModuleArtifact, ProcessRef, RequiredSurfaceRef,
4};
5
6use super::entry_points::{
7    compile_linked, compile_module_artifact_process, compile_program_internal,
8};
9use super::{CompiledProgram, prewarm};
10use rustc_hash::FxHasher;
11use std::borrow::Borrow;
12use std::collections::VecDeque;
13use std::hash::{Hash, Hasher};
14use std::sync::Arc;
15use thiserror::Error;
16
17const DEFAULT_COMPILED_PROGRAM_CACHE_CAPACITY: usize = 64;
18const DEFAULT_LINKED_PROGRAM_CACHE_CAPACITY: usize = 64;
19const SOURCE_CACHE_VERSION: &str = "lashlang-source-v1";
20
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub struct CompiledProgramCacheStats {
23    pub hits: u64,
24    pub misses: u64,
25    pub evictions: u64,
26    pub entries: usize,
27    pub capacity: usize,
28}
29
30const DEFAULT_COMPILED_PROCESS_CACHE_CAPACITY: usize = 64;
31
32#[derive(Clone, Debug, PartialEq, Eq, Hash)]
33pub struct CompiledProcessCacheKey {
34    pub process_ref: ProcessRef,
35    pub required_surface_ref: RequiredSurfaceRef,
36    pub compiler_version: &'static str,
37    pub vm_abi_version: &'static str,
38}
39
40impl CompiledProcessCacheKey {
41    pub fn new(process_ref: ProcessRef, required_surface_ref: RequiredSurfaceRef) -> Self {
42        Self {
43            process_ref,
44            required_surface_ref,
45            compiler_version: LASHLANG_COMPILER_VERSION,
46            vm_abi_version: LASHLANG_VM_ABI_VERSION,
47        }
48    }
49}
50
51pub struct CompiledProcessCache {
52    entries: VecDeque<CachedCompiledProcess>,
53    hits: u64,
54    misses: u64,
55    evictions: u64,
56    capacity: usize,
57}
58
59struct CachedCompiledProcess {
60    key: CompiledProcessCacheKey,
61    compiled: Arc<CompiledProgram>,
62}
63
64impl CompiledProcessCache {
65    pub fn new() -> Self {
66        Self::with_capacity(DEFAULT_COMPILED_PROCESS_CACHE_CAPACITY)
67    }
68
69    pub fn with_capacity(capacity: usize) -> Self {
70        prewarm();
71        Self {
72            entries: VecDeque::with_capacity(capacity),
73            hits: 0,
74            misses: 0,
75            evictions: 0,
76            capacity,
77        }
78    }
79
80    pub fn get_or_compile(
81        &mut self,
82        artifact: &ModuleArtifact,
83        process_ref: &ProcessRef,
84        required_surface_ref: &RequiredSurfaceRef,
85    ) -> Result<Arc<CompiledProgram>, crate::RuntimeError> {
86        let key = CompiledProcessCacheKey::new(process_ref.clone(), required_surface_ref.clone());
87        if let Some(entry) = self.entries.back()
88            && entry.key == key
89        {
90            self.hits += 1;
91            return Ok(entry.compiled.clone());
92        }
93        if let Some(index) = self.entries.iter().position(|entry| entry.key == key) {
94            self.hits += 1;
95            let entry = self
96                .entries
97                .remove(index)
98                .expect("cache index came from existing entry");
99            let compiled = entry.compiled.clone();
100            self.entries.push_back(entry);
101            return Ok(compiled);
102        }
103
104        self.misses += 1;
105        let compiled = Arc::new(compile_module_artifact_process(artifact, process_ref)?);
106        if self.capacity == 0 {
107            return Ok(compiled);
108        }
109        if self.entries.len() == self.capacity {
110            self.entries.pop_front();
111            self.evictions += 1;
112        }
113        self.entries.push_back(CachedCompiledProcess {
114            key,
115            compiled: compiled.clone(),
116        });
117        Ok(compiled)
118    }
119
120    pub fn clear(&mut self) {
121        self.entries.clear();
122        self.hits = 0;
123        self.misses = 0;
124        self.evictions = 0;
125    }
126
127    pub fn stats(&self) -> CompiledProgramCacheStats {
128        CompiledProgramCacheStats {
129            hits: self.hits,
130            misses: self.misses,
131            evictions: self.evictions,
132            entries: self.entries.len(),
133            capacity: self.capacity,
134        }
135    }
136}
137
138impl Default for CompiledProcessCache {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144#[derive(Debug, Error)]
145pub enum LinkedProgramCacheError {
146    #[error(transparent)]
147    Parse(#[from] crate::parser::ParseError),
148    #[error(transparent)]
149    Link(#[from] LinkError),
150}
151
152#[derive(Debug)]
153pub struct CompiledLinkedProgram {
154    linked: LinkedModule,
155    compiled: Arc<CompiledProgram>,
156}
157
158impl CompiledLinkedProgram {
159    pub fn linked_module(&self) -> &LinkedModule {
160        &self.linked
161    }
162
163    pub fn compiled_program(&self) -> &CompiledProgram {
164        self.compiled.as_ref()
165    }
166}
167
168pub struct LinkedProgramCache {
169    entries: VecDeque<CachedLinkedProgram>,
170    hits: u64,
171    misses: u64,
172    evictions: u64,
173    capacity: usize,
174}
175
176struct CachedLinkedProgram {
177    source_hash: u64,
178    source: Arc<str>,
179    program: Arc<CompiledLinkedProgram>,
180}
181
182impl LinkedProgramCache {
183    pub fn new() -> Self {
184        Self::with_capacity(DEFAULT_LINKED_PROGRAM_CACHE_CAPACITY)
185    }
186
187    pub fn with_capacity(capacity: usize) -> Self {
188        prewarm();
189        Self {
190            entries: VecDeque::with_capacity(capacity),
191            hits: 0,
192            misses: 0,
193            evictions: 0,
194            capacity,
195        }
196    }
197
198    pub fn get_or_compile(
199        &mut self,
200        source: &str,
201        surface: impl Borrow<LashlangSurface>,
202    ) -> Result<Arc<CompiledLinkedProgram>, LinkedProgramCacheError> {
203        let source_hash = program_source_hash(source);
204        let surface = surface.borrow();
205        if let Some(entry) = self.entries.back()
206            && linked_program_matches(entry, source_hash, source, surface)
207        {
208            self.hits += 1;
209            return Ok(entry.program.clone());
210        }
211
212        if let Some(index) = self
213            .entries
214            .iter()
215            .position(|entry| linked_program_matches(entry, source_hash, source, surface))
216        {
217            self.hits += 1;
218            let entry = self
219                .entries
220                .remove(index)
221                .expect("cache index came from existing entry");
222            let program = entry.program.clone();
223            self.entries.push_back(entry);
224            return Ok(program);
225        }
226
227        self.misses += 1;
228        let program = crate::parse(source)?;
229        let linked = LinkedModule::link(program, surface)?;
230        let compiled = Arc::new(compile_linked(&linked));
231        let program = Arc::new(CompiledLinkedProgram { linked, compiled });
232        if self.capacity == 0 {
233            return Ok(program);
234        }
235        if self.entries.len() == self.capacity {
236            self.entries.pop_front();
237            self.evictions += 1;
238        }
239        self.entries.push_back(CachedLinkedProgram {
240            source_hash,
241            source: Arc::<str>::from(source),
242            program: program.clone(),
243        });
244        Ok(program)
245    }
246
247    pub fn clear(&mut self) {
248        self.entries.clear();
249        self.hits = 0;
250        self.misses = 0;
251        self.evictions = 0;
252    }
253
254    pub fn stats(&self) -> CompiledProgramCacheStats {
255        CompiledProgramCacheStats {
256            hits: self.hits,
257            misses: self.misses,
258            evictions: self.evictions,
259            entries: self.entries.len(),
260            capacity: self.capacity,
261        }
262    }
263}
264
265impl Default for LinkedProgramCache {
266    fn default() -> Self {
267        Self::new()
268    }
269}
270
271pub struct CompiledProgramCache {
272    entries: VecDeque<CachedCompiledProgram>,
273    hits: u64,
274    misses: u64,
275    evictions: u64,
276    capacity: usize,
277}
278
279struct CachedCompiledProgram {
280    source_hash: u64,
281    source: Arc<str>,
282    compiled: Arc<CompiledProgram>,
283}
284
285impl CompiledProgramCache {
286    pub fn new() -> Self {
287        Self::with_capacity(DEFAULT_COMPILED_PROGRAM_CACHE_CAPACITY)
288    }
289
290    pub fn with_capacity(capacity: usize) -> Self {
291        prewarm();
292        Self {
293            entries: VecDeque::with_capacity(capacity),
294            hits: 0,
295            misses: 0,
296            evictions: 0,
297            capacity,
298        }
299    }
300
301    pub fn get_or_compile(
302        &mut self,
303        source: &str,
304    ) -> Result<Arc<CompiledProgram>, crate::parser::ParseError> {
305        let source_hash = program_source_hash(source);
306        if let Some(entry) = self.entries.back()
307            && program_source_matches(entry, source_hash, source)
308        {
309            self.hits += 1;
310            return Ok(entry.compiled.clone());
311        }
312
313        if let Some(index) = self
314            .entries
315            .iter()
316            .position(|entry| program_source_matches(entry, source_hash, source))
317        {
318            self.hits += 1;
319            let entry = self
320                .entries
321                .remove(index)
322                .expect("cache index came from existing entry");
323            let compiled = entry.compiled.clone();
324            self.entries.push_back(entry);
325            return Ok(compiled);
326        }
327
328        self.misses += 1;
329        let program = crate::parse(source)?;
330        let compiled = Arc::new(compile_program_internal(&program));
331        if self.capacity == 0 {
332            return Ok(compiled);
333        }
334        if self.entries.len() == self.capacity {
335            self.entries.pop_front();
336            self.evictions += 1;
337        }
338        self.entries.push_back(CachedCompiledProgram {
339            source_hash,
340            source: Arc::<str>::from(source),
341            compiled: compiled.clone(),
342        });
343        Ok(compiled)
344    }
345
346    pub fn clear(&mut self) {
347        self.entries.clear();
348        self.hits = 0;
349        self.misses = 0;
350        self.evictions = 0;
351    }
352
353    pub fn stats(&self) -> CompiledProgramCacheStats {
354        CompiledProgramCacheStats {
355            hits: self.hits,
356            misses: self.misses,
357            evictions: self.evictions,
358            entries: self.entries.len(),
359            capacity: self.capacity,
360        }
361    }
362}
363
364impl Default for CompiledProgramCache {
365    fn default() -> Self {
366        Self::new()
367    }
368}
369
370fn program_source_matches(entry: &CachedCompiledProgram, source_hash: u64, source: &str) -> bool {
371    source_matches(
372        entry.source_hash,
373        entry.source.as_ref(),
374        source_hash,
375        source,
376    )
377}
378
379fn linked_program_matches(
380    entry: &CachedLinkedProgram,
381    source_hash: u64,
382    source: &str,
383    surface: &LashlangSurface,
384) -> bool {
385    source_matches(
386        entry.source_hash,
387        entry.source.as_ref(),
388        source_hash,
389        source,
390    ) && surface.satisfies(&entry.program.linked.artifact.required_surface)
391}
392
393fn source_matches(cached_hash: u64, cached_source: &str, source_hash: u64, source: &str) -> bool {
394    cached_hash == source_hash && cached_source == source
395}
396
397fn program_source_hash(source: &str) -> u64 {
398    let mut hasher = FxHasher::default();
399    SOURCE_CACHE_VERSION.hash(&mut hasher);
400    source.hash(&mut hasher);
401    hasher.finish()
402}