Skip to main content

prov_graph/
memo.rs

1//! A read memo with the lifetime of one operation — the cheap half of not
2//! reading the same file twice.
3//!
4//! prov's passes are composed rather than fused: [`check`] runs a walk, then an
5//! orphan sweep, then a fixity pass, then five more, and each of them starts
6//! from a path and calls [`load`] on it. That composition is what makes the
7//! passes independently testable and independently correct, and it is worth
8//! keeping — but taken literally it means `check` reads and parses the same
9//! document once per pass that cares about it. The walk loads every reachable
10//! document to build the census; `fixity_findings` then loads every reachable
11//! document again to hash its body. Two full reads and two full parses, for a
12//! question the first read already answered.
13//!
14//! So remember the answer for as long as the operation lasts, and no longer.
15//!
16//! ## Directories, for the same reason and at a worse ratio
17//!
18//! The memo holds *listings* beside documents, because the same argument runs
19//! harder there. Resolution asks whether a link's target exists and — when it
20//! does not — whether some entry beside it differs only in case, which
21//! [`exact_name`] answers by reading the target's parent directory. That is one
22//! directory read **per link**, where the document memo saves one read per
23//! *pass*: a flat workspace of N documents holding N links into one directory
24//! read that directory N times, each read enumerating N entries, and `check`
25//! went quadratic on exactly that. Listings are also indexed rather than
26//! stored raw (`DirNames`), so the answer is a hash lookup and not a scan of
27//! everything the directory holds.
28//!
29//! [`exact_name`]: crate::graph::Graph
30//!
31//! ## Why the scope, and why it is explicit
32//!
33//! A memo with no end is a cache, and a cache has to be invalidated. A memo
34//! bounded by an *operation* barely has to be: nothing outside prov can write
35//! to the workspace between two of `check`'s sub-passes in any sense prov could
36//! have detected anyway, and everything inside prov that writes goes through
37//! `prov`'s `ChangeSet`, which forgets what it touched
38//! ([`Workspace::commit`]). There is no staleness window left that a stat could
39//! have closed and this cannot.
40//!
41//! The scope is explicit — a caller opens one with
42//! [`Graph::read_scope`](crate::graph::Graph::read_scope) and holds the guard — because a memo that switched
43//! itself on would be a cache again, and because the caller is the only one who
44//! knows where its operation begins. Scopes nest: an operation that opens one
45//! and then calls another that opens its own gets a single memo lasting the
46//! outer one, which is exactly the composition case (`prov`'s `check` opens a
47//! scope and then calls `walk`, which is welcome to open its own).
48//!
49//! ## What is not memoized
50//!
51//! Failures. A read that errored is not remembered, so a missing file is
52//! re-checked rather than pinned to its first answer — the cost is one syscall
53//! on a path prov already knows is trouble, and the alternative is a memo that
54//! can report a file absent after it appeared.
55//!
56//! [`check`]: https://docs.rs/prov
57//! [`load`]: crate::graph::Graph::load
58//! [`Workspace::commit`]: https://docs.rs/prov
59
60use std::collections::{HashMap, HashSet};
61use std::ffi::{OsStr, OsString};
62use std::path::{Path, PathBuf};
63use std::sync::{Arc, Mutex, MutexGuard};
64
65use crate::document::Document;
66
67/// Take a lock, recovering from a panic that poisoned it.
68///
69/// A memo and a cache are optimizations. Turning a panic that happened
70/// elsewhere into a second panic here would be a bug of prov's own making, and
71/// the worst that can actually be wrong behind a poisoned lock is a stale entry
72/// — which every caller already tolerates by construction.
73///
74/// `Mutex` rather than `RefCell` for the whole family: `&Workspace` must stay
75/// `Send` (`prov/tests/public_api.rs` pins that, so an embedder can drive
76/// `apply` and `discover` from a multi-threaded runtime), which needs the
77/// workspace itself to be `Sync`, which a `RefCell` is not. No guard is ever
78/// held across an `.await`.
79pub fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
80    mutex
81        .lock()
82        .unwrap_or_else(|poisoned| poisoned.into_inner())
83}
84
85/// How much document text one memo may hold. A workspace of a few thousand
86/// markdown documents lands well under this; the cap is here so that a
87/// workspace of unusually large documents degrades to "re-reads some of them"
88/// rather than to holding the whole tree in memory at once.
89const BUDGET: usize = 32 * 1024 * 1024;
90
91/// The largest single document worth remembering. A file this big is rare
92/// enough that keeping it would evict nothing useful and buy nothing.
93const MAX_DOCUMENT: usize = 1024 * 1024;
94
95/// One directory's entry names, indexed for the two questions a name lookup
96/// asks: is this name here, and is a name that differs from it only in ASCII
97/// case here?
98///
99/// Two maps rather than one, because a directory on a case-sensitive
100/// filesystem may hold both `Notes.md` and `notes.md`, and an exact match must
101/// win however the listing happened to be ordered — a single folded map would
102/// answer "the other one" whenever the wrong one was inserted last.
103///
104/// Keyed by [`OsString`], not `String`: the scan this replaced compared
105/// [`OsStr`]s, so a name that is not UTF-8 still matched itself exactly, and
106/// dropping such an entry here would turn a document prov can open into one it
107/// reports as missing.
108///
109/// [`OsStr`]: std::ffi::OsStr
110#[derive(Debug, Default)]
111pub(crate) struct DirNames {
112    /// Every entry name, as listed.
113    exact: HashSet<OsString>,
114    /// ASCII-lowercased name → the name as listed. Last writer wins, which only
115    /// matters when a directory holds two names differing in case, and then only
116    /// for the *inexact* answer — [`exact`](Self::exact) has already settled the
117    /// other one.
118    folded: HashMap<OsString, OsString>,
119}
120
121impl DirNames {
122    /// Index a directory read.
123    pub(crate) fn index(entries: &[crate::fs::DirEntry]) -> Self {
124        let mut names = Self::default();
125        for entry in entries {
126            let Some(name) = entry.file_name() else {
127                continue;
128            };
129            names
130                .folded
131                .insert(name.to_ascii_lowercase(), name.to_os_string());
132            names.exact.insert(name.to_os_string());
133        }
134        names
135    }
136
137    /// Whether the directory holds exactly this name.
138    pub(crate) fn holds(&self, name: &OsStr) -> bool {
139        self.exact.contains(name)
140    }
141
142    /// The name the directory holds that differs from `name` in ASCII case
143    /// alone, if any. Ask [`holds`](Self::holds) first: this will happily
144    /// return `name` itself.
145    pub(crate) fn case_variant(&self, name: &OsStr) -> Option<&OsStr> {
146        self.folded
147            .get(&name.to_ascii_lowercase())
148            .map(OsString::as_os_str)
149    }
150
151    /// Roughly the bytes this holds, for the memo's budget. Approximate on
152    /// purpose — the budget is a ceiling that keeps an unusual workspace from
153    /// eating memory, not an allocator.
154    fn weight(&self) -> usize {
155        let names = self.exact.iter().map(|n| n.len()).sum::<usize>();
156        let folded = self
157            .folded
158            .iter()
159            .map(|(k, v)| k.len() + v.len())
160            .sum::<usize>();
161        names + folded
162    }
163}
164
165/// What one operation has already read.
166#[derive(Debug, Default)]
167pub struct ReadMemo {
168    /// Scope nesting depth. Nothing is remembered outside a scope, and the
169    /// outermost exit is what clears it.
170    depth: usize,
171    docs: HashMap<PathBuf, (String, Document)>,
172    /// Listings held, keyed workspace-relative like [`docs`](Self::docs) — which
173    /// is what lets [`forget`](Self::forget) drop the one a write invalidates.
174    dirs: HashMap<PathBuf, Arc<DirNames>>,
175    /// Text and listing names held, against [`BUDGET`].
176    bytes: usize,
177}
178
179impl ReadMemo {
180    pub(crate) fn enter(&mut self) {
181        self.depth += 1;
182    }
183
184    /// Leave one scope. Only the outermost exit drops what was remembered — an
185    /// inner scope is covered by the one that encloses it.
186    pub(crate) fn leave(&mut self) {
187        self.depth = self.depth.saturating_sub(1);
188        if self.depth == 0 {
189            self.clear();
190        }
191    }
192
193    /// What was read for `path` this operation, if anything. `None` outside a
194    /// scope, always: a memo nobody opened holds nothing.
195    pub(crate) fn get(&self, path: &Path) -> Option<(String, Document)> {
196        if self.depth == 0 {
197            return None;
198        }
199        self.docs.get(path).cloned()
200    }
201
202    /// Remember what `path` read as. A no-op outside a scope, over the
203    /// per-document ceiling, or once the budget is spent.
204    pub(crate) fn remember(&mut self, path: &Path, text: &str, doc: &Document) {
205        if self.depth == 0 || text.len() > MAX_DOCUMENT || self.bytes + text.len() > BUDGET {
206            return;
207        }
208        self.bytes += text.len();
209        self.docs
210            .insert(path.to_path_buf(), (text.to_string(), doc.clone()));
211    }
212
213    /// The indexed listing of the workspace-relative directory `path`, if it
214    /// was read this operation. `None` outside a scope, like
215    /// [`get`](Self::get).
216    pub(crate) fn dir(&self, path: &Path) -> Option<Arc<DirNames>> {
217        if self.depth == 0 {
218            return None;
219        }
220        self.dirs.get(path).cloned()
221    }
222
223    /// Remember a directory read. A no-op outside a scope or once the budget is
224    /// spent — a listing that is not remembered costs a re-read and nothing
225    /// else.
226    pub(crate) fn remember_dir(&mut self, path: &Path, names: Arc<DirNames>) {
227        let weight = names.weight();
228        if self.depth == 0 || self.bytes + weight > BUDGET {
229            return;
230        }
231        self.bytes += weight;
232        self.dirs.insert(path.to_path_buf(), names);
233    }
234
235    /// Forget `path` — what a write to it means.
236    ///
237    /// Its **parent's listing** goes too. A write may create the name or a
238    /// remove may take it away, and the listing that enumerated the parent is
239    /// the only one that can be wrong about it — so the operation that stages a
240    /// change and then reads the workspace back does not resolve against a
241    /// directory as it stood beforehand.
242    pub fn forget(&mut self, path: &Path) {
243        if let Some((text, _)) = self.docs.remove(path) {
244            self.bytes -= text.len();
245        }
246        if let Some(parent) = path.parent()
247            && let Some(names) = self.dirs.remove(parent)
248        {
249            self.bytes -= names.weight();
250        }
251    }
252
253    pub(crate) fn clear(&mut self) {
254        self.docs.clear();
255        self.dirs.clear();
256        self.bytes = 0;
257    }
258
259    /// How many documents are remembered — the observable the tests assert on.
260    #[cfg(test)]
261    pub(crate) fn len(&self) -> usize {
262        self.docs.len()
263    }
264
265    /// How many directory listings are remembered, likewise.
266    #[cfg(test)]
267    pub(crate) fn dirs_len(&self) -> usize {
268        self.dirs.len()
269    }
270}
271
272/// An open read scope. Hold it for the operation; dropping it leaves the scope,
273/// and dropping the outermost one drops everything the operation remembered.
274///
275/// Obtained from [`Graph::read_scope`](crate::graph::Graph::read_scope).
276///
277/// ## Why it holds the memo rather than borrowing it
278///
279/// A guard that borrowed its graph would be unusable from the operations that
280/// need it most. A mutating verb reads (a census, a subtree walk), *then*
281/// stages and commits — and `commit` takes `&mut self`, which an outstanding
282/// `&self` borrow forbids. Every verb in `prov`'s `mutate` is that shape, so a
283/// borrowing guard could only be held across the read half and dropped before
284/// the writes, which in most of them is before the expensive pass has even
285/// started. Sharing the memo instead is what lets one scope cover a whole verb.
286///
287/// The lifetime tie was also a (weak) argument that a scope cannot be stashed
288/// and left open — "a memo with no end is a cache". That argument is now
289/// discipline rather than a type: hold the guard in a local, for one operation.
290/// What has not changed is that nothing outlives it — dropping the outermost
291/// guard clears the memo, whether or not the graph is still around.
292#[must_use = "a read scope ends the moment its guard is dropped"]
293pub struct ReadScope(Arc<Mutex<ReadMemo>>);
294
295impl ReadScope {
296    /// Enter the scope guarded by `memo`.
297    pub(crate) fn open(memo: &Arc<Mutex<ReadMemo>>) -> Self {
298        lock(memo).enter();
299        ReadScope(Arc::clone(memo))
300    }
301}
302
303impl Drop for ReadScope {
304    fn drop(&mut self) {
305        lock(&self.0).leave();
306    }
307}
308
309impl std::fmt::Debug for ReadScope {
310    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311        f.debug_struct("ReadScope")
312            .field("depth", &lock(&self.0).depth)
313            .finish()
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    fn doc(text: &str) -> Document {
322        Document::parse("a.md", text).unwrap()
323    }
324
325    #[test]
326    fn nothing_is_remembered_outside_a_scope() {
327        let mut memo = ReadMemo::default();
328        memo.remember(Path::new("a.md"), "hello", &doc("hello"));
329        assert_eq!(memo.len(), 0);
330        assert!(memo.get(Path::new("a.md")).is_none());
331    }
332
333    #[test]
334    fn a_scope_remembers_and_its_exit_forgets() {
335        let mut memo = ReadMemo::default();
336        memo.enter();
337        memo.remember(Path::new("a.md"), "hello", &doc("hello"));
338        assert_eq!(
339            memo.get(Path::new("a.md")).map(|(text, _)| text),
340            Some("hello".to_string())
341        );
342        memo.leave();
343        assert_eq!(memo.len(), 0);
344    }
345
346    /// The composition case: an operation that opens a scope and calls one that
347    /// opens its own must not lose the memo when the inner one returns.
348    #[test]
349    fn an_inner_scope_does_not_end_the_outer_one() {
350        let mut memo = ReadMemo::default();
351        memo.enter();
352        memo.remember(Path::new("a.md"), "hello", &doc("hello"));
353        memo.enter();
354        memo.leave();
355        assert!(
356            memo.get(Path::new("a.md")).is_some(),
357            "an inner scope's exit dropped the outer scope's memo"
358        );
359        memo.leave();
360        assert_eq!(memo.len(), 0);
361    }
362
363    #[test]
364    fn a_write_forgets_the_document_it_wrote() {
365        let mut memo = ReadMemo::default();
366        memo.enter();
367        memo.remember(Path::new("a.md"), "hello", &doc("hello"));
368        memo.forget(Path::new("a.md"));
369        assert!(memo.get(Path::new("a.md")).is_none());
370        assert_eq!(memo.len(), 0);
371    }
372
373    fn names(entries: &[&str]) -> Arc<DirNames> {
374        let entries: Vec<crate::fs::DirEntry> = entries
375            .iter()
376            .map(|n| crate::fs::DirEntry::new(*n, crate::fs::FileType::FILE))
377            .collect();
378        Arc::new(DirNames::index(&entries))
379    }
380
381    #[test]
382    fn a_listing_answers_an_exact_name_and_a_case_variant_apart() {
383        let names = names(&["Notes.md", "photo.jpg"]);
384        assert!(names.holds(OsStr::new("Notes.md")));
385        assert!(!names.holds(OsStr::new("notes.md")));
386        assert_eq!(
387            names.case_variant(OsStr::new("notes.md")),
388            Some(OsStr::new("Notes.md"))
389        );
390        assert_eq!(names.case_variant(OsStr::new("gone.md")), None);
391    }
392
393    /// Both spellings are present, so the exact one must win however the
394    /// listing was ordered — the reason the index keeps two maps.
395    #[test]
396    fn an_exact_name_wins_over_a_case_variant_of_itself() {
397        let names = names(&["notes.md", "Notes.md"]);
398        assert!(names.holds(OsStr::new("notes.md")));
399        assert!(names.holds(OsStr::new("Notes.md")));
400    }
401
402    #[test]
403    fn a_scope_remembers_a_directory_and_its_exit_forgets() {
404        let mut memo = ReadMemo::default();
405        memo.remember_dir(Path::new("notes"), names(&["a.md"]));
406        assert_eq!(memo.dirs_len(), 0, "nothing is remembered outside a scope");
407
408        memo.enter();
409        memo.remember_dir(Path::new("notes"), names(&["a.md"]));
410        assert!(memo.dir(Path::new("notes")).is_some());
411        memo.leave();
412        assert_eq!(memo.dirs_len(), 0);
413    }
414
415    /// A write creates or removes a name, so the listing that enumerated the
416    /// parent is stale — and it is the only listing that can be.
417    #[test]
418    fn forgetting_a_written_document_forgets_its_parent_listing() {
419        let mut memo = ReadMemo::default();
420        memo.enter();
421        memo.remember_dir(Path::new("notes"), names(&["a.md"]));
422        memo.remember_dir(Path::new("other"), names(&["b.md"]));
423
424        memo.forget(Path::new("notes/new.md"));
425        assert!(
426            memo.dir(Path::new("notes")).is_none(),
427            "the directory the write lands in still answers from before it"
428        );
429        assert!(
430            memo.dir(Path::new("other")).is_some(),
431            "an unrelated directory was dropped"
432        );
433    }
434
435    /// A write at the workspace root forgets the root listing — `parent()` of a
436    /// bare name is the empty path, which is how the root is keyed.
437    #[test]
438    fn forgetting_a_root_document_forgets_the_root_listing() {
439        let mut memo = ReadMemo::default();
440        memo.enter();
441        memo.remember_dir(Path::new(""), names(&["index.md"]));
442        memo.forget(Path::new("new.md"));
443        assert!(memo.dir(Path::new("")).is_none());
444    }
445
446    /// A memo is an optimization, so exceeding its budget must cost speed and
447    /// nothing else — the reads simply stop being remembered.
448    #[test]
449    fn an_oversized_document_is_not_remembered() {
450        let mut memo = ReadMemo::default();
451        memo.enter();
452        let huge = "x".repeat(MAX_DOCUMENT + 1);
453        memo.remember(Path::new("big.md"), &huge, &doc("body"));
454        assert_eq!(memo.len(), 0);
455    }
456}