1use std::path::{Path, PathBuf};
28use std::sync::atomic::{AtomicUsize, Ordering};
29use std::sync::{Arc, Mutex, PoisonError, RwLock};
30
31use fallow_types::discover::{DiscoveredFile, FileId};
32use fallow_types::extract::{ModuleInfo, SourceParseDegradation, SourceReadFailure};
33use fallow_types::source_fingerprint::SourceFingerprint;
34
35pub const DEFAULT_MAX_ENTRIES: usize = 4;
37
38pub const DEFAULT_MAX_RETAINED_BYTES: u64 = 512 * 1024 * 1024;
40
41const RETAINED_BYTES_PER_SOURCE_BYTE: u64 = 12;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct WarmParseLimits {
51 pub max_entries: usize,
54 pub max_retained_bytes: u64,
59}
60
61impl Default for WarmParseLimits {
62 fn default() -> Self {
63 Self {
64 max_entries: DEFAULT_MAX_ENTRIES,
65 max_retained_bytes: DEFAULT_MAX_RETAINED_BYTES,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub struct WarmParseCounts {
73 pub parse_runs: usize,
75 pub modules_parsed: usize,
77 pub disk_cache_hits: usize,
79 pub modules_reused: usize,
81}
82
83#[derive(Debug)]
85pub struct WarmParseStore {
86 limits: WarmParseLimits,
87 entries: Mutex<Vec<WarmEntry>>,
88 parse_runs: AtomicUsize,
89 modules_parsed: AtomicUsize,
90 disk_cache_hits: AtomicUsize,
91 modules_reused: AtomicUsize,
92}
93
94#[derive(Debug)]
95struct WarmEntry {
96 root: PathBuf,
97 cache_config_hash: u64,
98 paths: Vec<PathBuf>,
99 file_ids: Vec<FileId>,
100 fingerprints: Vec<SourceFingerprint>,
101 has_complexity: bool,
102 retained_bytes: u64,
103 parse: WarmParse,
104}
105
106#[derive(Debug, Clone, Copy)]
109pub(crate) struct WarmParseKey<'a> {
110 pub(crate) root: &'a Path,
111 pub(crate) cache_config_hash: u64,
112 pub(crate) files: &'a [DiscoveredFile],
113 pub(crate) fingerprints: &'a [SourceFingerprint],
114}
115
116impl WarmParseKey<'_> {
117 pub(crate) fn is_reusable(&self) -> bool {
120 self.files.len() == self.fingerprints.len()
121 && self
122 .fingerprints
123 .iter()
124 .all(|fingerprint| fingerprint.is_trustworthy_without_content())
125 }
126
127 fn retained_bytes(&self) -> u64 {
129 estimated_retained_bytes(self.fingerprints)
130 }
131}
132
133#[must_use]
138pub fn estimated_retained_bytes(fingerprints: &[SourceFingerprint]) -> u64 {
139 let source_bytes: u64 = fingerprints
140 .iter()
141 .map(|fingerprint| fingerprint.file_size)
142 .sum();
143 let module_bytes = u64::try_from(size_of::<ModuleInfo>()).unwrap_or(u64::MAX);
144 let file_count = u64::try_from(fingerprints.len()).unwrap_or(u64::MAX);
145 source_bytes
146 .saturating_mul(RETAINED_BYTES_PER_SOURCE_BYTE)
147 .saturating_add(file_count.saturating_mul(module_bytes))
148}
149
150#[derive(Debug, Clone)]
152pub(crate) struct WarmParse {
153 pub(crate) modules: Arc<[ModuleInfo]>,
154 pub(crate) read_failures: Arc<[SourceReadFailure]>,
155 pub(crate) parse_degradations: Arc<[SourceParseDegradation]>,
156}
157
158impl WarmEntry {
159 fn matches(&self, key: &WarmParseKey<'_>) -> bool {
160 self.matches_files(key) && self.fingerprints == key.fingerprints
161 }
162
163 fn matches_files(&self, key: &WarmParseKey<'_>) -> bool {
166 self.cache_config_hash == key.cache_config_hash
167 && self.root == key.root
168 && self
169 .paths
170 .iter()
171 .eq(key.files.iter().map(|file| &file.path))
172 && self
173 .file_ids
174 .iter()
175 .copied()
176 .eq(key.files.iter().map(|file| file.id))
177 }
178}
179
180impl WarmParseStore {
181 #[must_use]
183 pub fn new(limits: WarmParseLimits) -> Self {
184 Self {
185 limits,
186 entries: Mutex::new(Vec::new()),
187 parse_runs: AtomicUsize::new(0),
188 modules_parsed: AtomicUsize::new(0),
189 disk_cache_hits: AtomicUsize::new(0),
190 modules_reused: AtomicUsize::new(0),
191 }
192 }
193
194 #[must_use]
196 pub fn counts(&self) -> WarmParseCounts {
197 WarmParseCounts {
198 parse_runs: self.parse_runs.load(Ordering::Relaxed),
199 modules_parsed: self.modules_parsed.load(Ordering::Relaxed),
200 disk_cache_hits: self.disk_cache_hits.load(Ordering::Relaxed),
201 modules_reused: self.modules_reused.load(Ordering::Relaxed),
202 }
203 }
204
205 #[must_use]
207 pub fn len(&self) -> usize {
208 self.lock().len()
209 }
210
211 #[must_use]
213 pub fn is_empty(&self) -> bool {
214 self.lock().is_empty()
215 }
216
217 pub(crate) fn get(&self, key: &WarmParseKey<'_>, need_complexity: bool) -> Option<WarmParse> {
220 if !key.is_reusable() {
221 return None;
222 }
223 let mut entries = self.lock();
224 let position = entries
225 .iter()
226 .position(|entry| entry.matches(key) && (entry.has_complexity || !need_complexity))?;
227 let entry = entries.remove(position);
228 let parse = entry.parse.clone();
229 entries.push(entry);
230 drop(entries);
231 self.modules_reused
232 .fetch_add(parse.modules.len(), Ordering::Relaxed);
233 Some(parse)
234 }
235
236 pub(crate) fn put(&self, key: &WarmParseKey<'_>, has_complexity: bool, parse: WarmParse) {
240 let retained_bytes = key.retained_bytes();
241 let keep = key.is_reusable()
242 && self.limits.max_entries > 0
243 && retained_bytes <= self.limits.max_retained_bytes;
244 let entry = keep.then(|| WarmEntry {
245 root: key.root.to_path_buf(),
246 cache_config_hash: key.cache_config_hash,
247 paths: key.files.iter().map(|file| file.path.clone()).collect(),
248 file_ids: key.files.iter().map(|file| file.id).collect(),
249 fingerprints: key.fingerprints.to_vec(),
250 has_complexity,
251 retained_bytes,
252 parse,
253 });
254
255 let mut entries = self.lock();
256 entries.retain(|entry| !entry.matches_files(key));
257 entries.extend(entry);
258 let mut total_bytes: u64 = entries.iter().map(|entry| entry.retained_bytes).sum();
259 while entries.len() > self.limits.max_entries
260 || total_bytes > self.limits.max_retained_bytes
261 {
262 let removed = entries.remove(0);
263 total_bytes -= removed.retained_bytes;
264 }
265 drop(entries);
266 }
267
268 pub(crate) fn record_parse(&self, cache_misses: usize, cache_hits: usize) {
270 self.parse_runs.fetch_add(1, Ordering::Relaxed);
271 self.modules_parsed
272 .fetch_add(cache_misses, Ordering::Relaxed);
273 self.disk_cache_hits
274 .fetch_add(cache_hits, Ordering::Relaxed);
275 }
276
277 fn lock(&self) -> std::sync::MutexGuard<'_, Vec<WarmEntry>> {
278 self.entries.lock().unwrap_or_else(PoisonError::into_inner)
279 }
280}
281
282static INSTALLED: RwLock<Option<Arc<WarmParseStore>>> = RwLock::new(None);
283
284pub fn install(store: Option<Arc<WarmParseStore>>) {
291 *INSTALLED.write().unwrap_or_else(PoisonError::into_inner) = store;
292}
293
294#[must_use]
296pub fn installed() -> Option<Arc<WarmParseStore>> {
297 INSTALLED
298 .read()
299 .unwrap_or_else(PoisonError::into_inner)
300 .clone()
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 fn files(paths: &[&str]) -> Vec<DiscoveredFile> {
308 paths
309 .iter()
310 .enumerate()
311 .map(|(index, path)| DiscoveredFile {
312 id: FileId(u32::try_from(index).expect("small index")),
313 path: PathBuf::from(path),
314 size_bytes: 1,
315 })
316 .collect()
317 }
318
319 fn fingerprints(count: usize, size: u64) -> Vec<SourceFingerprint> {
320 (0..count)
321 .map(|index| SourceFingerprint::with_ctime(10 + index as u64, 20, size))
322 .collect()
323 }
324
325 fn parse() -> WarmParse {
326 WarmParse {
327 modules: Arc::from(Vec::new()),
328 read_failures: Arc::from(Vec::new()),
329 parse_degradations: Arc::from(Vec::new()),
330 }
331 }
332
333 fn key<'a>(
334 root: &'a Path,
335 files: &'a [DiscoveredFile],
336 fingerprints: &'a [SourceFingerprint],
337 ) -> WarmParseKey<'a> {
338 WarmParseKey {
339 root,
340 cache_config_hash: 7,
341 files,
342 fingerprints,
343 }
344 }
345
346 #[test]
347 fn a_hit_needs_the_same_files_and_fingerprints() {
348 let store = WarmParseStore::new(WarmParseLimits::default());
349 let root = Path::new("/project");
350 let listed = files(&["/project/a.ts", "/project/b.ts"]);
351 let marks = fingerprints(2, 5);
352 store.put(&key(root, &listed, &marks), true, parse());
353
354 assert!(store.get(&key(root, &listed, &marks), true).is_some());
355
356 let mut edited = marks.clone();
357 edited[1].mtime_ns += 1;
358 assert!(store.get(&key(root, &listed, &edited), false).is_none());
359
360 let added = files(&["/project/a.ts", "/project/b.ts", "/project/c.ts"]);
361 assert!(
362 store
363 .get(&key(root, &added, &fingerprints(3, 5)), false)
364 .is_none()
365 );
366
367 let mut other_config = key(root, &listed, &marks);
368 other_config.cache_config_hash = 8;
369 assert!(store.get(&other_config, false).is_none());
370 }
371
372 #[test]
373 fn a_parse_without_complexity_does_not_serve_a_request_for_it() {
374 let store = WarmParseStore::new(WarmParseLimits::default());
375 let root = Path::new("/project");
376 let listed = files(&["/project/a.ts"]);
377 let marks = fingerprints(1, 5);
378 store.put(&key(root, &listed, &marks), false, parse());
379
380 assert!(store.get(&key(root, &listed, &marks), true).is_none());
381 assert!(store.get(&key(root, &listed, &marks), false).is_some());
382 }
383
384 #[test]
385 fn fingerprints_without_ctime_are_not_kept() {
386 let store = WarmParseStore::new(WarmParseLimits::default());
387 let root = Path::new("/project");
388 let listed = files(&["/project/a.ts"]);
389 let marks = [SourceFingerprint::new(10, 5)];
390 store.put(&key(root, &listed, &marks), true, parse());
391
392 assert!(store.is_empty());
393 assert!(store.get(&key(root, &listed, &marks), false).is_none());
394 }
395
396 #[test]
397 fn a_new_parse_of_the_same_files_replaces_the_old_one() {
398 let store = WarmParseStore::new(WarmParseLimits::default());
399 let root = Path::new("/project");
400 let listed = files(&["/project/a.ts"]);
401 let before = fingerprints(1, 5);
402 let after = fingerprints(1, 6);
403 store.put(&key(root, &listed, &before), true, parse());
404 store.put(&key(root, &listed, &after), true, parse());
405
406 assert_eq!(store.len(), 1);
407 assert!(store.get(&key(root, &listed, &after), true).is_some());
408 }
409
410 #[test]
411 fn the_least_recently_used_entry_leaves_first() {
412 let store = WarmParseStore::new(WarmParseLimits {
413 max_entries: 2,
414 max_retained_bytes: u64::MAX,
415 });
416 let marks = fingerprints(1, 5);
417 let first = files(&["/first/a.ts"]);
418 let second = files(&["/second/a.ts"]);
419 let third = files(&["/third/a.ts"]);
420 store.put(&key(Path::new("/first"), &first, &marks), true, parse());
421 store.put(&key(Path::new("/second"), &second, &marks), true, parse());
422 assert!(
423 store
424 .get(&key(Path::new("/first"), &first, &marks), true)
425 .is_some()
426 );
427 store.put(&key(Path::new("/third"), &third, &marks), true, parse());
428
429 assert_eq!(store.len(), 2);
430 assert!(
431 store
432 .get(&key(Path::new("/second"), &second, &marks), true)
433 .is_none()
434 );
435 assert!(
436 store
437 .get(&key(Path::new("/first"), &first, &marks), true)
438 .is_some()
439 );
440 }
441
442 #[test]
443 fn the_default_limit_counts_the_memory_of_the_kept_modules() {
444 let store = WarmParseStore::new(WarmParseLimits::default());
445 let large = files(&["/large/a.ts"]);
446 store.put(
447 &key(
448 Path::new("/large"),
449 &large,
450 &fingerprints(1, 64 * 1024 * 1024),
451 ),
452 true,
453 parse(),
454 );
455 assert!(
456 store.is_empty(),
457 "the modules of 64 MiB of source take more memory than the default limit"
458 );
459
460 let medium = files(&["/medium/a.ts"]);
461 store.put(
462 &key(
463 Path::new("/medium"),
464 &medium,
465 &fingerprints(1, 16 * 1024 * 1024),
466 ),
467 true,
468 parse(),
469 );
470 assert_eq!(store.len(), 1);
471 }
472
473 #[test]
474 fn a_list_with_other_file_ids_is_not_served() {
475 let store = WarmParseStore::new(WarmParseLimits::default());
476 let root = Path::new("/project");
477 let listed = files(&["/project/a.ts", "/project/b.ts"]);
478 let marks = fingerprints(2, 5);
479 store.put(&key(root, &listed, &marks), true, parse());
480
481 let mut renumbered = listed.clone();
482 renumbered[0].id = FileId(7);
483 assert!(store.get(&key(root, &renumbered, &marks), false).is_none());
484 assert!(store.get(&key(root, &listed, &marks), false).is_some());
485 }
486
487 #[test]
488 fn the_memory_limit_bounds_the_store() {
489 let small = files(&["/small/a.ts"]);
490 let small_marks = fingerprints(1, 6);
491 let small_key = key(Path::new("/small"), &small, &small_marks);
492 let store = WarmParseStore::new(WarmParseLimits {
493 max_entries: 8,
494 max_retained_bytes: small_key.retained_bytes(),
495 });
496 let large = files(&["/large/a.ts", "/large/b.ts"]);
497 store.put(&small_key, true, parse());
498 store.put(
499 &key(Path::new("/large"), &large, &fingerprints(2, 6)),
500 true,
501 parse(),
502 );
503 assert_eq!(store.len(), 1, "a list over the limit is not kept");
504
505 let other = files(&["/other/a.ts"]);
506 store.put(
507 &key(Path::new("/other"), &other, &fingerprints(1, 6)),
508 true,
509 parse(),
510 );
511 assert_eq!(
512 store.len(),
513 1,
514 "the older list leaves to keep the sum within the limit"
515 );
516 assert!(
517 store
518 .get(&key(Path::new("/other"), &other, &fingerprints(1, 6)), true)
519 .is_some()
520 );
521 }
522}