diff --git a/GVFS/GVFS.Common/GitStatusCache.cs b/GVFS/GVFS.Common/GitStatusCache.cs index 1a5ac02a0..30bbc7bce 100644 --- a/GVFS/GVFS.Common/GitStatusCache.cs +++ b/GVFS/GVFS.Common/GitStatusCache.cs @@ -47,6 +47,8 @@ public class GitStatusCache : IDisposable private bool isStopping; private bool isInitialized; private StatusStatistics statistics; + private CancellationTokenSource shutdownTokenSource; + private Task activeHydrationTask; private volatile CacheState cacheState = CacheState.Dirty; @@ -65,6 +67,7 @@ public GitStatusCache(GVFSContext context, TimeSpan backoffTime) this.backoffTime = backoffTime; this.serializedGitStatusFilePath = this.context.Enlistment.GitStatusCachePath; this.statistics = new StatusStatistics(); + this.shutdownTokenSource = new CancellationTokenSource(); this.wakeUpThread = new AutoResetEvent(false); } @@ -79,6 +82,7 @@ public virtual void Initialize() public virtual void Shutdown() { this.isStopping = true; + this.shutdownTokenSource.Cancel(); if (this.isInitialized && this.updateStatusCacheThread != null) { @@ -177,6 +181,27 @@ public virtual void Dispose() { this.Shutdown(); + // Wait for the hydration task to complete before disposing the + // token source it may still be using. + if (this.activeHydrationTask != null) + { + try + { + this.activeHydrationTask.Wait(); + } + catch (AggregateException) + { + } + + this.activeHydrationTask = null; + } + + if (this.shutdownTokenSource != null) + { + this.shutdownTokenSource.Dispose(); + this.shutdownTokenSource = null; + } + if (this.wakeUpThread != null) { this.wakeUpThread.Dispose(); @@ -317,10 +342,29 @@ private void RebuildStatusCacheIfNeeded(bool ignoreBackoff) if (needToRebuild) { this.statistics.RecordBackgroundStatusScanRun(); - this.UpdateHydrationSummary(); + + // Run hydration summary in parallel with git status — they are independent + // operations and neither should delay the other. + Task hydrationTask = Task.Run(() => this.UpdateHydrationSummary()); + this.activeHydrationTask = hydrationTask; bool rebuildStatusCacheSucceeded = this.TryRebuildStatusCache(); + // Wait for hydration to complete before logging final stats. + try + { + hydrationTask.Wait(); + } + catch (AggregateException ex) + { + EventMetadata errorMetadata = new EventMetadata(); + errorMetadata.Add("Area", EtwArea); + errorMetadata.Add("Exception", ex.InnerException?.ToString()); + this.context.Tracer.RelatedError( + errorMetadata, + $"{nameof(GitStatusCache)}.{nameof(RebuildStatusCacheIfNeeded)}: Unhandled exception in hydration summary task."); + } + TimeSpan delayedTime = startTime - this.initialDelayTime; TimeSpan statusRunTime = DateTime.UtcNow - startTime; @@ -356,7 +400,7 @@ private void UpdateHydrationSummary() * and this is also a convenient place to log telemetry for it. */ EnlistmentHydrationSummary hydrationSummary = - EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem); + EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem, cancellationToken: this.shutdownTokenSource.Token); EventMetadata metadata = new EventMetadata(); metadata.Add("Area", EtwArea); if (hydrationSummary.IsValid) @@ -372,14 +416,20 @@ private void UpdateHydrationSummary() metadata, Keywords.Telemetry); } - else + else if (hydrationSummary.Error != null) { - metadata["Exception"] = hydrationSummary.Error?.ToString(); + metadata["Exception"] = hydrationSummary.Error.ToString(); this.context.Tracer.RelatedWarning( metadata, $"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: hydration summary could not be calculated.", Keywords.Telemetry); } + else + { + // Invalid summary with no error — likely cancelled during shutdown + this.context.Tracer.RelatedInfo( + $"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: hydration summary was cancelled."); + } } catch (Exception ex) { diff --git a/GVFS/GVFS.Common/HealthCalculator/EnlistmentHydrationSummary.cs b/GVFS/GVFS.Common/HealthCalculator/EnlistmentHydrationSummary.cs index 600ba91c5..74802541e 100644 --- a/GVFS/GVFS.Common/HealthCalculator/EnlistmentHydrationSummary.cs +++ b/GVFS/GVFS.Common/HealthCalculator/EnlistmentHydrationSummary.cs @@ -1,8 +1,9 @@ -using GVFS.Common.FileSystem; +using GVFS.Common.FileSystem; using GVFS.Common.Git; using System; using System.IO; -using System.Linq; +using System.Linq; +using System.Threading; namespace GVFS.Common { @@ -40,7 +41,8 @@ public string ToMessage() public static EnlistmentHydrationSummary CreateSummary( GVFSEnlistment enlistment, - PhysicalFileSystem fileSystem) + PhysicalFileSystem fileSystem, + CancellationToken cancellationToken = default) { try { @@ -51,14 +53,21 @@ public static EnlistmentHydrationSummary CreateSummary( /* Getting all the directories is also slow, but not as slow as reading the entire index, * GetTotalPathCount caches the count so this is only slow occasionally, * and the GitStatusCache manager also calls this to ensure it is updated frequently. */ + cancellationToken.ThrowIfCancellationRequested(); int totalFolderCount = GetHeadTreeCount(enlistment, fileSystem); EnlistmentPathData pathData = new EnlistmentPathData(); /* FUTURE: These could be optimized to only deal with counts instead of full path lists */ pathData.LoadPlaceholdersFromDatabase(enlistment); + + cancellationToken.ThrowIfCancellationRequested(); + pathData.LoadModifiedPaths(enlistment); + + cancellationToken.ThrowIfCancellationRequested(); + int hydratedFileCount = pathData.ModifiedFilePaths.Count + pathData.PlaceholderFilePaths.Count; int hydratedFolderCount = pathData.ModifiedFolderPaths.Count + pathData.PlaceholderFolderPaths.Count; return new EnlistmentHydrationSummary() @@ -68,6 +77,16 @@ public static EnlistmentHydrationSummary CreateSummary( TotalFileCount = totalFileCount, TotalFolderCount = totalFolderCount, }; + } + catch (OperationCanceledException) + { + return new EnlistmentHydrationSummary() + { + HydratedFileCount = -1, + HydratedFolderCount = -1, + TotalFileCount = -1, + TotalFolderCount = -1, + }; } catch (Exception e) {