Skip to content

Commit 66959f0

Browse files
authored
Merge pull request #1887 from microsoft/milestones/m267
M267 Release
2 parents 557076b + 45d0405 commit 66959f0

File tree

15 files changed

+607
-146
lines changed

15 files changed

+607
-146
lines changed

.github/workflows/build.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ jobs:
103103
104104
- name: Checkout source
105105
if: steps.check.outputs.result == ''
106-
uses: actions/checkout@v5
106+
uses: actions/checkout@v6
107107

108108
- name: Validate Microsoft Git version
109109
if: steps.check.outputs.result == ''
@@ -138,7 +138,7 @@ jobs:
138138
139139
- name: Checkout source
140140
if: steps.skip.outputs.result != 'true'
141-
uses: actions/checkout@v5
141+
uses: actions/checkout@v6
142142
with:
143143
path: src
144144

GVFS/GVFS.Common/GVFSConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ public static class GitStatusCache
127127
{
128128
public const string Name = "gitStatusCache";
129129
public static readonly string CachePath = Path.Combine(Name, "GitStatusCache.dat");
130+
public static readonly string TreeCount = Path.Combine(Name, "TreeCountCache.dat");
130131
}
131132
}
132133

GVFS/GVFS.Common/Git/GitProcess.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,11 @@ public Result MultiPackIndexRepack(string gitObjectDirectory, string batchSize)
682682
return this.InvokeGitAgainstDotGitFolder($"-c pack.threads=1 -c repack.packKeptObjects=true multi-pack-index repack --object-dir=\"{gitObjectDirectory}\" --batch-size={batchSize} --no-progress");
683683
}
684684

685+
public Result GetHeadTreeId()
686+
{
687+
return this.InvokeGitAgainstDotGitFolder("show -s --format=%T HEAD", usePreCommandHook: false);
688+
}
689+
685690
public Process GetGitProcess(string command, string workingDirectory, string dotGitDirectory, bool useReadObjectHook, bool redirectStandardError, string gitObjectsDirectory, bool usePreCommandHook)
686691
{
687692
ProcessStartInfo processInfo = new ProcessStartInfo(this.gitBinPath);

GVFS/GVFS.Common/GitStatusCache.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public class GitStatusCache : IDisposable
5252

5353
private object cacheFileLock = new object();
5454

55+
internal static bool TEST_EnableHydrationSummary = true;
56+
5557
public GitStatusCache(GVFSContext context, GitStatusCacheConfig config)
5658
: this(context, config.BackoffTime)
5759
{
@@ -315,6 +317,7 @@ private void RebuildStatusCacheIfNeeded(bool ignoreBackoff)
315317
if (needToRebuild)
316318
{
317319
this.statistics.RecordBackgroundStatusScanRun();
320+
this.UpdateHydrationSummary();
318321

319322
bool rebuildStatusCacheSucceeded = this.TryRebuildStatusCache();
320323

@@ -336,6 +339,57 @@ private void RebuildStatusCacheIfNeeded(bool ignoreBackoff)
336339
}
337340
}
338341

342+
private void UpdateHydrationSummary()
343+
{
344+
if (!TEST_EnableHydrationSummary)
345+
{
346+
return;
347+
}
348+
349+
try
350+
{
351+
/* While not strictly part of git status, enlistment hydration summary is used
352+
* in "git status" pre-command hook, and can take several seconds to compute on very large repos.
353+
* Accessing it here ensures that the value is cached for when a user invokes "git status",
354+
* and this is also a convenient place to log telemetry for it.
355+
*/
356+
EnlistmentHydrationSummary hydrationSummary =
357+
EnlistmentHydrationSummary.CreateSummary(this.context.Enlistment, this.context.FileSystem);
358+
EventMetadata metadata = new EventMetadata();
359+
metadata.Add("Area", EtwArea);
360+
if (hydrationSummary.IsValid)
361+
{
362+
metadata[nameof(hydrationSummary.TotalFolderCount)] = hydrationSummary.TotalFolderCount;
363+
metadata[nameof(hydrationSummary.TotalFileCount)] = hydrationSummary.TotalFileCount;
364+
metadata[nameof(hydrationSummary.HydratedFolderCount)] = hydrationSummary.HydratedFolderCount;
365+
metadata[nameof(hydrationSummary.HydratedFileCount)] = hydrationSummary.HydratedFileCount;
366+
367+
this.context.Tracer.RelatedEvent(
368+
EventLevel.Informational,
369+
nameof(EnlistmentHydrationSummary),
370+
metadata,
371+
Keywords.Telemetry);
372+
}
373+
else
374+
{
375+
this.context.Tracer.RelatedWarning(
376+
metadata,
377+
$"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: hydration summary could not be calculdated.",
378+
Keywords.Telemetry);
379+
}
380+
}
381+
catch (Exception ex)
382+
{
383+
EventMetadata metadata = new EventMetadata();
384+
metadata.Add("Area", EtwArea);
385+
metadata.Add("Exception", ex.ToString());
386+
this.context.Tracer.RelatedError(
387+
metadata,
388+
$"{nameof(GitStatusCache)}{nameof(RebuildStatusCacheIfNeeded)}: Exception trying to update hydration summary cache.",
389+
Keywords.Telemetry);
390+
}
391+
}
392+
339393
/// <summary>
340394
/// Rebuild the status cache. This will run the background status to
341395
/// generate status results, and update the serialized status cache

GVFS/GVFS.Common/HealthCalculator/EnlistmentHealthCalculator.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32
using System.Linq;
43

54
namespace GVFS.Common
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
using GVFS.Common.FileSystem;
2+
using GVFS.Common.Git;
3+
using System;
4+
using System.IO;
5+
using System.Linq;
6+
7+
namespace GVFS.Common
8+
{
9+
public class EnlistmentHydrationSummary
10+
{
11+
public int HydratedFileCount { get; private set; }
12+
public int TotalFileCount { get; private set; }
13+
public int HydratedFolderCount { get; private set; }
14+
public int TotalFolderCount { get; private set; }
15+
16+
public bool IsValid
17+
{
18+
get
19+
{
20+
return HydratedFileCount >= 0
21+
&& HydratedFolderCount >= 0
22+
&& TotalFileCount >= HydratedFileCount
23+
&& TotalFolderCount >= HydratedFolderCount;
24+
}
25+
}
26+
27+
public string ToMessage()
28+
{
29+
if (!IsValid)
30+
{
31+
return "Error calculating hydration. Run 'gvfs health' for details.";
32+
}
33+
34+
int fileHydrationPercent = TotalFileCount == 0 ? 0 : (100 * HydratedFileCount) / TotalFileCount;
35+
int folderHydrationPercent = TotalFolderCount == 0 ? 0 : ((100 * HydratedFolderCount) / TotalFolderCount);
36+
return $"{fileHydrationPercent}% of files and {folderHydrationPercent}% of folders hydrated. Run 'gvfs health' for details.";
37+
}
38+
39+
public static EnlistmentHydrationSummary CreateSummary(
40+
GVFSEnlistment enlistment,
41+
PhysicalFileSystem fileSystem)
42+
{
43+
try
44+
{
45+
/* Getting all the file paths from git index is slow and we only need the total count,
46+
* so we read the index file header instead of calling GetPathsFromGitIndex */
47+
int totalFileCount = GetIndexFileCount(enlistment, fileSystem);
48+
49+
/* Getting all the directories is also slow, but not as slow as reading the entire index,
50+
* GetTotalPathCount caches the count so this is only slow occasionally,
51+
* and the GitStatusCache manager also calls this to ensure it is updated frequently. */
52+
int totalFolderCount = GetHeadTreeCount(enlistment, fileSystem);
53+
54+
EnlistmentPathData pathData = new EnlistmentPathData();
55+
56+
/* FUTURE: These could be optimized to only deal with counts instead of full path lists */
57+
pathData.LoadPlaceholdersFromDatabase(enlistment);
58+
pathData.LoadModifiedPaths(enlistment);
59+
60+
int hydratedFileCount = pathData.ModifiedFilePaths.Count + pathData.PlaceholderFilePaths.Count;
61+
int hydratedFolderCount = pathData.ModifiedFolderPaths.Count + pathData.PlaceholderFolderPaths.Count;
62+
return new EnlistmentHydrationSummary()
63+
{
64+
HydratedFileCount = hydratedFileCount,
65+
HydratedFolderCount = hydratedFolderCount,
66+
TotalFileCount = totalFileCount,
67+
TotalFolderCount = totalFolderCount,
68+
};
69+
}
70+
catch
71+
{
72+
return new EnlistmentHydrationSummary()
73+
{
74+
HydratedFileCount = -1,
75+
HydratedFolderCount = -1,
76+
TotalFileCount = -1,
77+
TotalFolderCount = -1,
78+
};
79+
}
80+
}
81+
82+
/// <summary>
83+
/// Get the total number of files in the index.
84+
/// </summary>
85+
internal static int GetIndexFileCount(GVFSEnlistment enlistment, PhysicalFileSystem fileSystem)
86+
{
87+
string indexPath = Path.Combine(enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Index);
88+
using (var indexFile = fileSystem.OpenFileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, callFlushFileBuffers: false))
89+
{
90+
if (indexFile.Length < 12)
91+
{
92+
return -1;
93+
}
94+
/* The number of files in the index is a big-endian integer from
95+
* the 4 bytes at offsets 8-11 of the index file. */
96+
indexFile.Position = 8;
97+
var bytes = new byte[4];
98+
indexFile.Read(
99+
bytes, // Destination buffer
100+
offset: 0, // Offset in destination buffer, not in indexFile
101+
count: 4);
102+
if (BitConverter.IsLittleEndian)
103+
{
104+
Array.Reverse(bytes);
105+
}
106+
int count = BitConverter.ToInt32(bytes, 0);
107+
return count;
108+
}
109+
}
110+
111+
/// <summary>
112+
/// Get the total number of trees in the repo at HEAD.
113+
/// </summary>
114+
/// <remarks>
115+
/// This is used as the denominator in displaying percentage of hydrated
116+
/// directories as part of git status pre-command hook.
117+
/// It can take several seconds to calculate, so we cache it near the git status cache.
118+
/// </remarks>
119+
/// <returns>
120+
/// The number of subtrees at HEAD, which may be 0.
121+
/// Will return 0 if unsuccessful.
122+
/// </returns>
123+
internal static int GetHeadTreeCount(GVFSEnlistment enlistment, PhysicalFileSystem fileSystem)
124+
{
125+
var gitProcess = enlistment.CreateGitProcess();
126+
var headResult = gitProcess.GetHeadTreeId();
127+
if (headResult.ExitCodeIsFailure)
128+
{
129+
return 0;
130+
}
131+
var headSha = headResult.Output.Trim();
132+
var cacheFile = Path.Combine(
133+
enlistment.DotGVFSRoot,
134+
GVFSConstants.DotGVFS.GitStatusCache.TreeCount);
135+
136+
// Load from cache if cache matches current HEAD.
137+
if (fileSystem.FileExists(cacheFile))
138+
{
139+
try
140+
{
141+
var lines = fileSystem.ReadLines(cacheFile).ToArray();
142+
if (lines.Length == 2
143+
&& lines[0] == headSha
144+
&& int.TryParse(lines[1], out int cachedCount))
145+
{
146+
return cachedCount;
147+
}
148+
}
149+
catch
150+
{
151+
// Ignore errors reading the cache
152+
}
153+
}
154+
155+
int totalPathCount = 0;
156+
GitProcess.Result folderResult = gitProcess.LsTree(
157+
GVFSConstants.DotGit.HeadName,
158+
line => totalPathCount++,
159+
recursive: true,
160+
showDirectories: true);
161+
try
162+
{
163+
fileSystem.CreateDirectory(Path.GetDirectoryName(cacheFile));
164+
fileSystem.WriteAllText(cacheFile, $"{headSha}\n{totalPathCount}");
165+
}
166+
catch
167+
{
168+
// Ignore errors writing the cache
169+
}
170+
171+
return totalPathCount;
172+
}
173+
}
174+
}

0 commit comments

Comments
 (0)