OXYGENHUB

fsck ahead of Git: objects checked in parallel, graphs built with their lines in parallel, a few changes kept (refs #41) acadb5ed28a1

- fsck reads and decodes objects a chunk at a time on every core.
- Building graphs hands each file its blobs, then fills in its inserts'
  lines and builds it in one parallel pass (the lines were filled in one
  file at a time).
- The change encodings kept for inline inserts are the last four read
  (within 512 MB), not the last one: a batch of files that took text
  from the Linux tip and a later change in turn read the tip, hundreds of
  megabytes, again at each switch.

fsck on the Linux tree: 8.2 s (from 13.6 s; Git 12.6 s).
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 23:11 UTC
Signature
✔ signed by Andy McCall
Hash
acadb5ed28a17ba196c8bd519978745dfa54c0f31efa37e47d6df021a7ec1889
Depends on
0f93f49ce059 387335ff7f4d 3f1b8fe25ec9 519c7c061bcb 57d8dc05c9d4 638fb21420d7 982864068a59 c863c7a7a2d1 f9466021286a ff0af1ee299d
Depended on by
98a1620a3840
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.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
16 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/oxy-core/src/repo/fsck.rs +18 −15
…
132 unchanged lines
133 133
    /// Pass 1. Returns the objects that failed, so later passes report
134 134
    /// them once rather than at every reference.
135 135
    fn fsck_objects(&self, rep: &mut FsckReport) -> Result<HashSet<ObjectRef>> {
136 +
        use rayon::prelude::*;
136 137
        let mut bad = HashSet::new();
137 -
        for r in self.store.list()? {
138 -
            rep.objects += 1;
139 -
            match self.store.get(&r) {
140 -
                Ok((ty, body)) => {
141 -
                    if let Err(e) = decodes(ty, &body) {
142 -
                        rep.error(
143 -
                            Some(r),
144 -
                            format!("{} object does not decode: {e}", ty.name()),
145 -
                        );
146 -
                        bad.insert(r);
147 -
                    }
148 -
                }
149 -
                Err(e) => {
150 -
                    rep.error(Some(r), format!("unreadable object: {e}"));
151 -
                    bad.insert(r);
138 +
        // A chunk at a time, read (and verified) and decoded on every core.
139 +
        for refs in self.store.list()?.chunks(8192) {
140 +
            let checked: Vec<std::result::Result<(), String>> = self
141 +
                .store
142 +
                .get_many(refs)
143 +
                .into_par_iter()
144 +
                .map(|got| match got {
145 +
                    Ok((ty, body)) => decodes(ty, &body)
146 +
                        .map_err(|e| format!("{} object does not decode: {e}", ty.name())),
147 +
                    Err(e) => Err(format!("unreadable object: {e}")),
148 +
                })
149 +
                .collect();
150 +
            for (r, c) in refs.iter().zip(checked) {
151 +
                rep.objects += 1;
152 +
                if let Err(e) = c {
153 +
                    rep.error(Some(*r), e);
154 +
                    bad.insert(*r);
152 155
                }
153 156
            }
154 157
        }
…
355 unchanged lines
modified crates/oxy-core/src/repo/mod.rs +23 −9
…
128 unchanged lines
129 129
/// How many changes a state index may fall behind its working copy before
130 130
/// it is written again; until then each load replays them.
131 131
const INDEX_REPLAY_LIMIT: usize = 16;
132 +
/// The changes whose encodings are kept for building graphs: a batch of
133 +
/// files often takes inline text from a few changes in turn.
134 +
const CHANGE_CACHE_COUNT: usize = 4;
135 +
const CHANGE_CACHE_BYTES: usize = 512 << 20;
132 136
/// How many state indexes a workspace keeps.
133 137
const STATE_INDEXES_KEPT: usize = 2;
134 138
const INDEX_REPLAY_TIME: std::time::Duration = std::time::Duration::from_millis(50);
…
77 unchanged lines
212 216
    pub config: Config,
213 217
    /// What the working copy's file system can represent (spec §7.7).
214 218
    platform: crate::portable::Platform,
215 -
    /// The encoding of the change last read for its inline inserts.
216 -
    last_change: std::cell::RefCell<Option<(ChangeHash, std::sync::Arc<Vec<u8>>)>>,
219 +
    /// The encodings of the changes last read for their inline inserts,
220 +
    /// oldest first.
221 +
    last_change: std::cell::RefCell<Vec<(ChangeHash, std::sync::Arc<Vec<u8>>)>>,
217 222
}
218 223
219 224
/// Marker file of a bare repository, inside `.oxy/`.
…
697 unchanged lines
917 922
    }
918 923
919 924
    /// A change's encoding, for building graphs of its inline inserts. The
920 -
    /// last one read is kept: a checkout building graphs a batch at a time
921 -
    /// reads the same large change for every batch.
925 +
    /// last few read are kept: a checkout building graphs a batch at a
926 +
    /// time reads the same large changes for every batch, often in turn.
922 927
    fn change_bytes(
923 928
        &self,
924 929
        h: &ChangeHash,
925 930
    ) -> std::result::Result<std::sync::Arc<Vec<u8>>, crate::objects::ObjectError> {
926 -
        if let Some((k, b)) = &*self.last_change.borrow() {
927 -
            if k == h {
928 -
                return Ok(b.clone());
929 -
            }
931 +
        let mut cache = self.last_change.borrow_mut();
932 +
        if let Some(i) = cache.iter().position(|(k, _)| k == h) {
933 +
            let hit = cache.remove(i);
934 +
            let b = hit.1.clone();
935 +
            cache.push(hit);
936 +
            return Ok(b);
930 937
        }
931 938
        let b = std::sync::Arc::new(self.store.get_typed(h, ObjectType::Change).map_err(|e| {
932 939
            crate::objects::ObjectError::Schema(format!("change {}: {e}", h.short(12)))
933 940
        })?);
934 -
        *self.last_change.borrow_mut() = Some((*h, b.clone()));
941 +
        cache.push((*h, b.clone()));
942 +
        // The last few, within a memory budget; the newest always stays.
943 +
        while cache.len() > CHANGE_CACHE_COUNT
944 +
            || (cache.len() > 1
945 +
                && cache.iter().map(|(_, b)| b.len()).sum::<usize>() > CHANGE_CACHE_BYTES)
946 +
        {
947 +
            cache.remove(0);
948 +
        }
935 949
        Ok(b)
936 950
    }
937 951
…
1042 unchanged lines
modified crates/oxy-core/src/state.rs +53 −40
…
683 unchanged lines
684 684
        return Err(ObjectError::Schema("graph blobs: wrong count".into()));
685 685
    }
686 686
    drop(reading);
687 -
    let lines = crate::timing::phase("graphs: lines of inserts");
687 +
    let lines = crate::timing::phase("graphs: hand out blobs");
688 688
    let mut texts = blobs.split_off(bases).into_iter();
689 689
    let mut blobs = blobs.into_iter();
690 -
    // Pair each graph with its blob, then build them all in parallel:
691 -
    // graphs are independent.
692 -
    let mut work: Vec<(NodeId, DeferredBody, Option<Vec<u8>>)> = Vec::with_capacity(taken.len());
693 -
    for (node, mut d) in taken.drain(..) {
694 -
        // Fill in the lines of inserts read without them, in order.
695 -
        let mut want = std::mem::take(&mut d.texts).into_iter();
696 -
        for (_, edits) in d.edits.iter_mut() {
697 -
            for e in edits.iter_mut() {
698 -
                if let Edit::Insert { lines, .. } = e {
699 -
                    if lines.is_empty() {
700 -
                        *lines = match want.next() {
701 -
                            Some(TextRef::Blob(_, count, form)) => {
702 -
                                let content = texts.next().ok_or_else(|| {
703 -
                                    ObjectError::Schema("text blobs: wrong count".into())
704 -
                                })?;
705 -
                                crate::objects::text_lines(&content, count, form)?
706 -
                            }
707 -
                            Some(TextRef::Inline { change, start, end }) => {
708 -
                                let bytes = &changes[&change];
709 -
                                let range =
710 -
                                    bytes.get(start as usize..end as usize).ok_or_else(|| {
711 -
                                        ObjectError::Schema(
712 -
                                            "an inline insert lies outside its change".into(),
713 -
                                        )
714 -
                                    })?;
715 -
                                crate::objects::inline_lines(range)?
716 -
                            }
717 -
                            None => {
718 -
                                return Err(ObjectError::Schema(
719 -
                                    "an insert's text is missing".into(),
720 -
                                ))
721 -
                            }
722 -
                        };
723 -
                    }
724 -
                }
725 -
            }
690 +
    // Hand each graph its own blobs, in order, then fill in its inserts'
691 +
    // lines and build it, all in parallel: graphs are independent.
692 +
    type Work = (NodeId, DeferredBody, Option<Vec<u8>>, Vec<Vec<u8>>);
693 +
    let mut work: Vec<Work> = Vec::with_capacity(taken.len());
694 +
    for (node, d) in taken.drain(..) {
695 +
        let wanted = d
696 +
            .texts
697 +
            .iter()
698 +
            .filter(|t| matches!(t, TextRef::Blob(..)))
699 +
            .count();
700 +
        let mine: Vec<Vec<u8>> = texts.by_ref().take(wanted).collect();
701 +
        if mine.len() != wanted {
702 +
            return Err(ObjectError::Schema("text blobs: wrong count".into()));
726 703
        }
727 704
        let bytes = match &d.base {
728 705
            Some(DeferredBase::Blob(_)) => Some(
…
3 unchanged lines
732 709
            ),
733 710
            _ => None,
734 711
        };
735 -
        work.push((node, d, bytes));
712 +
        work.push((node, d, bytes, mine));
736 713
    }
737 714
    drop(lines);
738 -
    let _t = crate::timing::phase("graphs: build");
715 +
    let _t = crate::timing::phase("graphs: lines and build");
739 716
    use rayon::prelude::*;
717 +
    let changes = &changes;
740 718
    let built: Vec<(NodeId, FileGraph)> = work
741 719
        .into_par_iter()
742 -
        .map(|(node, mut d, bytes)| {
720 +
        .map(|(node, mut d, bytes, mine)| {
721 +
            // Fill in the lines of inserts read without them, in order.
722 +
            let mut want = std::mem::take(&mut d.texts).into_iter();
723 +
            let mut mine = mine.into_iter();
724 +
            for (_, edits) in d.edits.iter_mut() {
725 +
                for e in edits.iter_mut() {
726 +
                    if let Edit::Insert { lines, .. } = e {
727 +
                        if lines.is_empty() {
728 +
                            *lines = match want.next() {
729 +
                                Some(TextRef::Blob(_, count, form)) => {
730 +
                                    let content = mine.next().ok_or_else(|| {
731 +
                                        ObjectError::Schema("text blobs: wrong count".into())
732 +
                                    })?;
733 +
                                    crate::objects::text_lines(&content, count, form)?
734 +
                                }
735 +
                                Some(TextRef::Inline { change, start, end }) => {
736 +
                                    let bytes = &changes[&change];
737 +
                                    let range = bytes
738 +
                                        .get(start as usize..end as usize)
739 +
                                        .ok_or_else(|| {
740 +
                                            ObjectError::Schema(
741 +
                                                "an inline insert lies outside its change".into(),
742 +
                                            )
743 +
                                        })?;
744 +
                                    crate::objects::inline_lines(range)?
745 +
                                }
746 +
                                None => {
747 +
                                    return Err(ObjectError::Schema(
748 +
                                        "an insert's text is missing".into(),
749 +
                                    ))
750 +
                                }
751 +
                            };
752 +
                        }
753 +
                    }
754 +
                }
755 +
            }
743 756
            let mut graph = match (d.base.take(), bytes) {
744 757
                (Some(DeferredBase::Inline(v)), _) => FileGraph::from_value(&v)?,
745 758
                (Some(DeferredBase::Blob(_)), Some(bytes)) => {
…
1735 unchanged lines