OXYGENHUB

Coverage / crates/oxy-core/src/repo/checkpoint.rs 92.4%

170 of 184 lines run, 18 of 30 functions · run #384 on main · run missed

1
//! Checkpoints (spec §11): a serialized state kept per bookmark so loading
2
//! a state costs the changes since the checkpoint rather than all of them.
3
//!
4
//! The newest checkpoint of each bookmark is pointed at by
5
//! `.oxy/checkpoints/<bookmark>`; older ones become unreachable and are
6
//! garbage collected. A checkpoint applies to a requested head set when all
7
//! of its heads lie in that set's closure, which the loader establishes while
8
//! walking from the requested heads and stopping at checkpoint members.
9
10
use super::*;
11
use crate::objects::Checkpoint;
12
13
impl Repo {
143022×
    fn checkpoint_pointer(&self, bookmark: &str) -> PathBuf {
153022×
        self.oxy.join("checkpoints").join(name_file(bookmark))
163022×
    }
17
18
    /// The newest checkpoint recorded for a bookmark.
193008×
    pub fn latest_checkpoint(&self, bookmark: &str) -> Result<Option<(ObjectRef, Checkpoint)>> {
203008×
        match fs::read_to_string(self.checkpoint_pointer(bookmark)) {
2177×
            Ok(h) => {
2277×
                let r = ObjectRef::from_hex(h.trim())
2377×
                    .ok_or_else(|| RepoError::Msg("corrupt checkpoint pointer".into()))?;
2477×
                match self.store.get_typed(&r, ObjectType::Checkpoint) {
2577×
                    Ok(bytes) => Ok(Some((r, Checkpoint::decode(&bytes)?))),
260×
                    Err(StoreError::NotFound(_)) => Ok(None),
270×
                    Err(e) => Err(e.into()),
28
                }
29
            }
302931×
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
310×
            Err(e) => Err(e.into()),
32
        }
333008×
    }
34
35
    /// Every checkpoint pointer, for garbage collection roots.
36102×
    pub(crate) fn checkpoint_refs(&self) -> Result<Vec<ObjectRef>> {
37102×
        let dir = self.oxy.join("checkpoints");
38102×
        let mut out = Vec::new();
39102×
        if let Ok(rd) = fs::read_dir(&dir) {
4011×
            for e in rd.flatten() {
4111×
                if let Ok(h) = fs::read_to_string(e.path()) {
4211×
                    if let Some(r) = ObjectRef::from_hex(h.trim()) {
4311×
                        out.push(r);
4411×
                    }
450×
                }
46
            }
4791×
        }
48102×
        Ok(out)
49102×
    }
50
51
    /// Write a checkpoint of `state` (whose heads are `heads`) for `bookmark`.
5210×
    pub(crate) fn write_checkpoint(
5310×
        &self,
5410×
        bookmark: &str,
5510×
        state: &State,
5610×
        heads: &[ChangeHash],
5710×
    ) -> Result<(ObjectRef, usize)> {
5810×
        let batch = self.store.begin_batch()?;
5910×
        let (blob, payload_len) = self.store_checkpoint_payload(state)?;
6010×
        let cp = Checkpoint {
6110×
            heads: StateObject::new(heads.to_vec()).heads,
6210×
            count: state.len() as u64,
6310×
            payload: blob,
6410×
        };
6510×
        let r = self.store.put(ObjectType::Checkpoint, &cp.encode())?;
6610×
        batch.finish()?;
6710×
        fs::create_dir_all(self.oxy.join("checkpoints"))?;
6810×
        write_atomic(&self.checkpoint_pointer(bookmark), r.to_hex().as_bytes())?;
6910×
        Ok((r, payload_len))
7010×
    }
71
72
    /// Store `state` as a version 2 payload (spec §11): the graphs of large
73
    /// files as blobs of their own, shared with earlier checkpoints when
74
    /// unchanged, and the index naming them. Returns the index blob and its size.
7510×
    pub(crate) fn store_checkpoint_payload(&self, state: &State) -> Result<(ObjectRef, usize)> {
7610×
        let graphs = state.checkpoint_file_graphs();
7740×
        for (_, enc) in &graphs {
7840×
            if enc.len() >= crate::state::INLINE_GRAPH_LIMIT {
793×
                self.store_blob(enc)?;
8037×
            }
81
        }
8210×
        let index = state.checkpoint_index(&graphs, Repo::blob_ref);
8310×
        Ok((self.store_blob(&index)?, index.len()))
8410×
    }
85
86
    /// The graph blobs a checkpoint's payload names (none for version 1).
879×
    pub(crate) fn checkpoint_graph_blobs(&self, cp: &Checkpoint) -> Result<Vec<ObjectRef>> {
889×
        let payload = self.read_blob(&cp.payload)?;
899×
        Ok(State::checkpoint_graph_refs(&payload)?)
909×
    }
91
92
    /// Decode a checkpoint's payload, reading its graph blobs from the store.
9319×
    pub(crate) fn state_from_checkpoint(&self, payload: &[u8]) -> Result<State> {
9419×
        Ok(State::from_checkpoint(payload, |refs| {
954×
            self.read_blobs(refs).map_err(|e| {
962×
                crate::objects::ObjectError::Schema(format!("checkpoint graph blobs: {e}"))
972×
            })
984×
        })?)
9919×
    }
100
101
    /// Checkpoint the current bookmark's state now.
1026×
    pub fn checkpoint(&self) -> Result<(ObjectRef, usize, usize)> {
1036×
        let heads = self.bookmark_heads()?;
1046×
        let state = self.load_state_full(&heads)?;
1056×
        let (r, bytes) = self.write_checkpoint(&self.config.bookmark, &state, &heads)?;
1066×
        Ok((r, state.len(), bytes))
1076×
    }
108
109
    /// Point `bookmark` at an already stored checkpoint (a shallow clone's base).
1104×
    pub(crate) fn install_checkpoint(&self, bookmark: &str, r: &ObjectRef) -> Result<()> {
1114×
        Checkpoint::decode(&self.store.get_typed(r, ObjectType::Checkpoint)?)?;
1124×
        fs::create_dir_all(self.oxy.join("checkpoints"))?;
1134×
        write_atomic(&self.checkpoint_pointer(bookmark), r.to_hex().as_bytes())
1144×
    }
115
116
    /// A bundle holding a checkpoint of exactly `heads` with its payload
117
    /// (spec §16.1 `checkpoint`); the current bookmark checkpoint is reused
118
    /// when it matches, otherwise one is made and, when `heads` are the
119
    /// bookmark's, kept.
1204×
    pub fn checkpoint_bundle_for(&self, heads: &[ChangeHash]) -> Result<Bundle> {
1214×
        let t_find = crate::timing::phase("checkpoint: find or make");
1224×
        let wanted = StateObject::new(heads.to_vec()).heads;
1234×
        let bookmark = self.config.bookmark.clone();
1244×
        let current = self.bookmark_heads()?;
1254×
        let r = match self.latest_checkpoint(&bookmark)? {
1262×
            Some((r, cp)) if cp.heads == wanted => r,
127
            _ => {
1282×
                let state = self.load_state(&wanted)?;
1292×
                if current == wanted {
1302×
                    self.write_checkpoint(&bookmark, &state, &wanted)?.0
131
                } else {
1320×
                    let (blob, _) = self.store_checkpoint_payload(&state)?;
1330×
                    let cp = Checkpoint {
1340×
                        heads: wanted.clone(),
1350×
                        count: state.len() as u64,
1360×
                        payload: blob,
1370×
                    };
1380×
                    self.store.put(ObjectType::Checkpoint, &cp.encode())?
139
                }
140
            }
141
        };
1424×
        drop(t_find);
1434×
        let _t = crate::timing::phase("checkpoint: gather its objects");
1444×
        let cp = Checkpoint::decode(&self.store.get_typed(&r, ObjectType::Checkpoint)?)?;
1454×
        let mut bundle = Bundle::default();
1464×
        for c in Blob::decode(&self.store.get_typed(&cp.payload, ObjectType::Blob)?)?.chunks {
1474×
            bundle.objects.push(self.store.get(&c)?);
148
        }
1494×
        bundle.objects.push(self.store.get(&cp.payload)?);
150
        // The graph blobs and their chunks in two parallel reads (#137): a
151
        // big state's are tens of thousands, and one by one they took six
152
        // minutes for VS Code.
1534×
        let graphs = self.checkpoint_graph_blobs(&cp)?;
1544×
        let mut blobs = Vec::with_capacity(graphs.len());
1554×
        for got in self.store.get_many(&graphs) {
1561×
            blobs.push(got?);
157
        }
1584×
        let mut chunks = Vec::new();
1594×
        for (ty, body) in &blobs {
1601×
            if *ty != ObjectType::Blob {
1610×
                return fail("a checkpoint graph is not a blob");
1621×
            }
1631×
            chunks.extend(Blob::decode(body)?.chunks);
164
        }
1654×
        for got in self.store.get_many(&chunks) {
1661×
            bundle.objects.push(got?);
167
        }
1684×
        bundle.objects.extend(blobs);
1694×
        bundle.objects.push((ObjectType::Checkpoint, cp.encode()));
1704×
        Ok(bundle)
1714×
    }
172
173
    /// Load the state of `heads` using a checkpoint when one applies.
174210×
    pub(crate) fn load_state_checkpointed(&self, heads: &[ChangeHash]) -> Result<Option<State>> {
175210×
        self.load_state_checkpointed_as(heads, false)
176210×
    }
177
178
    /// As [`Repo::load_state_checkpointed`]; `history` builds no file graph.
1792258×
    pub(crate) fn load_state_checkpointed_as(
1802258×
        &self,
1812258×
        heads: &[ChangeHash],
1822258×
        history: bool,
1832258×
    ) -> Result<Option<State>> {
1842258×
        let mut bookmarks: Vec<String> = vec![self.config.bookmark.clone()];
1852258×
        if let Ok(rd) = fs::read_dir(self.oxy.join("checkpoints")) {
18658×
            for e in rd.flatten() {
18758×
                let name = file_name_to_name(&e.file_name().to_string_lossy());
18858×
                if !bookmarks.contains(&name) {
1890×
                    bookmarks.push(name);
19058×
                }
191
            }
1922200×
        }
1932258×
        for b in bookmarks {
1942258×
            let Some((_, cp)) = self.latest_checkpoint(&b)? else {
1952200×
                continue;
196
            };
19758×
            let payload = {
19858×
                let _t = crate::timing::phase("load: read checkpoint");
19958×
                self.read_blob(&cp.payload)?
200
            };
20157×
            let base = {
20258×
                let _t = crate::timing::phase("load: decode checkpoint");
20358×
                if history {
20448×
                    State::from_checkpoint_history(&payload)?
205
                } else {
20610×
                    self.state_from_checkpoint(&payload)?
207
                }
208
            };
20957×
            let cp_heads: BTreeSet<ChangeHash> = cp.heads.iter().copied().collect();
21057×
            let mut found: BTreeSet<ChangeHash> = BTreeSet::new();
21157×
            let mut delta: Vec<(ChangeHash, Change, Vec<crate::objects::TextRef>)> = Vec::new();
21257×
            let mut seen: BTreeSet<ChangeHash> = BTreeSet::new();
21357×
            let mut stack: Vec<ChangeHash> = heads.to_vec();
214240×
            while let Some(h) = stack.pop() {
215183×
                if !seen.insert(h) {
21628×
                    continue;
217155×
                }
218155×
                if base.contains(&h) {
219101×
                    if cp_heads.contains(&h) {
22052×
                        found.insert(h);
22152×
                    }
222101×
                    continue;
22354×
                }
224
                // For history, the skeleton: text is read only for the
225
                // graphs built later, and a state holding none can be
226
                // indexed (oxygen#93).
22754×
                let (c, texts) = if history {
22847×
                    self.store.get_change_lazy(&h)?
229
                } else {
2307×
                    (self.store.get_change(&h)?, Vec::new())
231
                };
23254×
                stack.extend(c.deps.iter().copied());
23354×
                delta.push((h, c, texts));
234
            }
23557×
            if found.len() != cp_heads.len() {
2366×
                continue;
23751×
            }
23851×
            let _t = crate::timing::phase("load: apply delta and validate");
23951×
            let mut state = base;
24051×
            for (h, c, texts) in delta {
24145×
                if history {
24238×
                    state.add_known(h, c, texts);
24338×
                } else {
2447×
                    state.add_unchecked(c);
2457×
                }
246
            }
24751×
            state.validate()?;
24851×
            return Ok(Some(state));
249
        }
2502206×
        Ok(None)
2512258×
    }
252
}