perf: parallelize toolchain import's tree walk across cores
commit 6cb99f7
perf: parallelize toolchain import's tree walk across cores
build_tree wrote one loose object per file, sequentially, single-threaded;
for a large import (rustup’s lib/rustlib/src/rust alone is tens of
thousands of files) that per-object filesystem syscall overhead dominated
wall time, not hashing or compression (already at zlib’s fastest level).
Each directory’s entries are now written in parallel via rayon, one
gix::odb::Handle clone per entry (Handle holds RefCell`s and so is
`Send but not Sync — it can’t be shared by reference across threads, but
cloning it per entry before the parallel fan-out is the intended way to use
it from more than one thread). A full rustup --embed import dropped from
timing out past 5 minutes to ~40s wall time on 4 cores, producing the exact
same tree id as before.
feat: parallelize `build_tree’s per-entry writes with rayon
Assisted-by: Claude:claude-sonnet-5
crates/git-toolchain/src/lib.rs
@@ -35,6 +35,7 @@
use gix::bstr::ByteSlice as _;
use gix::objs::tree::{Entry as TreeEntry, EntryKind, EntryMode};
use gix::objs::{FindExt as _, Tree, Write as _};
+use rayon::prelude::*;
/// The ref namespace holding toolchains, one ref per toolchain:
/// `refs/meta/toolchains/<name>`. A toolchain's identity is its tip commit's
@@ -366,31 +367,53 @@
}
/// Build `dir`'s tree bottom-up: a directory's own entries are all resolved
-/// (recursing into subdirectories, writing files and symlinks as blobs)
-/// before its own tree object is written, so every child is already an
-/// object id by the time its parent's entry list is sorted and written.
+/// in parallel (recursing into subdirectories, writing files and symlinks as
+/// blobs) before its own tree object is written, so every child is already
+/// an object id by the time its parent's entry list is sorted and written.
+///
+/// A large import (a rustup sysroot's `lib/rustlib/src/rust` alone is tens
+/// of thousands of files) is dominated by per-object filesystem syscall
+/// overhead, not by hashing or compression (gix's loose-object writer already
+/// runs zlib at its fastest level); fanning the write out across every core
+/// is the lever that actually matters here. `gix::odb::Handle` holds
+/// `RefCell`s and so is `Send` but not `Sync` — it cannot be shared by
+/// reference across threads — but it is cheaply `Clone` (an `Arc`-backed
+/// handle to the same store), which is the intended way to use one per
+/// thread; each entry gets its own clone before the parallel fan-out so no
+/// two threads ever touch the same `Handle`.
fn build_tree(odb: &gix::odb::Handle, dir: &Path) -> Result<Tree, Error> {
- let mut entries = Vec::new();
let read_dir = fs::read_dir(dir).map_err(|error| Error::Io(dir.to_owned(), error))?;
- for item in read_dir {
- let item = item.map_err(|error| Error::Io(dir.to_owned(), error))?;
- let path = item.path();
- let name = item
- .file_name()
- .into_string()
- .map_err(|_name| Error::NotUtf8(path.clone()))?;
- let file_type = item
- .file_type()
- .map_err(|error| Error::Io(path.clone(), error))?;
- let Some((oid, mode)) = write_entry(odb, &path, file_type)? else {
- continue;
- };
- entries.push(TreeEntry {
- mode,
- filename: name.into(),
- oid,
- });
- }
+ let items = read_dir
+ .collect::<Result<Vec<_>, _>>()
+ .map_err(|error| Error::Io(dir.to_owned(), error))?;
+
+ let mut entries: Vec<TreeEntry> = items
+ .into_iter()
+ .map(|item| (item, odb.clone()))
+ .collect::<Vec<_>>()
+ .into_par_iter()
+ .map(|(item, odb)| -> Result<Option<TreeEntry>, Error> {
+ let path = item.path();
+ let name = item
+ .file_name()
+ .into_string()
+ .map_err(|_name| Error::NotUtf8(path.clone()))?;
+ let file_type = item
+ .file_type()
+ .map_err(|error| Error::Io(path.clone(), error))?;
+ let Some((oid, mode)) = write_entry(&odb, &path, file_type)? else {
+ return Ok(None);
+ };
+ Ok(Some(TreeEntry {
+ mode,
+ filename: name.into(),
+ oid,
+ }))
+ })
+ .collect::<Result<Vec<Option<TreeEntry>>, Error>>()?
+ .into_iter()
+ .flatten()
+ .collect();
entries.sort();
Ok(Tree { entries })
}