Skip to content

Commit e88b403

Browse files
tyrielvCopilot
andcommitted
Add CancellationToken and async hydration to GitStatusCache
- Add CancellationToken parameter to CreateSummary (default: none) - Check cancellation between each phase of summary computation - Separate OperationCanceledException catch returns invalid summary - GitStatusCache creates CancellationTokenSource, cancels on Shutdown - Run UpdateHydrationSummary in parallel with TryRebuildStatusCache via Task.Run — independent operations should not delay each other Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4b3bc38 commit e88b403

2 files changed

Lines changed: 76 additions & 7 deletions

File tree

GVFS/GVFS.Common/GitStatusCache.cs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public class GitStatusCache : IDisposable
4747
private bool isStopping;
4848
private bool isInitialized;
4949
private StatusStatistics statistics;
50+
private CancellationTokenSource shutdownTokenSource;
51+
private Task activeHydrationTask;
5052

5153
private volatile CacheState cacheState = CacheState.Dirty;
5254

@@ -65,6 +67,7 @@ public GitStatusCache(GVFSContext context, TimeSpan backoffTime)
6567
this.backoffTime = backoffTime;
6668
this.serializedGitStatusFilePath = this.context.Enlistment.GitStatusCachePath;
6769
this.statistics = new StatusStatistics();
70+
this.shutdownTokenSource = new CancellationTokenSource();
6871

6972
this.wakeUpThread = new AutoResetEvent(false);
7073
}
@@ -79,6 +82,7 @@ public virtual void Initialize()
7982
public virtual void Shutdown()
8083
{
8184
this.isStopping = true;
85+
this.shutdownTokenSource.Cancel();
8286

8387
if (this.isInitialized && this.updateStatusCacheThread != null)
8488
{
@@ -177,6 +181,27 @@ public virtual void Dispose()
177181
{
178182
this.Shutdown();
179183

184+
// Wait for the hydration task to complete before disposing the
185+
// token source it may still be using.
186+
if (this.activeHydrationTask != null)
187+
{
188+
try
189+
{
190+
this.activeHydrationTask.Wait();
191+
}
192+
catch (AggregateException)
193+
{
194+
}
195+
196+
this.activeHydrationTask = null;
197+
}
198+
199+
if (this.shutdownTokenSource != null)
200+
{
201+
this.shutdownTokenSource.Dispose();
202+
this.shutdownTokenSource = null;
203+
}
204+
180205
if (this.wakeUpThread != null)
181206
{
182207
this.wakeUpThread.Dispose();
@@ -317,10 +342,29 @@ private void RebuildStatusCacheIfNeeded(bool ignoreBackoff)
317342
if (needToRebuild)
318343
{
319344
this.statistics.RecordBackgroundStatusScanRun();
320-
this.UpdateHydrationSummary();
345+
346+
// Run hydration summary in parallel with git status — they are independent
347+
// operations and neither should delay the other.
348+
Task hydrationTask = Task.Run(() => this.UpdateHydrationSummary());
349+
this.activeHydrationTask = hydrationTask;
321350

322351
bool rebuildStatusCacheSucceeded = this.TryRebuildStatusCache();
323352

353+
// Wait for hydration to complete before logging final stats.
354+
try
355+
{
356+
hydrationTask.Wait();
357+
}
358+
catch (AggregateException ex)
359+
{
360+
EventMetadata errorMetadata = new EventMetadata();
361+
errorMetadata.Add("Area", EtwArea);
362+
errorMetadata.Add("Exception", ex.InnerException?.ToString());
363+
this.context.Tracer.RelatedError(
364+
errorMetadata,
365+
$"{nameof(GitStatusCache)}.{nameof(RebuildStatusCacheIfNeeded)}: Unhandled exception in hydration summary task.");
366+
}
367+
324368
TimeSpan delayedTime = startTime - this.initialDelayTime;
325369
TimeSpan statusRunTime = DateTime.UtcNow - startTime;
326370

@@ -356,7 +400,7 @@ private void UpdateHydrationSummary()
356400
* and this is also a convenient place to log telemetry for it.
357401
*/
358402
EnlistmentHydrationSummary hydrationSummary =
359-
EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem);
403+
EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem, cancellationToken: this.shutdownTokenSource.Token);
360404
EventMetadata metadata = new EventMetadata();
361405
metadata.Add("Area", EtwArea);
362406
if (hydrationSummary.IsValid)
@@ -372,14 +416,20 @@ private void UpdateHydrationSummary()
372416
metadata,
373417
Keywords.Telemetry);
374418
}
375-
else
419+
else if (hydrationSummary.Error != null)
376420
{
377-
metadata["Exception"] = hydrationSummary.Error?.ToString();
421+
metadata["Exception"] = hydrationSummary.Error.ToString();
378422
this.context.Tracer.RelatedWarning(
379423
metadata,
380424
$"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: hydration summary could not be calculated.",
381425
Keywords.Telemetry);
382426
}
427+
else
428+
{
429+
// Invalid summary with no error — likely cancelled during shutdown
430+
this.context.Tracer.RelatedInfo(
431+
$"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: hydration summary was cancelled.");
432+
}
383433
}
384434
catch (Exception ex)
385435
{

GVFS/GVFS.Common/HealthCalculator/EnlistmentHydrationSummary.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
using GVFS.Common.FileSystem;
1+
using GVFS.Common.FileSystem;
22
using GVFS.Common.Git;
33
using System;
44
using System.IO;
5-
using System.Linq;
5+
using System.Linq;
6+
using System.Threading;
67

78
namespace GVFS.Common
89
{
@@ -40,7 +41,8 @@ public string ToMessage()
4041

4142
public static EnlistmentHydrationSummary CreateSummary(
4243
GVFSEnlistment enlistment,
43-
PhysicalFileSystem fileSystem)
44+
PhysicalFileSystem fileSystem,
45+
CancellationToken cancellationToken = default)
4446
{
4547
try
4648
{
@@ -51,14 +53,21 @@ public static EnlistmentHydrationSummary CreateSummary(
5153
/* Getting all the directories is also slow, but not as slow as reading the entire index,
5254
* GetTotalPathCount caches the count so this is only slow occasionally,
5355
* and the GitStatusCache manager also calls this to ensure it is updated frequently. */
56+
cancellationToken.ThrowIfCancellationRequested();
5457
int totalFolderCount = GetHeadTreeCount(enlistment, fileSystem);
5558

5659
EnlistmentPathData pathData = new EnlistmentPathData();
5760

5861
/* FUTURE: These could be optimized to only deal with counts instead of full path lists */
5962
pathData.LoadPlaceholdersFromDatabase(enlistment);
63+
64+
cancellationToken.ThrowIfCancellationRequested();
65+
6066
pathData.LoadModifiedPaths(enlistment);
6167

68+
69+
cancellationToken.ThrowIfCancellationRequested();
70+
6271
int hydratedFileCount = pathData.ModifiedFilePaths.Count + pathData.PlaceholderFilePaths.Count;
6372
int hydratedFolderCount = pathData.ModifiedFolderPaths.Count + pathData.PlaceholderFolderPaths.Count;
6473
return new EnlistmentHydrationSummary()
@@ -68,6 +77,16 @@ public static EnlistmentHydrationSummary CreateSummary(
6877
TotalFileCount = totalFileCount,
6978
TotalFolderCount = totalFolderCount,
7079
};
80+
}
81+
catch (OperationCanceledException)
82+
{
83+
return new EnlistmentHydrationSummary()
84+
{
85+
HydratedFileCount = -1,
86+
HydratedFolderCount = -1,
87+
TotalFileCount = -1,
88+
TotalFolderCount = -1,
89+
};
7190
}
7291
catch (Exception e)
7392
{

0 commit comments

Comments
 (0)