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//! ## Why the scope, and why it is explicit
17//!
18//! A memo with no end is a cache, and a cache has to be invalidated. A memo
19//! bounded by an *operation* barely has to be: nothing outside prov can write
20//! to the workspace between two of `check`'s sub-passes in any sense prov could
21//! have detected anyway, and everything inside prov that writes goes through
22//! `prov`'s `ChangeSet`, which forgets what it touched
23//! ([`Workspace::commit`]). There is no staleness window left that a stat could
24//! have closed and this cannot.
25//!
26//! The scope is explicit — a caller opens one with
27//! [`Graph::read_scope`](crate::graph::Graph::read_scope) and holds the guard — because a memo that switched
28//! itself on would be a cache again, and because the caller is the only one who
29//! knows where its operation begins. Scopes nest: an operation that opens one
30//! and then calls another that opens its own gets a single memo lasting the
31//! outer one, which is exactly the composition case (`history_capture` opens a
32//! scope and then calls `reachable_files`, which is welcome to open its own).
33//!
34//! ## What is not memoized
35//!
36//! Failures. A read that errored is not remembered, so a missing file is
37//! re-checked rather than pinned to its first answer — the cost is one syscall
38//! on a path prov already knows is trouble, and the alternative is a memo that
39//! can report a file absent after it appeared.
40//!
41//! [`check`]: https://docs.rs/prov
42//! [`load`]: crate::graph::Graph::load
43//! [`Workspace::commit`]: https://docs.rs/prov
44
45use std::collections::HashMap;
46use std::path::{Path, PathBuf};
47use std::sync::{Mutex, MutexGuard};
48
49use crate::document::Document;
50
51/// Take a lock, recovering from a panic that poisoned it.
52///
53/// A memo and a cache are optimizations. Turning a panic that happened
54/// elsewhere into a second panic here would be a bug of prov's own making, and
55/// the worst that can actually be wrong behind a poisoned lock is a stale entry
56/// — which every caller already tolerates by construction.
57///
58/// `Mutex` rather than `RefCell` for the whole family: `&Workspace` must stay
59/// `Send` (`prov/tests/public_api.rs` pins that, so an embedder can drive
60/// `apply` and `discover` from a multi-threaded runtime), which needs the
61/// workspace itself to be `Sync`, which a `RefCell` is not. No guard is ever
62/// held across an `.await`.
63pub fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
64    mutex
65        .lock()
66        .unwrap_or_else(|poisoned| poisoned.into_inner())
67}
68
69/// How much document text one memo may hold. A workspace of a few thousand
70/// markdown documents lands well under this; the cap is here so that a
71/// workspace of unusually large documents degrades to "re-reads some of them"
72/// rather than to holding the whole tree in memory at once.
73const BUDGET: usize = 32 * 1024 * 1024;
74
75/// The largest single document worth remembering. A file this big is rare
76/// enough that keeping it would evict nothing useful and buy nothing.
77const MAX_DOCUMENT: usize = 1024 * 1024;
78
79/// What one operation has already read.
80#[derive(Debug, Default)]
81pub struct ReadMemo {
82    /// Scope nesting depth. Nothing is remembered outside a scope, and the
83    /// outermost exit is what clears it.
84    depth: usize,
85    docs: HashMap<PathBuf, (String, Document)>,
86    /// Text held, against [`BUDGET`].
87    bytes: usize,
88}
89
90impl ReadMemo {
91    pub(crate) fn enter(&mut self) {
92        self.depth += 1;
93    }
94
95    /// Leave one scope. Only the outermost exit drops what was remembered — an
96    /// inner scope is covered by the one that encloses it.
97    pub(crate) fn leave(&mut self) {
98        self.depth = self.depth.saturating_sub(1);
99        if self.depth == 0 {
100            self.clear();
101        }
102    }
103
104    /// What was read for `path` this operation, if anything. `None` outside a
105    /// scope, always: a memo nobody opened holds nothing.
106    pub(crate) fn get(&self, path: &Path) -> Option<(String, Document)> {
107        if self.depth == 0 {
108            return None;
109        }
110        self.docs.get(path).cloned()
111    }
112
113    /// Remember what `path` read as. A no-op outside a scope, over the
114    /// per-document ceiling, or once the budget is spent.
115    pub(crate) fn remember(&mut self, path: &Path, text: &str, doc: &Document) {
116        if self.depth == 0 || text.len() > MAX_DOCUMENT || self.bytes + text.len() > BUDGET {
117            return;
118        }
119        self.bytes += text.len();
120        self.docs
121            .insert(path.to_path_buf(), (text.to_string(), doc.clone()));
122    }
123
124    /// Forget `path` — what a write to it means.
125    pub fn forget(&mut self, path: &Path) {
126        if let Some((text, _)) = self.docs.remove(path) {
127            self.bytes -= text.len();
128        }
129    }
130
131    pub(crate) fn clear(&mut self) {
132        self.docs.clear();
133        self.bytes = 0;
134    }
135
136    /// How many documents are remembered — the observable the tests assert on.
137    #[cfg(test)]
138    pub(crate) fn len(&self) -> usize {
139        self.docs.len()
140    }
141}
142
143/// An open read scope. Hold it for the operation; dropping it leaves the scope,
144/// and dropping the outermost one drops everything the operation remembered.
145///
146/// Obtained from [`Graph::read_scope`](crate::graph::Graph::read_scope)(crate::graph::Graph::read_scope).
147#[must_use = "a read scope ends the moment its guard is dropped"]
148pub struct ReadScope<'a>(pub(crate) &'a Mutex<ReadMemo>);
149
150impl<'a> ReadScope<'a> {
151    /// Enter the scope guarded by `memo`.
152    pub(crate) fn open(memo: &'a Mutex<ReadMemo>) -> Self {
153        lock(memo).enter();
154        ReadScope(memo)
155    }
156}
157
158impl Drop for ReadScope<'_> {
159    fn drop(&mut self) {
160        lock(self.0).leave();
161    }
162}
163
164impl std::fmt::Debug for ReadScope<'_> {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        f.debug_struct("ReadScope")
167            .field("depth", &lock(self.0).depth)
168            .finish()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    fn doc(text: &str) -> Document {
177        Document::parse("a.md", text).unwrap()
178    }
179
180    #[test]
181    fn nothing_is_remembered_outside_a_scope() {
182        let mut memo = ReadMemo::default();
183        memo.remember(Path::new("a.md"), "hello", &doc("hello"));
184        assert_eq!(memo.len(), 0);
185        assert!(memo.get(Path::new("a.md")).is_none());
186    }
187
188    #[test]
189    fn a_scope_remembers_and_its_exit_forgets() {
190        let mut memo = ReadMemo::default();
191        memo.enter();
192        memo.remember(Path::new("a.md"), "hello", &doc("hello"));
193        assert_eq!(
194            memo.get(Path::new("a.md")).map(|(text, _)| text),
195            Some("hello".to_string())
196        );
197        memo.leave();
198        assert_eq!(memo.len(), 0);
199    }
200
201    /// The composition case: an operation that opens a scope and calls one that
202    /// opens its own must not lose the memo when the inner one returns.
203    #[test]
204    fn an_inner_scope_does_not_end_the_outer_one() {
205        let mut memo = ReadMemo::default();
206        memo.enter();
207        memo.remember(Path::new("a.md"), "hello", &doc("hello"));
208        memo.enter();
209        memo.leave();
210        assert!(
211            memo.get(Path::new("a.md")).is_some(),
212            "an inner scope's exit dropped the outer scope's memo"
213        );
214        memo.leave();
215        assert_eq!(memo.len(), 0);
216    }
217
218    #[test]
219    fn a_write_forgets_the_document_it_wrote() {
220        let mut memo = ReadMemo::default();
221        memo.enter();
222        memo.remember(Path::new("a.md"), "hello", &doc("hello"));
223        memo.forget(Path::new("a.md"));
224        assert!(memo.get(Path::new("a.md")).is_none());
225        assert_eq!(memo.len(), 0);
226    }
227
228    /// A memo is an optimization, so exceeding its budget must cost speed and
229    /// nothing else — the reads simply stop being remembered.
230    #[test]
231    fn an_oversized_document_is_not_remembered() {
232        let mut memo = ReadMemo::default();
233        memo.enter();
234        let huge = "x".repeat(MAX_DOCUMENT + 1);
235        memo.remember(Path::new("big.md"), &huge, &doc("body"));
236        assert_eq!(memo.len(), 0);
237    }
238}