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 |