1use std::path::{Path, PathBuf};
32use std::sync::atomic::{AtomicUsize, Ordering};
33use std::sync::{Arc, Mutex, PoisonError, RwLock};
34
35use fallow_types::discover::{DiscoveredFile, FileId};
36use fallow_types::extract::{ModuleInfo, SourceParseDegradation, SourceReadFailure};
37use fallow_types::source_fingerprint::SourceFingerprint;
38
39pub const DEFAULT_MAX_ENTRIES: usize = 4;
41
42pub const DEFAULT_MAX_RETAINED_BYTES: u64 = 512 * 1024 * 1024;
44
45const RETAINED_BYTES_PER_SOURCE_BYTE: u64 = 12;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct WarmParseLimits {
55 pub max_entries: usize,
58 pub max_retained_bytes: u64,
63}
64
65impl Default for WarmParseLimits {
66 fn default() -> Self {
67 Self {
68 max_entries: DEFAULT_MAX_ENTRIES,
69 max_retained_bytes: DEFAULT_MAX_RETAINED_BYTES,
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
76pub struct WarmParseCounts {
77 pub parse_runs: usize,
79 pub modules_parsed: usize,
81 pub disk_cache_hits: usize,
83 pub modules_reused: usize,
85}
86
87#[derive(Debug)]
89pub struct WarmParseStore {
90 limits: WarmParseLimits,
91 entries: Mutex<Vec<WarmEntry>>,
92 parse_runs: AtomicUsize,
93 modules_parsed: AtomicUsize,
94 disk_cache_hits: AtomicUsize,
95 modules_reused: AtomicUsize,
96}
97
98#[derive(Debug)]
99struct WarmEntry {
100 root: PathBuf,
101 cache_config_hash: u64,
102 paths: Vec<PathBuf>,
103 file_ids: Vec<FileId>,
104 fingerprints: Vec<SourceFingerprint>,
105 read_started_ns: u64,
107 has_complexity: bool,
108 retained_bytes: u64,
109 parse: WarmParse,
110}
111
112#[derive(Debug, Clone, Copy)]
115pub(crate) struct WarmParseKey<'a> {
116 pub(crate) root: &'a Path,
117 pub(crate) cache_config_hash: u64,
118 pub(crate) files: &'a [DiscoveredFile],
119 pub(crate) fingerprints: &'a [SourceFingerprint],
120 pub(crate) read_started_ns: u64,
122}
123
124impl WarmParseKey<'_> {
125 pub(crate) fn is_reusable(&self) -> bool {
129 self.files.len() == self.fingerprints.len()
130 && self
131 .fingerprints
132 .iter()
133 .all(|fingerprint| fingerprint.is_trustworthy_without_content())
134 }
135
136 fn retained_bytes(&self) -> u64 {
138 estimated_retained_bytes(self.fingerprints)
139 }
140}
141
142#[must_use]
147pub fn estimated_retained_bytes(fingerprints: &[SourceFingerprint]) -> u64 {
148 let source_bytes: u64 = fingerprints
149 .iter()
150 .map(|fingerprint| fingerprint.file_size)
151 .sum();
152 let module_bytes = u64::try_from(size_of::<ModuleInfo>()).unwrap_or(u64::MAX);
153 let file_count = u64::try_from(fingerprints.len()).unwrap_or(u64::MAX);
154 source_bytes
155 .saturating_mul(RETAINED_BYTES_PER_SOURCE_BYTE)
156 .saturating_add(file_count.saturating_mul(module_bytes))
157}
158
159fn unsettled_files_match(
165 key: &WarmParseKey<'_>,
166 kept_read_started_ns: u64,
167 modules: &[ModuleInfo],
168) -> bool {
169 key.fingerprints
170 .iter()
171 .zip(key.files)
172 .all(|(fingerprint, file)| {
173 fingerprint.is_settled_before(kept_read_started_ns)
174 || crate::session_reuse::module_matches_source(modules, file.id, &file.path)
175 })
176}
177
178#[derive(Debug, Clone)]
180pub(crate) struct WarmParse {
181 pub(crate) modules: Arc<[ModuleInfo]>,
182 pub(crate) read_failures: Arc<[SourceReadFailure]>,
183 pub(crate) parse_degradations: Arc<[SourceParseDegradation]>,
184}
185
186impl WarmEntry {
187 fn matches(&self, key: &WarmParseKey<'_>) -> bool {
191 self.matches_files(key) && self.fingerprints == key.fingerprints
192 }
193
194 fn matches_files(&self, key: &WarmParseKey<'_>) -> bool {
197 self.cache_config_hash == key.cache_config_hash
198 && self.root == key.root
199 && self
200 .paths
201 .iter()
202 .eq(key.files.iter().map(|file| &file.path))
203 && self
204 .file_ids
205 .iter()
206 .copied()
207 .eq(key.files.iter().map(|file| file.id))
208 }
209}
210
211impl WarmParseStore {
212 #[must_use]
214 pub fn new(limits: WarmParseLimits) -> Self {
215 Self {
216 limits,
217 entries: Mutex::new(Vec::new()),
218 parse_runs: AtomicUsize::new(0),
219 modules_parsed: AtomicUsize::new(0),
220 disk_cache_hits: AtomicUsize::new(0),
221 modules_reused: AtomicUsize::new(0),
222 }
223 }
224
225 #[must_use]
227 pub fn counts(&self) -> WarmParseCounts {
228 WarmParseCounts {
229 parse_runs: self.parse_runs.load(Ordering::Relaxed),
230 modules_parsed: self.modules_parsed.load(Ordering::Relaxed),
231 disk_cache_hits: self.disk_cache_hits.load(Ordering::Relaxed),
232 modules_reused: self.modules_reused.load(Ordering::Relaxed),
233 }
234 }
235
236 #[must_use]
238 pub fn len(&self) -> usize {
239 self.lock().len()
240 }
241
242 #[must_use]
244 pub fn is_empty(&self) -> bool {
245 self.lock().is_empty()
246 }
247
248 pub(crate) fn get(&self, key: &WarmParseKey<'_>, need_complexity: bool) -> Option<WarmParse> {
251 if !key.is_reusable() {
252 return None;
253 }
254 let entries = self.lock();
255 let entry = entries
256 .iter()
257 .find(|entry| entry.matches(key) && (entry.has_complexity || !need_complexity))?;
258 let kept_read_started_ns = entry.read_started_ns;
259 let parse = entry.parse.clone();
260 drop(entries);
261 if !unsettled_files_match(key, kept_read_started_ns, &parse.modules) {
264 return None;
265 }
266 let mut entries = self.lock();
267 if let Some(position) = entries
268 .iter()
269 .position(|entry| Arc::ptr_eq(&entry.parse.modules, &parse.modules))
270 {
271 let mut entry = entries.remove(position);
272 entry.read_started_ns = entry.read_started_ns.max(key.read_started_ns);
276 entries.push(entry);
277 }
278 drop(entries);
279 self.modules_reused
280 .fetch_add(parse.modules.len(), Ordering::Relaxed);
281 Some(parse)
282 }
283
284 pub(crate) fn put(&self, key: &WarmParseKey<'_>, has_complexity: bool, parse: WarmParse) {
288 let retained_bytes = key.retained_bytes();
289 let keep = key.is_reusable()
290 && self.limits.max_entries > 0
291 && retained_bytes <= self.limits.max_retained_bytes;
292 let entry = keep.then(|| WarmEntry {
293 root: key.root.to_path_buf(),
294 cache_config_hash: key.cache_config_hash,
295 paths: key.files.iter().map(|file| file.path.clone()).collect(),
296 file_ids: key.files.iter().map(|file| file.id).collect(),
297 fingerprints: key.fingerprints.to_vec(),
298 read_started_ns: key.read_started_ns,
299 has_complexity,
300 retained_bytes,
301 parse,
302 });
303
304 let mut entries = self.lock();
305 entries.retain(|entry| !entry.matches_files(key));
306 entries.extend(entry);
307 let mut total_bytes: u64 = entries.iter().map(|entry| entry.retained_bytes).sum();
308 while entries.len() > self.limits.max_entries
309 || total_bytes > self.limits.max_retained_bytes
310 {
311 let removed = entries.remove(0);
312 total_bytes -= removed.retained_bytes;
313 }
314 drop(entries);
315 }
316
317 pub(crate) fn record_parse(&self, cache_misses: usize, cache_hits: usize) {
319 self.parse_runs.fetch_add(1, Ordering::Relaxed);
320 self.modules_parsed
321 .fetch_add(cache_misses, Ordering::Relaxed);
322 self.disk_cache_hits
323 .fetch_add(cache_hits, Ordering::Relaxed);
324 }
325
326 fn lock(&self) -> std::sync::MutexGuard<'_, Vec<WarmEntry>> {
327 self.entries.lock().unwrap_or_else(PoisonError::into_inner)
328 }
329}
330
331static INSTALLED: RwLock<Option<Arc<WarmParseStore>>> = RwLock::new(None);
332
333pub fn install(store: Option<Arc<WarmParseStore>>) {
340 *INSTALLED.write().unwrap_or_else(PoisonError::into_inner) = store;
341}
342
343#[must_use]
345pub fn installed() -> Option<Arc<WarmParseStore>> {
346 INSTALLED
347 .read()
348 .unwrap_or_else(PoisonError::into_inner)
349 .clone()
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use fallow_types::source_fingerprint::TIMESTAMP_SETTLE_WINDOW_NS;
356
357 fn files(paths: &[&str]) -> Vec<DiscoveredFile> {
358 paths
359 .iter()
360 .enumerate()
361 .map(|(index, path)| DiscoveredFile {
362 id: FileId(u32::try_from(index).expect("small index")),
363 path: PathBuf::from(path),
364 size_bytes: 1,
365 })
366 .collect()
367 }
368
369 fn fingerprints(count: usize, size: u64) -> Vec<SourceFingerprint> {
370 (0..count)
371 .map(|index| SourceFingerprint::with_ctime(10 + index as u64, 20, size))
372 .collect()
373 }
374
375 fn parse() -> WarmParse {
376 WarmParse {
377 modules: Arc::from(Vec::new()),
378 read_failures: Arc::from(Vec::new()),
379 parse_degradations: Arc::from(Vec::new()),
380 }
381 }
382
383 fn key<'a>(
384 root: &'a Path,
385 files: &'a [DiscoveredFile],
386 fingerprints: &'a [SourceFingerprint],
387 ) -> WarmParseKey<'a> {
388 WarmParseKey {
389 root,
390 cache_config_hash: 7,
391 files,
392 fingerprints,
393 read_started_ns: u64::MAX,
394 }
395 }
396
397 #[test]
398 fn a_hit_needs_the_same_files_and_fingerprints() {
399 let store = WarmParseStore::new(WarmParseLimits::default());
400 let root = Path::new("/project");
401 let listed = files(&["/project/a.ts", "/project/b.ts"]);
402 let marks = fingerprints(2, 5);
403 store.put(&key(root, &listed, &marks), true, parse());
404
405 assert!(store.get(&key(root, &listed, &marks), true).is_some());
406
407 let mut edited = marks.clone();
408 edited[1].mtime_ns += 1;
409 assert!(store.get(&key(root, &listed, &edited), false).is_none());
410
411 let added = files(&["/project/a.ts", "/project/b.ts", "/project/c.ts"]);
412 assert!(
413 store
414 .get(&key(root, &added, &fingerprints(3, 5)), false)
415 .is_none()
416 );
417
418 let mut other_config = key(root, &listed, &marks);
419 other_config.cache_config_hash = 8;
420 assert!(store.get(&other_config, false).is_none());
421 }
422
423 #[test]
424 fn a_parse_without_complexity_does_not_serve_a_request_for_it() {
425 let store = WarmParseStore::new(WarmParseLimits::default());
426 let root = Path::new("/project");
427 let listed = files(&["/project/a.ts"]);
428 let marks = fingerprints(1, 5);
429 store.put(&key(root, &listed, &marks), false, parse());
430
431 assert!(store.get(&key(root, &listed, &marks), true).is_none());
432 assert!(store.get(&key(root, &listed, &marks), false).is_some());
433 }
434
435 #[test]
436 fn fingerprints_without_ctime_are_not_kept() {
437 let store = WarmParseStore::new(WarmParseLimits::default());
438 let root = Path::new("/project");
439 let listed = files(&["/project/a.ts"]);
440 let marks = [SourceFingerprint::new(10, 5)];
441 store.put(&key(root, &listed, &marks), true, parse());
442
443 assert!(store.is_empty());
444 assert!(store.get(&key(root, &listed, &marks), false).is_none());
445 }
446
447 #[test]
448 fn a_fingerprint_inside_the_settle_window_needs_the_kept_content() {
449 let project = tempfile::tempdir().expect("project");
450 let source = project.path().join("a.ts");
451 std::fs::write(&source, "export const a = 1;\n").expect("write source");
452 let store = WarmParseStore::new(WarmParseLimits::default());
453 let listed = files(&[source.to_str().expect("utf-8 path")]);
454 let marks = [SourceFingerprint::with_ctime(10, 20, 5)];
455 let recent = WarmParseKey {
456 read_started_ns: 20,
457 ..key(project.path(), &listed, &marks)
458 };
459 store.put(&recent, true, parse());
460
461 assert_eq!(store.len(), 1);
462 assert!(
463 store.get(&recent, false).is_none(),
464 "a readable file without a matching kept module is not reused"
465 );
466 let settled = WarmParseKey {
467 read_started_ns: u64::MAX,
468 ..recent
469 };
470 store.put(&settled, true, parse());
471 assert!(
472 store.get(&settled, false).is_some(),
473 "a settled fingerprint needs no content check"
474 );
475 }
476
477 fn parsed_file(dir: &Path, source: &str) -> (Vec<DiscoveredFile>, WarmParse) {
480 let path = dir.join("a.ts");
481 std::fs::write(&path, source).expect("write source");
482 let listed = files(&[path.to_str().expect("utf-8 path")]);
483 let module = fallow_extract::parse_single_file(&listed[0]).expect("parse source");
484 let parse = WarmParse {
485 modules: Arc::from(vec![module]),
486 ..parse()
487 };
488 (listed, parse)
489 }
490
491 #[test]
492 fn an_unsettled_fingerprint_with_the_kept_content_is_reused() {
493 let project = tempfile::tempdir().expect("project");
494 let (listed, kept) = parsed_file(project.path(), "export const a = 1;\n");
495 let marks = [SourceFingerprint::with_ctime(10, 20, 5)];
496 let recent = WarmParseKey {
497 read_started_ns: 20,
498 ..key(project.path(), &listed, &marks)
499 };
500 let store = WarmParseStore::new(WarmParseLimits::default());
501 store.put(&recent, true, kept);
502
503 assert!(store.get(&recent, false).is_some());
504 }
505
506 #[test]
510 fn a_verified_hit_settles_the_entry() {
511 let project = tempfile::tempdir().expect("project");
512 let (listed, kept) = parsed_file(project.path(), "export const a = 1;\n");
513 let marks = [SourceFingerprint::with_ctime(10, 20, 5)];
514 let recent = WarmParseKey {
515 read_started_ns: 20,
516 ..key(project.path(), &listed, &marks)
517 };
518 let store = WarmParseStore::new(WarmParseLimits::default());
519 store.put(&recent, true, kept);
520
521 let later = WarmParseKey {
522 read_started_ns: 20 + TIMESTAMP_SETTLE_WINDOW_NS,
523 ..recent
524 };
525 assert!(
526 store.get(&later, false).is_some(),
527 "the kept content matches"
528 );
529 assert_eq!(
530 store.lock()[0].read_started_ns,
531 later.read_started_ns,
532 "the entry takes the read start of the verified hit"
533 );
534
535 std::fs::write(&listed[0].path, "export const b = 1;\n").expect("rewrite source");
539 assert!(
540 store.get(&later, false).is_some(),
541 "a settled entry needs no content read"
542 );
543 }
544
545 #[test]
546 fn a_new_parse_of_the_same_files_replaces_the_old_one() {
547 let store = WarmParseStore::new(WarmParseLimits::default());
548 let root = Path::new("/project");
549 let listed = files(&["/project/a.ts"]);
550 let before = fingerprints(1, 5);
551 let after = fingerprints(1, 6);
552 store.put(&key(root, &listed, &before), true, parse());
553 store.put(&key(root, &listed, &after), true, parse());
554
555 assert_eq!(store.len(), 1);
556 assert!(store.get(&key(root, &listed, &after), true).is_some());
557 }
558
559 #[test]
560 fn the_least_recently_used_entry_leaves_first() {
561 let store = WarmParseStore::new(WarmParseLimits {
562 max_entries: 2,
563 max_retained_bytes: u64::MAX,
564 });
565 let marks = fingerprints(1, 5);
566 let first = files(&["/first/a.ts"]);
567 let second = files(&["/second/a.ts"]);
568 let third = files(&["/third/a.ts"]);
569 store.put(&key(Path::new("/first"), &first, &marks), true, parse());
570 store.put(&key(Path::new("/second"), &second, &marks), true, parse());
571 assert!(
572 store
573 .get(&key(Path::new("/first"), &first, &marks), true)
574 .is_some()
575 );
576 store.put(&key(Path::new("/third"), &third, &marks), true, parse());
577
578 assert_eq!(store.len(), 2);
579 assert!(
580 store
581 .get(&key(Path::new("/second"), &second, &marks), true)
582 .is_none()
583 );
584 assert!(
585 store
586 .get(&key(Path::new("/first"), &first, &marks), true)
587 .is_some()
588 );
589 }
590
591 #[test]
592 fn the_default_limit_counts_the_memory_of_the_kept_modules() {
593 let store = WarmParseStore::new(WarmParseLimits::default());
594 let large = files(&["/large/a.ts"]);
595 store.put(
596 &key(
597 Path::new("/large"),
598 &large,
599 &fingerprints(1, 64 * 1024 * 1024),
600 ),
601 true,
602 parse(),
603 );
604 assert!(
605 store.is_empty(),
606 "the modules of 64 MiB of source take more memory than the default limit"
607 );
608
609 let medium = files(&["/medium/a.ts"]);
610 store.put(
611 &key(
612 Path::new("/medium"),
613 &medium,
614 &fingerprints(1, 16 * 1024 * 1024),
615 ),
616 true,
617 parse(),
618 );
619 assert_eq!(store.len(), 1);
620 }
621
622 #[test]
623 fn a_list_with_other_file_ids_is_not_served() {
624 let store = WarmParseStore::new(WarmParseLimits::default());
625 let root = Path::new("/project");
626 let listed = files(&["/project/a.ts", "/project/b.ts"]);
627 let marks = fingerprints(2, 5);
628 store.put(&key(root, &listed, &marks), true, parse());
629
630 let mut renumbered = listed.clone();
631 renumbered[0].id = FileId(7);
632 assert!(store.get(&key(root, &renumbered, &marks), false).is_none());
633 assert!(store.get(&key(root, &listed, &marks), false).is_some());
634 }
635
636 #[test]
637 fn the_memory_limit_bounds_the_store() {
638 let small = files(&["/small/a.ts"]);
639 let small_marks = fingerprints(1, 6);
640 let small_key = key(Path::new("/small"), &small, &small_marks);
641 let store = WarmParseStore::new(WarmParseLimits {
642 max_entries: 8,
643 max_retained_bytes: small_key.retained_bytes(),
644 });
645 let large = files(&["/large/a.ts", "/large/b.ts"]);
646 store.put(&small_key, true, parse());
647 store.put(
648 &key(Path::new("/large"), &large, &fingerprints(2, 6)),
649 true,
650 parse(),
651 );
652 assert_eq!(store.len(), 1, "a list over the limit is not kept");
653
654 let other = files(&["/other/a.ts"]);
655 store.put(
656 &key(Path::new("/other"), &other, &fingerprints(1, 6)),
657 true,
658 parse(),
659 );
660 assert_eq!(
661 store.len(),
662 1,
663 "the older list leaves to keep the sum within the limit"
664 );
665 assert!(
666 store
667 .get(&key(Path::new("/other"), &other, &fingerprints(1, 6)), true)
668 .is_some()
669 );
670 }
671}