Skip to content

Commit 1ef345c

Browse files
authored
Merge pull request #1917 from tyrielv/tyrielv/hydration-status-4-projection
Replace git ls-tree with projection-based folder count
2 parents 0ae14cd + daf6d46 commit 1ef345c

11 files changed

Lines changed: 427 additions & 159 deletions

File tree

GVFS/GVFS.Common/GVFSConstants.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ public static class GitStatusCache
134134
{
135135
public const string Name = "gitStatusCache";
136136
public static readonly string CachePath = Path.Combine(Name, "GitStatusCache.dat");
137-
public static readonly string TreeCount = Path.Combine(Name, "TreeCountCache.dat");
138137
}
139138

140139
public static class HydrationStatus

GVFS/GVFS.Common/GVFSEnlistment.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ private GVFSEnlistment(string enlistmentRoot, string gitBinPath, GitAuthenticati
5454

5555
public string GVFSLogsRoot { get; }
5656

57+
/// <summary>
58+
/// Path to the git index file.
59+
/// </summary>
60+
public string GitIndexPath => Path.Combine(this.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Index);
61+
5762
public string LocalCacheRoot { get; private set; }
5863

5964
public string BlobSizesRoot { get; private set; }

GVFS/GVFS.Common/Git/GitProcess.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -682,11 +682,6 @@ public Result MultiPackIndexRepack(string gitObjectDirectory, string batchSize)
682682
return this.InvokeGitAgainstDotGitFolder($"-c pack.threads=1 -c repack.packKeptObjects=true multi-pack-index repack --object-dir=\"{gitObjectDirectory}\" --batch-size={batchSize} --no-progress");
683683
}
684684

685-
public Result GetHeadTreeId()
686-
{
687-
return this.InvokeGitAgainstDotGitFolder("rev-parse \"HEAD^{tree}\"", usePreCommandHook: false);
688-
}
689-
690685
public Process GetGitProcess(string command, string workingDirectory, string dotGitDirectory, bool useReadObjectHook, bool redirectStandardError, string gitObjectsDirectory, bool usePreCommandHook)
691686
{
692687
ProcessStartInfo processInfo = new ProcessStartInfo(this.gitBinPath);

GVFS/GVFS.Common/GitStatusCache.cs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public class GitStatusCache : IDisposable
5050
private CancellationTokenSource shutdownTokenSource;
5151
private Task activeHydrationTask;
5252

53+
private Func<int> projectedFolderCountProvider;
54+
5355
private volatile CacheState cacheState = CacheState.Dirty;
5456

5557
private object cacheFileLock = new object();
@@ -72,6 +74,23 @@ public GitStatusCache(GVFSContext context, TimeSpan backoffTime)
7274
this.wakeUpThread = new AutoResetEvent(false);
7375
}
7476

77+
/// <summary>
78+
/// Sets the provider used to get the total projected folder count for hydration
79+
/// summary computation. Must be called before <see cref="Initialize"/> for
80+
/// hydration summary to function.
81+
/// </summary>
82+
/// <remarks>
83+
/// This is set post-construction because of a circular dependency:
84+
/// InProcessMount creates GitStatusCache before FileSystemCallbacks,
85+
/// but the provider requires GitIndexProjection, which is created
86+
/// inside FileSystemCallbacks. FileSystemCallbacks calls this method
87+
/// after GitIndexProjection is available.
88+
/// </remarks>
89+
public void SetProjectedFolderCountProvider(Func<int> provider)
90+
{
91+
this.projectedFolderCountProvider = provider;
92+
}
93+
7594
public virtual void Initialize()
7695
{
7796
this.isInitialized = true;
@@ -183,17 +202,16 @@ public virtual void Dispose()
183202

184203
// Wait for the hydration task to complete before disposing the
185204
// token source it may still be using.
186-
if (this.activeHydrationTask != null)
205+
Task hydrationTask = Interlocked.Exchange(ref this.activeHydrationTask, null);
206+
if (hydrationTask != null)
187207
{
188208
try
189209
{
190-
this.activeHydrationTask.Wait();
210+
hydrationTask.Wait(TimeSpan.FromSeconds(5));
191211
}
192212
catch (AggregateException)
193213
{
194214
}
195-
196-
this.activeHydrationTask = null;
197215
}
198216

199217
if (this.shutdownTokenSource != null)
@@ -346,11 +364,12 @@ private void RebuildStatusCacheIfNeeded(bool ignoreBackoff)
346364
// Run hydration summary in parallel with git status — they are independent
347365
// operations and neither should delay the other.
348366
Task hydrationTask = Task.Run(() => this.UpdateHydrationSummary());
349-
this.activeHydrationTask = hydrationTask;
367+
Interlocked.Exchange(ref this.activeHydrationTask, hydrationTask);
350368

351369
bool rebuildStatusCacheSucceeded = this.TryRebuildStatusCache();
352370

353371
// Wait for hydration to complete before logging final stats.
372+
// Exceptions are observed here to avoid unobserved task exceptions.
354373
try
355374
{
356375
hydrationTask.Wait();
@@ -385,6 +404,11 @@ private void RebuildStatusCacheIfNeeded(bool ignoreBackoff)
385404

386405
private void UpdateHydrationSummary()
387406
{
407+
if (this.projectedFolderCountProvider == null)
408+
{
409+
return;
410+
}
411+
388412
bool enabled = TEST_EnableHydrationSummaryOverride
389413
?? this.context.Repository.LibGit2RepoInvoker.GetConfigBoolOrDefault(GVFSConstants.GitConfig.ShowHydrationStatus, GVFSConstants.GitConfig.ShowHydrationStatusDefault);
390414
if (!enabled)
@@ -409,7 +433,7 @@ private void UpdateHydrationSummary()
409433
* and this is also a convenient place to log telemetry for it.
410434
*/
411435
EnlistmentHydrationSummary hydrationSummary =
412-
EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem, this.context.Tracer, cancellationToken: this.shutdownTokenSource.Token);
436+
EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem, this.context.Tracer, this.projectedFolderCountProvider, this.shutdownTokenSource.Token);
413437
EventMetadata metadata = new EventMetadata();
414438
metadata.Add("Area", EtwArea);
415439
if (hydrationSummary.IsValid)

GVFS/GVFS.Common/HealthCalculator/EnlistmentHydrationSummary.cs

Lines changed: 8 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
using GVFS.Common.FileSystem;
2-
using GVFS.Common.Git;
32
using GVFS.Common.Tracing;
43
using System;
54
using System.Diagnostics;
65
using System.IO;
7-
using System.Linq;
86
using System.Threading;
97

108
namespace GVFS.Common
@@ -45,6 +43,7 @@ public static EnlistmentHydrationSummary CreateSummary(
4543
GVFSEnlistment enlistment,
4644
PhysicalFileSystem fileSystem,
4745
ITracer tracer,
46+
Func<int> projectedFolderCountProvider,
4847
CancellationToken cancellationToken = default)
4948
{
5049
Stopwatch totalStopwatch = Stopwatch.StartNew();
@@ -57,7 +56,6 @@ public static EnlistmentHydrationSummary CreateSummary(
5756
phaseStopwatch.Restart();
5857
int totalFileCount = GetIndexFileCount(enlistment, fileSystem);
5958
long indexReadMs = phaseStopwatch.ElapsedMilliseconds;
60-
6159
cancellationToken.ThrowIfCancellationRequested();
6260

6361
EnlistmentPathData pathData = new EnlistmentPathData();
@@ -66,15 +64,13 @@ public static EnlistmentHydrationSummary CreateSummary(
6664
phaseStopwatch.Restart();
6765
pathData.LoadPlaceholdersFromDatabase(enlistment);
6866
long placeholderLoadMs = phaseStopwatch.ElapsedMilliseconds;
69-
7067
cancellationToken.ThrowIfCancellationRequested();
7168

7269
phaseStopwatch.Restart();
7370
pathData.LoadModifiedPaths(enlistment, tracer);
7471
long modifiedPathsLoadMs = phaseStopwatch.ElapsedMilliseconds;
72+
cancellationToken.ThrowIfCancellationRequested();
7573

76-
cancellationToken.ThrowIfCancellationRequested();
77-
7874
int hydratedFileCount = pathData.ModifiedFilePaths.Count + pathData.PlaceholderFilePaths.Count;
7975
int hydratedFolderCount = pathData.ModifiedFolderPaths.Count + pathData.PlaceholderFolderPaths.Count;
8076

@@ -100,11 +96,12 @@ public static EnlistmentHydrationSummary CreateSummary(
10096
return soFar;
10197
}
10298

103-
/* Getting all the directories is also slow, but not as slow as reading the entire index,
104-
* GetTotalPathCount caches the count so this is only slow occasionally,
105-
* and the GitStatusCache manager also calls this to ensure it is updated frequently. */
99+
/* Get the total folder count from the caller-provided function.
100+
* In the mount process, this comes from the in-memory projection (essentially free).
101+
* In gvfs health --status fallback, this parses the git index via GitIndexProjection. */
102+
cancellationToken.ThrowIfCancellationRequested();
106103
phaseStopwatch.Restart();
107-
int totalFolderCount = GetHeadTreeCount(enlistment, fileSystem, tracer);
104+
int totalFolderCount = projectedFolderCountProvider();
108105
long treeCountMs = phaseStopwatch.ElapsedMilliseconds;
109106

110107
EmitDurationTelemetry(tracer, totalStopwatch.ElapsedMilliseconds, indexReadMs, placeholderLoadMs, modifiedPathsLoadMs, treeCountMs, earlyExit: false);
@@ -169,7 +166,7 @@ private static void EmitDurationTelemetry(
169166
/// </summary>
170167
internal static int GetIndexFileCount(GVFSEnlistment enlistment, PhysicalFileSystem fileSystem)
171168
{
172-
string indexPath = Path.Combine(enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Index);
169+
string indexPath = enlistment.GitIndexPath;
173170
using (var indexFile = fileSystem.OpenFileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, callFlushFileBuffers: false))
174171
{
175172
if (indexFile.Length < 12)
@@ -193,77 +190,5 @@ internal static int GetIndexFileCount(GVFSEnlistment enlistment, PhysicalFileSys
193190
}
194191
}
195192

196-
/// <summary>
197-
/// Get the total number of trees in the repo at HEAD.
198-
/// </summary>
199-
/// <remarks>
200-
/// This is used as the denominator in displaying percentage of hydrated
201-
/// directories as part of git status pre-command hook.
202-
/// It can take several seconds to calculate, so we cache it near the git status cache.
203-
/// </remarks>
204-
/// <returns>
205-
/// The number of subtrees at HEAD, which may be 0.
206-
/// Will return 0 if unsuccessful.
207-
/// </returns>
208-
internal static int GetHeadTreeCount(GVFSEnlistment enlistment, PhysicalFileSystem fileSystem, ITracer tracer)
209-
{
210-
var gitProcess = enlistment.CreateGitProcess();
211-
var headResult = gitProcess.GetHeadTreeId();
212-
if (headResult.ExitCodeIsFailure)
213-
{
214-
tracer.RelatedError($"Failed to get HEAD tree ID: \nOutput: {headResult.Output}\n\nError:{headResult.Errors}");
215-
return 0;
216-
}
217-
var headSha = headResult.Output.Trim();
218-
var cacheFile = Path.Combine(
219-
enlistment.DotGVFSRoot,
220-
GVFSConstants.DotGVFS.GitStatusCache.TreeCount);
221-
222-
// Load from cache if cache matches current HEAD.
223-
if (fileSystem.FileExists(cacheFile))
224-
{
225-
try
226-
{
227-
var lines = fileSystem.ReadLines(cacheFile).ToArray();
228-
if (lines.Length == 2
229-
&& lines[0] == headSha
230-
&& int.TryParse(lines[1], out int cachedCount))
231-
{
232-
return cachedCount;
233-
}
234-
}
235-
catch (Exception ex)
236-
{
237-
tracer.RelatedWarning($"Failed to read tree count cache file at {cacheFile}: {ex}");
238-
// Ignore errors reading the cache
239-
}
240-
}
241-
242-
int totalPathCount = 0;
243-
GitProcess.Result folderResult = gitProcess.LsTree(
244-
GVFSConstants.DotGit.HeadName,
245-
line => totalPathCount++,
246-
recursive: true,
247-
showDirectories: true);
248-
249-
if (GitProcess.Result.SuccessCode != folderResult.ExitCode)
250-
{
251-
tracer.RelatedError($"Failed to get tree count from HEAD: \nOutput: {folderResult.Output}\n\nError:{folderResult.Errors}");
252-
return 0;
253-
}
254-
255-
try
256-
{
257-
fileSystem.CreateDirectory(Path.GetDirectoryName(cacheFile));
258-
fileSystem.WriteAllText(cacheFile, $"{headSha}\n{totalPathCount}");
259-
}
260-
catch (Exception ex)
261-
{
262-
// Ignore errors writing the cache
263-
tracer.RelatedWarning($"Failed to write tree count cache file at {cacheFile}: {ex}");
264-
}
265-
266-
return totalPathCount;
267-
}
268193
}
269194
}

0 commit comments

Comments
 (0)