use anyhow::anyhow; use git_hash::ObjectId; use git_odb::Find; use std::{ path::{Path, PathBuf}, time::Instant, }; const GITOXIDE_STATIC_CACHE_SIZE: usize = 64; const GITOXIDE_CACHED_OBJECT_DATA_PER_THREAD_IN_BYTES: usize = 60_000_000; fn main() -> anyhow::Result<()> { let repo_git_dir = std::env::args() .nth(1) .ok_or_else(|| anyhow!("First argument is the .git directory to work in")) .and_then(|p| { let p = PathBuf::from(p).canonicalize()?; if p.extension().unwrap_or_default() == "git" || p.file_name().unwrap_or_default() == ".git" || p.join("HEAD").is_file() { Ok(p) } else { Err(anyhow!("Path '{}' needs to be a .git directory", p.display())) } })?; let repo_objects_dir = { let mut d = repo_git_dir.clone(); d.push("objects"); d }; let hashes = { let db = git_odb::linked::Db::at(&repo_objects_dir)?; let start = Instant::now(); let hashes = db.iter().collect::, _>>()?; let elapsed = start.elapsed(); println!("gitoxide: {} objects (collected in {:?}", hashes.len(), elapsed); hashes }; let start = Instant::now(); let bytes = do_gitoxide(&hashes, &repo_objects_dir, || { git_odb::pack::cache::lru::MemoryCappedHashmap::new(GITOXIDE_CACHED_OBJECT_DATA_PER_THREAD_IN_BYTES) })?; let elapsed = start.elapsed(); let objs_per_sec = |elapsed: std::time::Duration| hashes.len() as f32 / elapsed.as_secs_f32(); println!( "gitoxide (cache = {:.0}MB): confirmed {} bytes in {:?} ({:0.0} objects/s)", GITOXIDE_CACHED_OBJECT_DATA_PER_THREAD_IN_BYTES as f32 / (1024 * 1024) as f32, bytes, elapsed, objs_per_sec(elapsed) ); let start = Instant::now(); let bytes = do_gitoxide( &hashes, &repo_objects_dir, git_odb::pack::cache::lru::StaticLinkedList::::default, )?; let elapsed = start.elapsed(); let objs_per_sec = |elapsed: std::time::Duration| hashes.len() as f32 / elapsed.as_secs_f32(); println!( "gitoxide (small static cache): confirmed {} bytes in {:?} ({:0.0} objects/s)", bytes, elapsed, objs_per_sec(elapsed) ); let start = Instant::now(); let bytes = do_gitoxide_in_parallel(&hashes, &repo_objects_dir, || { git_odb::pack::cache::lru::MemoryCappedHashmap::new(GITOXIDE_CACHED_OBJECT_DATA_PER_THREAD_IN_BYTES) })?; let elapsed = start.elapsed(); println!( "parallel gitoxide (cache = {:.0}MB): confirmed {} bytes in {:?} ({:0.0} objects/s)", GITOXIDE_CACHED_OBJECT_DATA_PER_THREAD_IN_BYTES as f32 / (1024 * 1024) as f32, bytes, elapsed, objs_per_sec(elapsed) ); let start = Instant::now(); let bytes = do_gitoxide_in_parallel( &hashes, &repo_objects_dir, git_odb::pack::cache::lru::StaticLinkedList::::default, )?; let elapsed = start.elapsed(); println!( "parallel gitoxide (small static cache): confirmed {} bytes in {:?} ({:0.0} objects/s)", bytes, elapsed, objs_per_sec(elapsed) ); let start = Instant::now(); let bytes = do_gitoxide(&hashes, &repo_objects_dir, git_odb::pack::cache::Never::default)?; let elapsed = start.elapsed(); println!( "gitoxide (uncached): confirmed {} bytes in {:?} ({:0.0} objects/s)", bytes, elapsed, objs_per_sec(elapsed) ); let start = Instant::now(); let bytes = do_gitoxide_in_parallel(&hashes, &repo_objects_dir, git_odb::pack::cache::Never::default)?; let elapsed = start.elapsed(); println!( "parallel gitoxide (uncached): confirmed {} bytes in {:?} ({:0.0} objects/s)", bytes, elapsed, objs_per_sec(elapsed) ); let start = Instant::now(); let bytes = do_git2(hashes.as_slice(), &repo_git_dir)?; let elapsed = start.elapsed(); println!( "libgit2: confirmed {} bytes in {:?} ({:0.0} objects/s)", bytes, elapsed, objs_per_sec(elapsed) ); let start = Instant::now(); let bytes = do_parallel_git2(hashes.as_slice(), &repo_git_dir)?; let elapsed = start.elapsed(); println!( "parallel libgit2: confirmed {} bytes in {:?} ({:0.0} objects/s)", bytes, elapsed, objs_per_sec(elapsed) ); Ok(()) } fn do_git2(hashes: &[ObjectId], git_dir: &Path) -> anyhow::Result { git2::opts::strict_hash_verification(false); let repo = git2::Repository::open(git_dir)?; let odb = repo.odb()?; let mut bytes = 0u64; for hash in hashes { let hash = git2::Oid::from_bytes(hash.as_slice())?; let obj = odb.read(hash)?; bytes += obj.len() as u64; } Ok(bytes) } fn do_parallel_git2(hashes: &[ObjectId], git_dir: &Path) -> anyhow::Result { use rayon::prelude::*; git2::opts::strict_hash_verification(false); let bytes = std::sync::atomic::AtomicU64::default(); hashes.par_iter().try_for_each_init::<_, _, _, anyhow::Result<_>>( || git2::Repository::open(git_dir).expect("git directory is valid"), |repo, hash| { let odb = repo.odb()?; let hash = git2::Oid::from_bytes(hash.as_slice())?; let obj = odb.read(hash)?; bytes.fetch_add(obj.len() as u64, std::sync::atomic::Ordering::Relaxed); Ok(()) }, )?; Ok(bytes.load(std::sync::atomic::Ordering::Acquire)) } fn do_gitoxide(hashes: &[ObjectId], objects_dir: &Path, new_cache: impl FnOnce() -> C) -> anyhow::Result where C: git_odb::pack::cache::DecodeEntry, { let odb = git_odb::linked::Db::at(objects_dir)?; let mut buf = Vec::new(); let mut bytes = 0u64; let mut cache = new_cache(); for hash in hashes { let obj = odb.find(hash, &mut buf, &mut cache)?.expect("object must exist"); bytes += obj.data.len() as u64; } Ok(bytes) } fn do_gitoxide_in_parallel( hashes: &[ObjectId], objects_dir: &Path, new_cache: impl Fn() -> C + Sync + Send, ) -> anyhow::Result where C: git_odb::pack::cache::DecodeEntry, { use rayon::prelude::*; let odb = git_odb::linked::Db::at(objects_dir)?; let bytes = std::sync::atomic::AtomicU64::default(); hashes.par_iter().try_for_each_init::<_, _, _, anyhow::Result<_>>( || (Vec::new(), new_cache()), |(buf, cache), hash| { let obj = odb.find(hash, buf, cache)?.expect("object must exist"); bytes.fetch_add(obj.data.len() as u64, std::sync::atomic::Ordering::Relaxed); Ok(()) }, )?; Ok(bytes.load(std::sync::atomic::Ordering::Acquire)) }