Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 110 additions & 22 deletions GVFS/GVFS.Common/GitStatusCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ public class GitStatusCache : IDisposable
private CancellationTokenSource shutdownTokenSource;
private Task activeHydrationTask;

private volatile EnlistmentHydrationSummary cachedHydrationSummary;
private volatile HydrationCacheSnapshot hydrationCache = new HydrationCacheSnapshot(null, NamedPipeMessages.HydrationStatus.CacheState.NotComputed, 0);
private int hydrationInvalidationVersion;
private long hydrationRequestCount;
private long hydrationUnavailableCount;
private readonly Lock hydrationStatisticsLock = new Lock();

private Func<int> projectedFolderCountProvider;

Expand Down Expand Up @@ -119,6 +123,7 @@ public virtual void Shutdown()
/// </summary>
public virtual void Invalidate()
{
Interlocked.Increment(ref this.hydrationInvalidationVersion);
this.lastInvalidationTime = DateTime.UtcNow;
this.cacheState = CacheState.Dirty;
}
Expand All @@ -142,9 +147,29 @@ public void RefreshAndWait()
/// Returns the cached hydration summary if one has been computed,
/// or null if no valid summary is available yet.
/// </summary>
public EnlistmentHydrationSummary GetCachedHydrationSummary()
public EnlistmentHydrationSummary GetCachedHydrationSummary(out NamedPipeMessages.HydrationStatus.CacheInfo info)
{
return this.cachedHydrationSummary;
HydrationCacheSnapshot snapshot = this.hydrationCache;
EnlistmentHydrationSummary summary = snapshot.Summary;
bool available = summary != null && summary.IsValid;
lock (this.hydrationStatisticsLock)
{
this.hydrationRequestCount++;
if (!available)
{
this.hydrationUnavailableCount++;
}
}

info = new NamedPipeMessages.HydrationStatus.CacheInfo
{
State = snapshot.State,
CalculationId = summary?.Diagnostics?.CalculationId,
AgeMs = summary == null ? -1 : Math.Max(0, (long)(DateTime.UtcNow - snapshot.CompletedUtc).TotalMilliseconds),
Invalidated = snapshot.InvalidationVersion != Volatile.Read(ref this.hydrationInvalidationVersion),
CountsComplete = summary?.Diagnostics?.CountsComplete ?? false,
};
return available ? summary : null;
}

/// <summary>
Expand Down Expand Up @@ -253,6 +278,22 @@ public virtual bool WriteTelemetryandReset(EventMetadata metadata)
}

StatusStatistics statusStatistics = Interlocked.Exchange(ref this.statistics, new StatusStatistics());
long hydrationRequests;
long hydrationUnavailable;
lock (this.hydrationStatisticsLock)
{
hydrationRequests = this.hydrationRequestCount;
hydrationUnavailable = this.hydrationUnavailableCount;
this.hydrationRequestCount = 0;
this.hydrationUnavailableCount = 0;
}
if (hydrationRequests > 0)
{
metadata["HydrationStatus.RequestCount"] = hydrationRequests;
metadata["HydrationStatus.UnavailableCount"] = hydrationUnavailable;
metadata["HydrationStatus.CurrentCacheState"] = this.hydrationCache.State;
wroteTelemetry = true;
}

if (statusStatistics.BackgroundStatusScanCount > 0)
{
Expand Down Expand Up @@ -413,26 +454,24 @@ private void RebuildStatusCacheIfNeeded(bool ignoreBackoff)
}
}

private void UpdateHydrationSummary()
internal void UpdateHydrationSummary()
{
if (this.projectedFolderCountProvider == null)
{
this.RecordHydrationSkip(NamedPipeMessages.HydrationStatus.CacheState.ProviderUnavailable);
return;
}

bool enabled = TEST_EnableHydrationSummaryOverride
?? this.context.Repository.LibGit2RepoInvoker.GetConfigBoolOrDefault(GVFSConstants.GitConfig.ShowHydrationStatus, GVFSConstants.GitConfig.ShowHydrationStatusDefault);
if (!enabled)
if (!this.GetHydrationSummaryEnabled())
{
this.RecordHydrationSkip(NamedPipeMessages.HydrationStatus.CacheState.FeatureDisabled);
return;
}

HydrationStatusCircuitBreaker circuitBreaker = new HydrationStatusCircuitBreaker(
this.context.Enlistment.DotGVFSRoot,
this.context.Tracer);

HydrationStatusCircuitBreaker circuitBreaker = this.CreateHydrationCircuitBreaker();
if (circuitBreaker.IsDisabled())
{
this.RecordHydrationSkip(NamedPipeMessages.HydrationStatus.CacheState.CircuitBreakerDisabled);
return;
}

Expand All @@ -443,14 +482,25 @@ private void UpdateHydrationSummary()
* Accessing it here ensures that the value is cached for when a user invokes "git status",
* and this is also a convenient place to log telemetry for it.
*/
int invalidationVersion = Volatile.Read(ref this.hydrationInvalidationVersion);
HydrationSummaryDiagnostics diagnostics = new HydrationSummaryDiagnostics
{
Caller = HydrationSummaryCaller.BackgroundCache,
FolderCountSource = HydrationFolderCountSource.Projection,
};
EnlistmentHydrationSummary hydrationSummary =
EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem, this.context.Tracer, this.projectedFolderCountProvider, this.shutdownTokenSource.Token);
this.CreateHydrationSummary(diagnostics);
NamedPipeMessages.HydrationStatus.CacheState state = hydrationSummary.IsValid
? diagnostics.CountsComplete ? NamedPipeMessages.HydrationStatus.CacheState.Available : NamedPipeMessages.HydrationStatus.CacheState.IncompleteData
: diagnostics.Outcome == HydrationSummaryOutcome.Cancelled ? NamedPipeMessages.HydrationStatus.CacheState.Cancelled
: diagnostics.Outcome == HydrationSummaryOutcome.Failed ? NamedPipeMessages.HydrationStatus.CacheState.Failed
: NamedPipeMessages.HydrationStatus.CacheState.InvalidCounts;
this.hydrationCache = new HydrationCacheSnapshot(hydrationSummary, state, invalidationVersion);
EventMetadata metadata = new EventMetadata();
metadata.Add("Area", EtwArea);
if (hydrationSummary.IsValid)
{
this.cachedHydrationSummary = hydrationSummary;

metadata["CalculationId"] = diagnostics.CalculationId.ToString("N");
metadata[nameof(hydrationSummary.TotalFolderCount)] = hydrationSummary.TotalFolderCount;
metadata[nameof(hydrationSummary.TotalFileCount)] = hydrationSummary.TotalFileCount;
metadata[nameof(hydrationSummary.HydratedFolderCount)] = hydrationSummary.HydratedFolderCount;
Expand All @@ -464,36 +514,74 @@ private void UpdateHydrationSummary()
}
else if (hydrationSummary.Error != null)
{
this.cachedHydrationSummary = null;
circuitBreaker.RecordFailure();
metadata["Exception"] = hydrationSummary.Error.ToString();
this.context.Tracer.RelatedWarning(
metadata,
$"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: hydration summary could not be calculated.",
Keywords.Telemetry);
Keywords.None);
}
else
{
// Invalid summary with no error — likely cancelled during shutdown
this.cachedHydrationSummary = null;
this.context.Tracer.RelatedInfo(
$"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: hydration summary was cancelled.");
$"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: hydration summary outcome: {diagnostics.Outcome}.");
}
}
catch (Exception ex)
{
this.cachedHydrationSummary = null;
this.hydrationCache = new HydrationCacheSnapshot(null, NamedPipeMessages.HydrationStatus.CacheState.Failed, Volatile.Read(ref this.hydrationInvalidationVersion));
circuitBreaker.RecordFailure();
EventMetadata metadata = new EventMetadata();
metadata.Add("Area", EtwArea);
metadata.Add("Exception", ex.ToString());
this.context.Tracer.RelatedError(
metadata.Add("ExceptionType", ex.GetType().FullName);
metadata.Add("ExceptionHResult", ex.HResult);
this.context.Tracer.RelatedWarning(new EventMetadata { ["Exception"] = ex.ToString() }, "Hydration cache update failed.", Keywords.None);
this.context.Tracer.RelatedWarning(
metadata,
$"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: Exception trying to update hydration summary cache.",
Keywords.Telemetry);
}
}

internal virtual bool GetHydrationSummaryEnabled()
{
return TEST_EnableHydrationSummaryOverride
?? this.context.Repository.LibGit2RepoInvoker.GetConfigBoolOrDefault(GVFSConstants.GitConfig.ShowHydrationStatus, GVFSConstants.GitConfig.ShowHydrationStatusDefault);
}

internal virtual HydrationStatusCircuitBreaker CreateHydrationCircuitBreaker()
{
return new HydrationStatusCircuitBreaker(this.context.Enlistment.DotGVFSRoot, this.context.Tracer);
}

internal virtual EnlistmentHydrationSummary CreateHydrationSummary(HydrationSummaryDiagnostics diagnostics)
{
return EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem, this.context.Tracer,
this.projectedFolderCountProvider, this.shutdownTokenSource.Token, diagnostics);
}

private void RecordHydrationSkip(NamedPipeMessages.HydrationStatus.CacheState state)
{
HydrationCacheSnapshot previous = this.hydrationCache;
this.hydrationCache = new HydrationCacheSnapshot(previous.Summary, state, previous.InvalidationVersion, previous.CompletedUtc);
}

private sealed class HydrationCacheSnapshot
{
public HydrationCacheSnapshot(EnlistmentHydrationSummary summary, NamedPipeMessages.HydrationStatus.CacheState state, int invalidationVersion, DateTime? completedUtc = null)
{
this.Summary = summary;
this.State = state;
this.InvalidationVersion = invalidationVersion;
this.CompletedUtc = completedUtc ?? DateTime.UtcNow;
}

public EnlistmentHydrationSummary Summary { get; }
public NamedPipeMessages.HydrationStatus.CacheState State { get; }
public int InvalidationVersion { get; }
public DateTime CompletedUtc { get; }
}

/// <summary>
/// Rebuild the status cache. This will run the background status to
/// generate status results, and update the serialized status cache
Expand Down
Loading
Loading