OXYGENHUB

fsck checks a change's text as its blobs, without decoding it d77e1702d770

fsck's references pass read every change whole, so for the Linux tree's
first change it decoded all 1.8 GB of text only to check dependencies,
the author and the blobs. It now reads the change's skeleton and checks
each text blob and its chunks as it does an opaque file's (the objects
pass has checked every object's hash already). fsck's passes are timed.
On the Linux tip fsck takes 17.2 s and 984 MB (from 24.7 s and 2.8 GB).
A test checks a missing text blob is still named.

refs #35
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 03:13 UTC
Signature
✔ signed by Andy McCall
Hash
d77e1702d7705eb25261a1ad1021fb2ac63478312ab41c5f050479de323858fe
Depends on
1d360c91a289 387335ff7f4d 982864068a59 a87c7b9d563a da652d6392d8
Depended on by
648189388f1a a07f60a37fb2 75b6c8665219
In bookmarks
agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
6 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/oxy-core/src/repo/fsck.rs +27 −5
…
109 unchanged lines
110 110
    /// Check the whole repository; see the module documentation.
111 111
    pub fn fsck(&self) -> Result<FsckReport> {
112 112
        let mut rep = FsckReport::default();
113 -
        let bad = self.fsck_objects(&mut rep)?;
114 -
        self.fsck_ops(&mut rep, &bad);
115 -
        self.fsck_references(&mut rep, &bad)?;
116 -
        self.fsck_derived(&mut rep)?;
113 +
        let bad = {
114 +
            let _t = crate::timing::phase("fsck: objects");
115 +
            self.fsck_objects(&mut rep)?
116 +
        };
117 +
        {
118 +
            let _t = crate::timing::phase("fsck: operations");
119 +
            self.fsck_ops(&mut rep, &bad);
120 +
        }
121 +
        {
122 +
            let _t = crate::timing::phase("fsck: references");
123 +
            self.fsck_references(&mut rep, &bad)?;
124 +
        }
125 +
        {
126 +
            let _t = crate::timing::phase("fsck: states and checkpoints");
127 +
            self.fsck_derived(&mut rep)?;
128 +
        }
117 129
        rep.problems.sort_by_key(|p| std::cmp::Reverse(p.severity));
118 130
        Ok(rep)
119 131
    }
…
154 unchanged lines
274 286
            if !seen.insert(h) {
275 287
                continue;
276 288
            }
277 -
            let c = match self.store.get_change(&h) {
289 +
            // The skeleton: the text is checked as the blobs it is stored
290 +
            // in, not decoded (the objects pass checked every object).
291 +
            let (c, texts) = match self.store.get_change_lazy(&h) {
278 292
                Ok(c) => c,
279 293
                Err(StoreError::NotFound(_)) if boundary.contains(&h) => continue,
280 294
                Err(StoreError::NotFound(_)) => {
…
15 unchanged lines
296 310
                    Some(h),
297 311
                    format!("author identity {} is not stored", c.author.short(12)),
298 312
                );
313 +
            }
314 +
            for t in texts {
315 +
                if let crate::objects::TextRef::Blob(b, _, _) = t {
316 +
                    if blobs_seen.insert(b) {
317 +
                        // A change cannot be read without its text: never partial.
318 +
                        self.fsck_blob(rep, b, &label, false, bad);
319 +
                    }
320 +
                }
299 321
            }
300 322
            for e in &c.edits {
301 323
                let Edit::Replace { old, new, .. } = e else {
…
186 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +26 −0
…
6172 unchanged lines
6173 6173
    assert!(view.conflicts.is_empty(), "{:?}", view.conflicts);
6174 6174
    assert!(view.entries.contains_key("d/new.txt"));
6175 6175
}
6176 +
6177 +
/// fsck checks a change's text as the blobs it is stored in, without
6178 +
/// decoding it: a missing text blob is an error named for the change.
6179 +
#[test]
6180 +
fn fsck_names_a_missing_text_blob() {
6181 +
    hermetic();
6182 +
    let dir = tempfile::tempdir().unwrap();
6183 +
    let root = dir.path();
6184 +
    let repo = Repo::init(root).unwrap();
6185 +
    let big: String = (0..3000).map(|i| format!("line {i}\n")).collect();
6186 +
    write(root, "big.txt", big.as_bytes());
6187 +
    let h = repo.commit("big", &[]).unwrap();
6188 +
    assert_eq!(repo.fsck().unwrap().errors(), 0);
6189 +
    let (_, texts) = repo.store().get_change_lazy(&h).unwrap();
6190 +
    let Some(crate::objects::TextRef::Blob(b, _, _)) = texts.first().copied() else {
6191 +
        panic!("a stored text")
6192 +
    };
6193 +
    fs::remove_file(repo.store().path_for(&b)).unwrap();
6194 +
    let rep = repo.fsck().unwrap();
6195 +
    assert!(rep.errors() > 0);
6196 +
    assert!(
6197 +
        rep.problems.iter().any(|p| p.what.contains(&h.short(12))),
6198 +
        "{:?}",
6199 +
        rep.problems
6200 +
    );
6201 +
}
modified docs/benchmarks.md +10 −0
…
486 unchanged lines
487 487
on every core: 357 MB (349 MB pack) in 52 s. The text blob framing is the
488 488
next step (a change format version 3 with raw text blobs).
489 489
490 +
### fsck (2026-09-30)
491 +
492 +
On the Linux tip in change format 3, `oxy fsck` took 24.7 s and 2.8 GB:
493 +
its references pass read every change whole, the first one's 1.8 GB of
494 +
text into memory, to check dependencies, authors and blobs. It now reads
495 +
a change's skeleton and checks its text as the blobs it is stored in (the
496 +
objects pass has checked every object already): 17.2 s and 984 MB, the
497 +
references pass 1.7 s. Phases are timed (`OXY_TIMINGS`): objects 5.1 s,
498 +
references 1.7 s, states and checkpoints 10.4 s.
499 +