Skip to content

Commit 820341d

Browse files
authored
Merge pull request #1912 from tyrielv/tyrielv/hydration-status-1-itracer
Improve exception logging and telemetry for hydration status
2 parents 39d3282 + 1be9cdb commit 820341d

6 files changed

Lines changed: 145 additions & 51 deletions

File tree

GVFS/GVFS.Common/GVFSConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public static class LogFileTypes
9999

100100
public const string Clone = "clone";
101101
public const string Dehydrate = "dehydrate";
102+
public const string Health = "health";
102103
public const string MountVerb = MountPrefix + "_verb";
103104
public const string MountProcess = MountPrefix + "_process";
104105
public const string MountUpgrade = MountPrefix + "_repoupgrade";

GVFS/GVFS.Common/GitStatusCache.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ private void UpdateHydrationSummary()
409409
* and this is also a convenient place to log telemetry for it.
410410
*/
411411
EnlistmentHydrationSummary hydrationSummary =
412-
EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem, cancellationToken: this.shutdownTokenSource.Token);
412+
EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem, this.context.Tracer, cancellationToken: this.shutdownTokenSource.Token);
413413
EventMetadata metadata = new EventMetadata();
414414
metadata.Add("Area", EtwArea);
415415
if (hydrationSummary.IsValid)

GVFS/GVFS.Common/HealthCalculator/EnlistmentHydrationSummary.cs

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using GVFS.Common.FileSystem;
22
using GVFS.Common.Git;
3+
using GVFS.Common.Tracing;
34
using System;
5+
using System.Diagnostics;
46
using System.IO;
57
using System.Linq;
68
using System.Threading;
@@ -41,35 +43,72 @@ public string ToMessage()
4143

4244
public static EnlistmentHydrationSummary CreateSummary(
4345
GVFSEnlistment enlistment,
44-
PhysicalFileSystem fileSystem,
46+
PhysicalFileSystem fileSystem,
47+
ITracer tracer,
4548
CancellationToken cancellationToken = default)
4649
{
50+
Stopwatch totalStopwatch = Stopwatch.StartNew();
51+
Stopwatch phaseStopwatch = new Stopwatch();
52+
4753
try
4854
{
4955
/* Getting all the file paths from git index is slow and we only need the total count,
5056
* so we read the index file header instead of calling GetPathsFromGitIndex */
57+
phaseStopwatch.Restart();
5158
int totalFileCount = GetIndexFileCount(enlistment, fileSystem);
59+
long indexReadMs = phaseStopwatch.ElapsedMilliseconds;
5260

53-
/* Getting all the directories is also slow, but not as slow as reading the entire index,
54-
* GetTotalPathCount caches the count so this is only slow occasionally,
55-
* and the GitStatusCache manager also calls this to ensure it is updated frequently. */
56-
cancellationToken.ThrowIfCancellationRequested();
57-
int totalFolderCount = GetHeadTreeCount(enlistment, fileSystem);
61+
cancellationToken.ThrowIfCancellationRequested();
5862

5963
EnlistmentPathData pathData = new EnlistmentPathData();
6064

6165
/* FUTURE: These could be optimized to only deal with counts instead of full path lists */
66+
phaseStopwatch.Restart();
6267
pathData.LoadPlaceholdersFromDatabase(enlistment);
63-
64-
cancellationToken.ThrowIfCancellationRequested();
65-
66-
pathData.LoadModifiedPaths(enlistment);
68+
long placeholderLoadMs = phaseStopwatch.ElapsedMilliseconds;
69+
70+
cancellationToken.ThrowIfCancellationRequested();
71+
72+
phaseStopwatch.Restart();
73+
pathData.LoadModifiedPaths(enlistment, tracer);
74+
long modifiedPathsLoadMs = phaseStopwatch.ElapsedMilliseconds;
6775

68-
6976
cancellationToken.ThrowIfCancellationRequested();
7077

7178
int hydratedFileCount = pathData.ModifiedFilePaths.Count + pathData.PlaceholderFilePaths.Count;
7279
int hydratedFolderCount = pathData.ModifiedFolderPaths.Count + pathData.PlaceholderFolderPaths.Count;
80+
81+
/* Getting the head tree count (used for TotalFolderCount) is potentially slower than the other parts
82+
* of the operation, so we do it last and check that the other parts would succeed before running it.
83+
*/
84+
var soFar = new EnlistmentHydrationSummary()
85+
{
86+
HydratedFileCount = hydratedFileCount,
87+
HydratedFolderCount = hydratedFolderCount,
88+
TotalFileCount = totalFileCount,
89+
TotalFolderCount = hydratedFolderCount + 1, // Not calculated yet, use a dummy valid value.
90+
};
91+
92+
if (!soFar.IsValid)
93+
{
94+
soFar.TotalFolderCount = 0; // Set to default invalid value to avoid confusion with the dummy value above.
95+
tracer.RelatedWarning(
96+
$"Hydration summary early exit: data invalid before tree count. " +
97+
$"TotalFileCount={totalFileCount}, HydratedFileCount={hydratedFileCount}, " +
98+
$"HydratedFolderCount={hydratedFolderCount}");
99+
EmitDurationTelemetry(tracer, totalStopwatch.ElapsedMilliseconds, indexReadMs, placeholderLoadMs, modifiedPathsLoadMs, treeCountMs: 0, earlyExit: true);
100+
return soFar;
101+
}
102+
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. */
106+
phaseStopwatch.Restart();
107+
int totalFolderCount = GetHeadTreeCount(enlistment, fileSystem, tracer);
108+
long treeCountMs = phaseStopwatch.ElapsedMilliseconds;
109+
110+
EmitDurationTelemetry(tracer, totalStopwatch.ElapsedMilliseconds, indexReadMs, placeholderLoadMs, modifiedPathsLoadMs, treeCountMs, earlyExit: false);
111+
73112
return new EnlistmentHydrationSummary()
74113
{
75114
HydratedFileCount = hydratedFileCount,
@@ -90,6 +129,7 @@ public static EnlistmentHydrationSummary CreateSummary(
90129
}
91130
catch (Exception e)
92131
{
132+
tracer.RelatedError($"Hydration summary failed with exception after {totalStopwatch.ElapsedMilliseconds}ms: {e.Message}");
93133
return new EnlistmentHydrationSummary()
94134
{
95135
HydratedFileCount = -1,
@@ -101,6 +141,29 @@ public static EnlistmentHydrationSummary CreateSummary(
101141
}
102142
}
103143

144+
private static void EmitDurationTelemetry(
145+
ITracer tracer,
146+
long totalMs,
147+
long indexReadMs,
148+
long placeholderLoadMs,
149+
long modifiedPathsLoadMs,
150+
long treeCountMs,
151+
bool earlyExit)
152+
{
153+
EventMetadata metadata = new EventMetadata();
154+
metadata["TotalMs"] = totalMs;
155+
metadata["IndexReadMs"] = indexReadMs;
156+
metadata["PlaceholderLoadMs"] = placeholderLoadMs;
157+
metadata["ModifiedPathsLoadMs"] = modifiedPathsLoadMs;
158+
metadata["TreeCountMs"] = treeCountMs;
159+
metadata["EarlyExit"] = earlyExit;
160+
tracer.RelatedEvent(
161+
EventLevel.Informational,
162+
"HydrationSummaryDuration",
163+
metadata,
164+
Keywords.Telemetry);
165+
}
166+
104167
/// <summary>
105168
/// Get the total number of files in the index.
106169
/// </summary>
@@ -142,12 +205,13 @@ internal static int GetIndexFileCount(GVFSEnlistment enlistment, PhysicalFileSys
142205
/// The number of subtrees at HEAD, which may be 0.
143206
/// Will return 0 if unsuccessful.
144207
/// </returns>
145-
internal static int GetHeadTreeCount(GVFSEnlistment enlistment, PhysicalFileSystem fileSystem)
208+
internal static int GetHeadTreeCount(GVFSEnlistment enlistment, PhysicalFileSystem fileSystem, ITracer tracer)
146209
{
147210
var gitProcess = enlistment.CreateGitProcess();
148211
var headResult = gitProcess.GetHeadTreeId();
149212
if (headResult.ExitCodeIsFailure)
150213
{
214+
tracer.RelatedError($"Failed to get HEAD tree ID: \nOutput: {headResult.Output}\n\nError:{headResult.Errors}");
151215
return 0;
152216
}
153217
var headSha = headResult.Output.Trim();
@@ -168,8 +232,9 @@ internal static int GetHeadTreeCount(GVFSEnlistment enlistment, PhysicalFileSyst
168232
return cachedCount;
169233
}
170234
}
171-
catch
235+
catch (Exception ex)
172236
{
237+
tracer.RelatedWarning($"Failed to read tree count cache file at {cacheFile}: {ex}");
173238
// Ignore errors reading the cache
174239
}
175240
}
@@ -180,14 +245,22 @@ internal static int GetHeadTreeCount(GVFSEnlistment enlistment, PhysicalFileSyst
180245
line => totalPathCount++,
181246
recursive: true,
182247
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+
183255
try
184256
{
185257
fileSystem.CreateDirectory(Path.GetDirectoryName(cacheFile));
186258
fileSystem.WriteAllText(cacheFile, $"{headSha}\n{totalPathCount}");
187259
}
188-
catch
260+
catch (Exception ex)
189261
{
190262
// Ignore errors writing the cache
263+
tracer.RelatedWarning($"Failed to write tree count cache file at {cacheFile}: {ex}");
191264
}
192265

193266
return totalPathCount;

GVFS/GVFS.Common/HealthCalculator/EnlistmentPathData.cs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using GVFS.Common.FileSystem;
33
using GVFS.Common.Git;
44
using GVFS.Common.NamedPipes;
5+
using GVFS.Common.Tracing;
56
using System;
67
using System.Collections.Generic;
78
using System.IO;
@@ -94,30 +95,39 @@ public void LoadPathsFromGitIndex(GVFSEnlistment enlistment)
9495
this.GitTrackingPaths.AddRange(skipWorktreeFiles);
9596
}
9697

97-
public void LoadModifiedPaths(GVFSEnlistment enlistment)
98+
public void LoadModifiedPaths(GVFSEnlistment enlistment, ITracer tracer)
9899
{
99-
if (TryLoadModifiedPathsFromPipe(enlistment))
100+
if (TryLoadModifiedPathsFromPipe(enlistment, tracer))
100101
{
101102
return;
102103
}
104+
105+
// Most likely GVFS is not mounted. Give a basic effort to read the modified paths database.
106+
string filePath = Path.Combine(enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.ModifiedPaths);
103107
try
104108
{
105-
/* Most likely GVFS is not mounted. Give a basic effort to read the modified paths database */
106-
var filePath = Path.Combine(enlistment.DotGVFSRoot, GVFSConstants.DotGVFS.Databases.ModifiedPaths);
107-
using (var file = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read))
108-
using (var reader = new StreamReader(file))
109+
using (FileStream file = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read))
110+
using (StreamReader reader = new StreamReader(file))
109111
{
110112
AddModifiedPaths(ReadModifiedPathDatabaseLines(reader));
111113
}
112114
}
113-
catch { }
115+
catch (Exception ex)
116+
{
117+
tracer.RelatedWarning($"Failed to read modified paths file at {filePath}: {ex.Message}");
118+
}
114119
}
115120

116121
private IEnumerable<string> ReadModifiedPathDatabaseLines(StreamReader r)
117122
{
118123
while (!r.EndOfStream)
119124
{
120-
var line = r.ReadLine();
125+
string line = r.ReadLine();
126+
if (line == null)
127+
{
128+
continue;
129+
}
130+
121131
const string LinePrefix = "A ";
122132
if (line.StartsWith(LinePrefix))
123133
{
@@ -133,7 +143,7 @@ private IEnumerable<string> ReadModifiedPathDatabaseLines(StreamReader r)
133143
/// <remarks>If/when modified paths are moved to SQLite go there instead</remarks>
134144
/// <param name="enlistment">The enlistment being operated on</param>
135145
/// <returns>An array containing all of the modified paths in string format</returns>
136-
private bool TryLoadModifiedPathsFromPipe(GVFSEnlistment enlistment)
146+
private bool TryLoadModifiedPathsFromPipe(GVFSEnlistment enlistment, ITracer tracer)
137147
{
138148
using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName))
139149
{
@@ -157,8 +167,9 @@ private bool TryLoadModifiedPathsFromPipe(GVFSEnlistment enlistment)
157167

158168
modifiedPathsList = modifiedPathsResponse.Body.Split(new char[] { '\0' }, StringSplitOptions.RemoveEmptyEntries);
159169
}
160-
catch (BrokenPipeException e)
170+
catch (Exception ex)
161171
{
172+
tracer.RelatedWarning($"Failed to load modified paths via named pipe: {ex.Message}");
162173
return false;
163174
}
164175

GVFS/GVFS.UnitTests/Common/EnlistmentHydrationSummaryTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public void HeadTreeCountCacheTests((string CachePrecontents, string ExpectedCac
112112
args.CachePrecontents != null,
113113
this.fileSystem.FileExists(totalPathCountPath));
114114

115-
int result = EnlistmentHydrationSummary.GetHeadTreeCount(this.context.Enlistment, this.context.FileSystem);
115+
int result = EnlistmentHydrationSummary.GetHeadTreeCount(this.context.Enlistment, this.context.FileSystem, this.context.Tracer);
116116

117117
this.fileSystem.FileExists(totalPathCountPath).ShouldBeTrue();
118118
var postContents = this.fileSystem.ReadAllText(totalPathCountPath);

GVFS/GVFS/CommandLine/HealthVerb.cs

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using CommandLine;
22
using GVFS.Common;
33
using GVFS.Common.FileSystem;
4+
using GVFS.Common.Tracing;
45
using System;
56
using System.Collections.Generic;
67
using System.Linq;
@@ -39,47 +40,55 @@ public class HealthVerb : GVFSVerb.ForExistingEnlistment
3940

4041
protected override void Execute(GVFSEnlistment enlistment)
4142
{
42-
if (this.StatusOnly)
43+
using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, HealthVerbName))
4344
{
44-
this.OutputHydrationPercent(enlistment);
45-
return;
46-
}
45+
tracer.AddLogFileEventListener(
46+
GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.Health),
47+
EventLevel.Informational,
48+
Keywords.Any);
4749

48-
// Now default to the current working directory when running the verb without a specified path
49-
if (string.IsNullOrEmpty(this.Directory) || this.Directory.Equals("."))
50-
{
51-
if (Environment.CurrentDirectory.StartsWith(enlistment.WorkingDirectoryRoot, GVFSPlatform.Instance.Constants.PathComparison))
50+
if (this.StatusOnly)
5251
{
53-
this.Directory = Environment.CurrentDirectory.Substring(enlistment.WorkingDirectoryRoot.Length);
52+
this.OutputHydrationPercent(enlistment, tracer);
53+
return;
5454
}
55-
else
55+
56+
// Now default to the current working directory when running the verb without a specified path
57+
if (string.IsNullOrEmpty(this.Directory) || this.Directory.Equals("."))
5658
{
57-
// If the path is not under the source root, set the directory to empty
58-
this.Directory = string.Empty;
59+
if (Environment.CurrentDirectory.StartsWith(enlistment.WorkingDirectoryRoot, GVFSPlatform.Instance.Constants.PathComparison))
60+
{
61+
this.Directory = Environment.CurrentDirectory.Substring(enlistment.WorkingDirectoryRoot.Length);
62+
}
63+
else
64+
{
65+
// If the path is not under the source root, set the directory to empty
66+
this.Directory = string.Empty;
67+
}
5968
}
60-
}
6169

62-
this.Output.WriteLine("\nGathering repository data...");
70+
this.Output.WriteLine("\nGathering repository data...");
6371

64-
this.Directory = this.Directory.Replace(GVFSPlatform.GVFSPlatformConstants.PathSeparator, GVFSConstants.GitPathSeparator);
72+
this.Directory = this.Directory.Replace(GVFSPlatform.GVFSPlatformConstants.PathSeparator, GVFSConstants.GitPathSeparator);
6573

66-
EnlistmentPathData pathData = new EnlistmentPathData();
74+
EnlistmentPathData pathData = new EnlistmentPathData();
6775

68-
pathData.LoadPlaceholdersFromDatabase(enlistment);
69-
pathData.LoadModifiedPaths(enlistment);
70-
pathData.LoadPathsFromGitIndex(enlistment);
76+
pathData.LoadPlaceholdersFromDatabase(enlistment);
77+
pathData.LoadModifiedPaths(enlistment, tracer);
78+
pathData.LoadPathsFromGitIndex(enlistment);
7179

72-
pathData.NormalizeAllPaths();
80+
pathData.NormalizeAllPaths();
7381

74-
EnlistmentHealthCalculator enlistmentHealthCalculator = new EnlistmentHealthCalculator(pathData);
75-
EnlistmentHealthData enlistmentHealthData = enlistmentHealthCalculator.CalculateStatistics(this.Directory);
82+
EnlistmentHealthCalculator enlistmentHealthCalculator = new EnlistmentHealthCalculator(pathData);
83+
EnlistmentHealthData enlistmentHealthData = enlistmentHealthCalculator.CalculateStatistics(this.Directory);
7684

77-
this.PrintOutput(enlistmentHealthData);
85+
this.PrintOutput(enlistmentHealthData);
86+
}
7887
}
7988

80-
private void OutputHydrationPercent(GVFSEnlistment enlistment)
89+
private void OutputHydrationPercent(GVFSEnlistment enlistment, ITracer tracer)
8190
{
82-
var summary = EnlistmentHydrationSummary.CreateSummary(enlistment, this.FileSystem);
91+
EnlistmentHydrationSummary summary = EnlistmentHydrationSummary.CreateSummary(enlistment, this.FileSystem, tracer);
8392
this.Output.WriteLine(summary.ToMessage());
8493
}
8594

0 commit comments

Comments
 (0)