diff --git a/GVFS/GVFS.Common/Git/GitRepo.cs b/GVFS/GVFS.Common/Git/GitRepo.cs
index ee9d8b96d..cd11436d8 100644
--- a/GVFS/GVFS.Common/Git/GitRepo.cs
+++ b/GVFS/GVFS.Common/Git/GitRepo.cs
@@ -113,6 +113,21 @@ public virtual bool TryGetBlobLength(string blobSha, out long size)
return this.GetLooseBlobState(blobSha, null, out size) == LooseBlobState.Exists;
}
+ ///
+ /// Try to find the SHAs of subtrees missing from the given tree.
+ ///
+ /// Tree to look up
+ /// SHAs of subtrees of this tree which are not downloaded yet.
+ ///
+ public virtual bool TryGetMissingSubTrees(string treeSha, out string[] subtrees)
+ {
+ string[] missingSubtrees = null;
+ var succeeded = this.libgit2RepoInvoker.TryInvoke(repo =>
+ repo.GetMissingSubTrees(treeSha), out missingSubtrees);
+ subtrees = missingSubtrees;
+ return succeeded;
+ }
+
public void Dispose()
{
if (this.libgit2RepoInvoker != null)
diff --git a/GVFS/GVFS.Common/Git/LibGit2Repo.cs b/GVFS/GVFS.Common/Git/LibGit2Repo.cs
index 0849dd6b7..f9edcce64 100644
--- a/GVFS/GVFS.Common/Git/LibGit2Repo.cs
+++ b/GVFS/GVFS.Common/Git/LibGit2Repo.cs
@@ -1,5 +1,6 @@
using GVFS.Common.Tracing;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
@@ -147,6 +148,82 @@ public virtual bool TryCopyBlob(string sha, Action writeAction)
return true;
}
+ ///
+ /// Get the list of missing subtrees for the given treeSha.
+ ///
+ /// Tree to look up
+ /// SHAs of subtrees of this tree which are not downloaded yet.
+ public virtual string[] GetMissingSubTrees(string treeSha)
+ {
+ List missingSubtreesList = new List();
+ IntPtr treeHandle;
+ if (Native.RevParseSingle(out treeHandle, this.RepoHandle, treeSha) != Native.SuccessCode
+ || treeHandle == IntPtr.Zero)
+ {
+ return Array.Empty();
+ }
+
+ try
+ {
+ if (Native.Object.GetType(treeHandle) != Native.ObjectTypes.Tree)
+ {
+ return Array.Empty();
+ }
+
+ uint entryCount = Native.Tree.GetEntryCount(treeHandle);
+ for (uint i = 0; i < entryCount; i++)
+ {
+ if (this.IsMissingSubtree(treeHandle, i, out string entrySha))
+ {
+ missingSubtreesList.Add(entrySha);
+ }
+ }
+ }
+ finally
+ {
+ Native.Object.Free(treeHandle);
+ }
+
+ return missingSubtreesList.ToArray();
+ }
+
+ ///
+ /// Determine if the given index of a tree is a subtree and if it is missing.
+ /// If it is a missing subtree, return the SHA of the subtree.
+ ///
+ private bool IsMissingSubtree(IntPtr treeHandle, uint i, out string entrySha)
+ {
+ entrySha = null;
+ IntPtr entryHandle = Native.Tree.GetEntryByIndex(treeHandle, i);
+ if (entryHandle == IntPtr.Zero)
+ {
+ return false;
+ }
+
+ var entryMode = Native.Tree.GetEntryFileMode(entryHandle);
+ if (entryMode != Native.Tree.TreeEntryFileModeDirectory)
+ {
+ return false;
+ }
+
+ var entryId = Native.Tree.GetEntryId(entryHandle);
+ if (entryId == IntPtr.Zero)
+ {
+ return false;
+ }
+
+ var rawEntrySha = Native.IntPtrToGitOid(entryId);
+ entrySha = rawEntrySha.ToString();
+
+ if (this.ObjectExists(entrySha))
+ {
+ return false;
+ }
+ return true;
+ /* Both the entryHandle and the entryId handle are owned by the treeHandle, so we shouldn't free them or it will lead to corruption of the later entries */
+ }
+
+
public void Dispose()
{
this.Dispose(true);
@@ -247,6 +324,26 @@ public static class Blob
[DllImport(Git2NativeLibName, EntryPoint = "git_blob_rawcontent")]
public static unsafe extern byte* GetRawContent(IntPtr objectHandle);
}
+
+ public static class Tree
+ {
+ [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entrycount")]
+ public static extern uint GetEntryCount(IntPtr treeHandle);
+
+ [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_byindex")]
+ public static extern IntPtr GetEntryByIndex(IntPtr treeHandle, uint index);
+
+ [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_id")]
+ public static extern IntPtr GetEntryId(IntPtr entryHandle);
+
+ /* git_tree_entry_type requires the object to exist, so we can't use it to check if
+ * a missing entry is a tree. Instead, we can use the file mode to determine if it is a tree. */
+ [DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_filemode")]
+ public static extern uint GetEntryFileMode(IntPtr entryHandle);
+
+ public const uint TreeEntryFileModeDirectory = 0x4000;
+
+ }
}
}
}
\ No newline at end of file
diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs
index a9dd95b70..52426075b 100644
--- a/GVFS/GVFS.Mount/InProcessMount.cs
+++ b/GVFS/GVFS.Mount/InProcessMount.cs
@@ -27,6 +27,12 @@ public class InProcessMount
private const int MaxPipeNameLength = 250;
private const int MutexMaxWaitTimeMS = 500;
+ // This is value chosen based on tested scenarios to limit the required download time for
+ // all the trees. This is approximately the amount of trees that can be downloaded in 1 second.
+ // Downloading an entire commit pack also takes around 1 second, so this should limit downloading
+ // all the trees in a commit to ~2-3 seconds.
+ private const int MissingTreeThresholdForDownloadingCommitPack = 200;
+
private readonly bool showDebugWindow;
private FileSystemCallbacks fileSystemCallbacks;
@@ -47,7 +53,6 @@ public class InProcessMount
private ManualResetEvent unmountEvent;
private readonly Dictionary treesWithDownloadedCommits = new Dictionary();
- private DateTime lastCommitPackDownloadTime = DateTime.MinValue;
// True if InProcessMount is calling git reset as part of processing
// a folder dehydrate request
@@ -518,13 +523,14 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name
if (this.ShouldDownloadCommitPack(objectSha, out string commitSha)
&& this.gitObjects.TryDownloadCommit(commitSha))
{
- this.DownloadedCommitPack(objectSha: objectSha, commitSha: commitSha);
+ this.DownloadedCommitPack(commitSha);
response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult);
// FUTURE: Should the stats be updated to reflect all the trees in the pack?
// FUTURE: Should we try to clean up duplicate trees or increase depth of the commit download?
}
else if (this.gitObjects.TryDownloadAndSaveObject(objectSha, GVFSGitObjects.RequestSource.NamedPipeMessage) == GitObjects.DownloadAndSaveObjectResult.Success)
{
+ this.UpdateTreesForDownloadedCommits(objectSha);
response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult);
}
else
@@ -548,7 +554,7 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name
* Otherwise, the trees for the commit may be needed soon depending on the context.
* e.g. git log (without a pathspec) doesn't need trees, but git checkout does.
*
- * Save the tree/commit so if the tree is requested soon we can download all the trees for the commit in a batch.
+ * Save the tree/commit so if more trees are requested we can download all the trees for the commit in a batch.
*/
this.treesWithDownloadedCommits[treeSha] = objectSha;
}
@@ -561,28 +567,67 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name
private bool PrefetchHasBeenDone()
{
var prefetchPacks = this.gitObjects.ReadPackFileNames(this.enlistment.GitPackRoot, GVFSConstants.PrefetchPackPrefix);
- return prefetchPacks.Length > 0;
+ var result = prefetchPacks.Length > 0;
+ if (result)
+ {
+ this.treesWithDownloadedCommits.Clear();
+ }
+ return result;
}
private bool ShouldDownloadCommitPack(string objectSha, out string commitSha)
{
-
if (!this.treesWithDownloadedCommits.TryGetValue(objectSha, out commitSha)
|| this.PrefetchHasBeenDone())
{
return false;
}
- /* This is a heuristic to prevent downloading multiple packs related to git history commands,
- * since commits downloaded close together likely have similar trees. */
- var timePassed = DateTime.UtcNow - this.lastCommitPackDownloadTime;
- return (timePassed > TimeSpan.FromMinutes(5));
+ /* This is a heuristic to prevent downloading multiple packs related to git history commands.
+ * Closely related commits are likely to have similar trees, so we'll find fewer missing trees in them.
+ * Conversely, if we know (from previously downloaded missing trees) that a commit has a lot of missing
+ * trees left, we'll probably need to download many more trees for the commit so we should download the pack.
+ */
+ var commitShaLocal = commitSha; // can't use out parameter in lambda
+ int missingTreeCount = this.treesWithDownloadedCommits.Where(x => x.Value == commitShaLocal).Count();
+ return missingTreeCount > MissingTreeThresholdForDownloadingCommitPack;
+ }
+
+ private void UpdateTreesForDownloadedCommits(string objectSha)
+ {
+ /* If we are downloading missing trees, we probably are missing more trees for the commit.
+ * Update our list of trees associated with the commit so we can use the # of missing trees
+ * as a heuristic to decide whether to batch download all the trees for the commit the
+ * next time a missing one is requested.
+ */
+ if (!this.treesWithDownloadedCommits.TryGetValue(objectSha, out var commitSha)
+ || this.PrefetchHasBeenDone())
+ {
+ return;
+ }
+
+ if (!this.context.Repository.TryGetObjectType(objectSha, out var objectType)
+ || objectType != Native.ObjectTypes.Tree)
+ {
+ return;
+ }
+
+ if (this.context.Repository.TryGetMissingSubTrees(objectSha, out var missingSubTrees))
+ {
+ foreach (var missingSubTree in missingSubTrees)
+ {
+ this.treesWithDownloadedCommits[missingSubTree] = commitSha;
+ }
+ }
}
- private void DownloadedCommitPack(string objectSha, string commitSha)
+ private void DownloadedCommitPack(string commitSha)
{
- this.lastCommitPackDownloadTime = DateTime.UtcNow;
- this.treesWithDownloadedCommits.Remove(objectSha);
+ var toRemove = this.treesWithDownloadedCommits.Where(x => x.Value == commitSha).ToList();
+ foreach (var tree in toRemove)
+ {
+ this.treesWithDownloadedCommits.Remove(tree.Key);
+ }
}
private void HandlePostFetchJobRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection)