From d2a393ace09ae1d90b2558a53315ee84c495186b Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Thu, 4 Jun 2026 11:48:42 -0700 Subject: [PATCH] Use git_odb_exists instead of git_revparse_single for ObjectExists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the heavyweight git_revparse_single call in LibGit2Repo.ObjectExists with git_odb_exists, a purpose-built existence check that skips revparse expression parsing and git_object handle allocation. Benchmarked on an os.2020 enlistment (59.7M objects, 14 packs): - Existing objects: ~800 ns/op (comparable) - Missing objects: 1.3ms vs 2.8ms (2.1x faster) The ODB handle is lazily acquired on first ObjectExists call via git_repository_odb (returns the repo's internal ODB, ref-counted) and freed in Dispose. Concurrent first-time calls are safe via Interlocked.CompareExchange — the loser frees its duplicate handle. Falls back to revparse if ODB acquisition fails. Add ObjectCanBeParsed method that retains the old revparse behavior for callers that need corruption detection (LooseObjectsStep) or that may receive refs/abbreviated SHAs rather than full 40-char hex (BlobPrefetcher.DownloadMissingCommit, which takes raw CLI input). Assisted-by: Claude Opus 4.6 Signed-off-by: Tyler Vella --- GVFS/GVFS.Common/Git/GitRepo.cs | 12 +++ GVFS/GVFS.Common/Git/LibGit2Repo.cs | 73 +++++++++++++++++++ .../Maintenance/LooseObjectsStep.cs | 2 +- GVFS/GVFS.Common/Prefetch/BlobPrefetcher.cs | 5 +- 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/GVFS/GVFS.Common/Git/GitRepo.cs b/GVFS/GVFS.Common/Git/GitRepo.cs index d88ebbd89..7b6dfddb8 100644 --- a/GVFS/GVFS.Common/Git/GitRepo.cs +++ b/GVFS/GVFS.Common/Git/GitRepo.cs @@ -114,6 +114,18 @@ public virtual bool ObjectExists(string blobSha) return output; } + /// + /// Checks whether the object can be fully parsed by libgit2 (not just that it exists). + /// Use this to detect corrupt objects. For simple existence checks, + /// prefer which is faster. + /// + public virtual bool ObjectCanBeParsed(string sha) + { + bool output = false; + this.libgit2RepoInvoker.TryInvoke(repo => repo.ObjectCanBeParsed(sha), out output); + return output; + } + /// /// Try to find the size of a given blob by SHA1 hash. /// diff --git a/GVFS/GVFS.Common/Git/LibGit2Repo.cs b/GVFS/GVFS.Common/Git/LibGit2Repo.cs index f0e7bc464..dafcc8d54 100644 --- a/GVFS/GVFS.Common/Git/LibGit2Repo.cs +++ b/GVFS/GVFS.Common/Git/LibGit2Repo.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; +using System.Threading; namespace GVFS.Common.Git { public class LibGit2Repo : IDisposable { private bool disposedValue = false; + private IntPtr odbHandle = IntPtr.Zero; public delegate void MultiVarConfigCallback(string value); @@ -104,6 +106,55 @@ public virtual bool CommitAndRootTreeExists(string commitish, out string treeSha } public virtual bool ObjectExists(string sha) + { + IntPtr odb = this.odbHandle; + if (odb == IntPtr.Zero) + { + if (Native.Odb.GetOdb(out IntPtr newOdb, this.RepoHandle) != Native.ResultCode.Success) + { + return this.ObjectExistsFallback(sha); + } + + IntPtr existing = Interlocked.CompareExchange(ref this.odbHandle, newOdb, IntPtr.Zero); + if (existing != IntPtr.Zero) + { + // Another thread won the race — free our duplicate and use theirs + Native.Odb.Free(newOdb); + odb = existing; + } + else + { + odb = newOdb; + } + } + + GitOid oid; + if (Native.Odb.OidFromStr(out oid, sha) != Native.ResultCode.Success) + { + return false; + } + + return Native.Odb.Exists(odb, ref oid) == 1; + } + + private bool ObjectExistsFallback(string sha) + { + IntPtr objHandle; + if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.ResultCode.Success) + { + return false; + } + + Native.Object.Free(objHandle); + return true; + } + + /// + /// Checks whether the object can be fully parsed by libgit2 (not just that it exists). + /// Use this when you need to detect corrupt objects. For simple existence checks, + /// prefer which is faster. + /// + public virtual bool ObjectCanBeParsed(string sha) { IntPtr objHandle; if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.ResultCode.Success) @@ -360,6 +411,12 @@ protected virtual void Dispose(bool disposing) { if (!this.disposedValue) { + if (this.odbHandle != IntPtr.Zero) + { + Native.Odb.Free(this.odbHandle); + this.odbHandle = IntPtr.Zero; + } + Native.Repo.Free(this.RepoHandle); Native.Shutdown(); this.disposedValue = true; @@ -504,6 +561,22 @@ public static class Repo public static extern void Free(IntPtr repoHandle); } + public static class Odb + { + [DllImport(Git2NativeLibName, EntryPoint = "git_repository_odb")] + public static extern ResultCode GetOdb(out IntPtr odbHandle, IntPtr repoHandle); + + /// 1 if the object exists, 0 otherwise + [DllImport(Git2NativeLibName, EntryPoint = "git_odb_exists")] + public static extern int Exists(IntPtr odbHandle, ref GitOid id); + + [DllImport(Git2NativeLibName, EntryPoint = "git_odb_free")] + public static extern void Free(IntPtr odbHandle); + + [DllImport(Git2NativeLibName, EntryPoint = "git_oid_fromstr")] + public static extern ResultCode OidFromStr(out GitOid oid, string str); + } + public static class Config { [DllImport(Git2NativeLibName, EntryPoint = "git_repository_config")] diff --git a/GVFS/GVFS.Common/Maintenance/LooseObjectsStep.cs b/GVFS/GVFS.Common/Maintenance/LooseObjectsStep.cs index f71f0d6cc..f45049ec6 100644 --- a/GVFS/GVFS.Common/Maintenance/LooseObjectsStep.cs +++ b/GVFS/GVFS.Common/Maintenance/LooseObjectsStep.cs @@ -172,7 +172,7 @@ public void ClearCorruptLooseObjects(EventMetadata metadata) // may be more bad objects in the next batch after deleting the corrupt objects. foreach (string objectId in this.GetBatchOfLooseObjects(2 * this.MaxLooseObjectsInPack)) { - if (!this.Context.Repository.ObjectExists(objectId)) + if (!this.Context.Repository.ObjectCanBeParsed(objectId)) { string objectFile = this.GetLooseObjectFileName(objectId); diff --git a/GVFS/GVFS.Common/Prefetch/BlobPrefetcher.cs b/GVFS/GVFS.Common/Prefetch/BlobPrefetcher.cs index 29bc4cc67..7010ebb7e 100644 --- a/GVFS/GVFS.Common/Prefetch/BlobPrefetcher.cs +++ b/GVFS/GVFS.Common/Prefetch/BlobPrefetcher.cs @@ -476,7 +476,10 @@ protected void DownloadMissingCommit(string commitSha, GitObjects gitObjects) { using (LibGit2Repo repo = new LibGit2Repo(this.Tracer, this.Enlistment.WorkingDirectoryBackingRoot)) { - if (!repo.ObjectExists(commitSha)) + // Use ObjectCanBeParsed (revparse) rather than ObjectExists (odb_exists) + // because commitSha may be a ref name or abbreviated SHA from CLI input + // (e.g. FastFetch --commit), not necessarily a full 40-char hex SHA. + if (!repo.ObjectCanBeParsed(commitSha)) { if (!gitObjects.TryDownloadCommit(commitSha)) {