@@ -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 , this . context . Tracer ) ;
403+ EnlistmentHydrationSummary . CreateSummary ( this . context . Enlistment , this . context . FileSystem , this . context . Tracer , 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 {
0 commit comments