Skip to content

Commit f15ab14

Browse files
committed
Add 'gvfs cache' verb to display shared cache info
Shows cache root, git objects path, repo URL, pack file summary (prefetch count/size, other pack count/size, latest prefetch timestamp), and loose object count. Emits a CacheInfo telemetry event with all stats.
1 parent 5924add commit f15ab14

3 files changed

Lines changed: 390 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using GVFS.CommandLine;
2+
using NUnit.Framework;
3+
using System;
4+
using System.IO;
5+
6+
namespace GVFS.UnitTests.CommandLine
7+
{
8+
[TestFixture]
9+
public class CacheVerbTests
10+
{
11+
private CacheVerb cacheVerb;
12+
private string testDir;
13+
14+
[SetUp]
15+
public void Setup()
16+
{
17+
this.cacheVerb = new CacheVerb();
18+
this.testDir = Path.Combine(Path.GetTempPath(), "CacheVerbTests_" + Guid.NewGuid().ToString("N"));
19+
Directory.CreateDirectory(this.testDir);
20+
}
21+
22+
[TearDown]
23+
public void TearDown()
24+
{
25+
if (Directory.Exists(this.testDir))
26+
{
27+
Directory.Delete(this.testDir, recursive: true);
28+
}
29+
}
30+
31+
[TestCase(0, "0 bytes")]
32+
[TestCase(512, "512 bytes")]
33+
[TestCase(1023, "1023 bytes")]
34+
[TestCase(1024, "1.0 KB")]
35+
[TestCase(1536, "1.5 KB")]
36+
[TestCase(1048576, "1.0 MB")]
37+
[TestCase(1572864, "1.5 MB")]
38+
[TestCase(1073741824, "1.0 GB")]
39+
[TestCase(1610612736, "1.5 GB")]
40+
[TestCase(10737418240, "10.0 GB")]
41+
public void FormatSizeReturnsExpectedString(long bytes, string expected)
42+
{
43+
Assert.AreEqual(expected, this.cacheVerb.FormatSize(bytes));
44+
}
45+
46+
[TestCase]
47+
public void GetPackSummaryWithNoPacks()
48+
{
49+
string packDir = Path.Combine(this.testDir, "pack");
50+
Directory.CreateDirectory(packDir);
51+
52+
this.cacheVerb.GetPackSummary(
53+
packDir,
54+
out int prefetchCount,
55+
out long prefetchSize,
56+
out int otherCount,
57+
out long otherSize,
58+
out long latestTimestamp);
59+
60+
Assert.AreEqual(0, prefetchCount);
61+
Assert.AreEqual(0, prefetchSize);
62+
Assert.AreEqual(0, otherCount);
63+
Assert.AreEqual(0, otherSize);
64+
Assert.AreEqual(0, latestTimestamp);
65+
}
66+
67+
[TestCase]
68+
public void GetPackSummaryCategorizesPrefetchAndOtherPacks()
69+
{
70+
string packDir = Path.Combine(this.testDir, "pack");
71+
Directory.CreateDirectory(packDir);
72+
73+
this.CreateFileWithSize(Path.Combine(packDir, "prefetch-1000-aabbccdd.pack"), 100);
74+
this.CreateFileWithSize(Path.Combine(packDir, "prefetch-2000-eeff0011.pack"), 200);
75+
this.CreateFileWithSize(Path.Combine(packDir, "pack-abcdef1234567890.pack"), 50);
76+
77+
this.cacheVerb.GetPackSummary(
78+
packDir,
79+
out int prefetchCount,
80+
out long prefetchSize,
81+
out int otherCount,
82+
out long otherSize,
83+
out long latestTimestamp);
84+
85+
Assert.AreEqual(2, prefetchCount);
86+
Assert.AreEqual(300, prefetchSize);
87+
Assert.AreEqual(1, otherCount);
88+
Assert.AreEqual(50, otherSize);
89+
Assert.AreEqual(2000, latestTimestamp);
90+
}
91+
92+
[TestCase]
93+
public void GetPackSummaryIgnoresNonPackFiles()
94+
{
95+
string packDir = Path.Combine(this.testDir, "pack");
96+
Directory.CreateDirectory(packDir);
97+
98+
this.CreateFileWithSize(Path.Combine(packDir, "prefetch-1000-aabb.pack"), 100);
99+
this.CreateFileWithSize(Path.Combine(packDir, "prefetch-1000-aabb.idx"), 50);
100+
this.CreateFileWithSize(Path.Combine(packDir, "multi-pack-index"), 10);
101+
102+
this.cacheVerb.GetPackSummary(
103+
packDir,
104+
out int prefetchCount,
105+
out long prefetchSize,
106+
out int otherCount,
107+
out long otherSize,
108+
out long latestTimestamp);
109+
110+
Assert.AreEqual(1, prefetchCount);
111+
Assert.AreEqual(100, prefetchSize);
112+
Assert.AreEqual(0, otherCount);
113+
Assert.AreEqual(0, otherSize);
114+
}
115+
116+
[TestCase]
117+
public void GetPackSummaryHandlesBothGuidAndSHA1HashFormats()
118+
{
119+
string packDir = Path.Combine(this.testDir, "pack");
120+
Directory.CreateDirectory(packDir);
121+
122+
// GVFS format: 32-char GUID
123+
this.CreateFileWithSize(Path.Combine(packDir, "prefetch-1000-b8d9efad32194d98894532905daf88ec.pack"), 100);
124+
// Scalar format: 40-char SHA1
125+
this.CreateFileWithSize(Path.Combine(packDir, "prefetch-2000-9babd9b75521f9caf693b485329d3d5669c88564.pack"), 200);
126+
127+
this.cacheVerb.GetPackSummary(
128+
packDir,
129+
out int prefetchCount,
130+
out long prefetchSize,
131+
out int otherCount,
132+
out long otherSize,
133+
out long latestTimestamp);
134+
135+
Assert.AreEqual(2, prefetchCount);
136+
Assert.AreEqual(300, prefetchSize);
137+
Assert.AreEqual(2000, latestTimestamp);
138+
}
139+
140+
[TestCase]
141+
public void CountLooseObjectsWithNoObjects()
142+
{
143+
int count = this.cacheVerb.CountLooseObjects(this.testDir);
144+
Assert.AreEqual(0, count);
145+
}
146+
147+
[TestCase]
148+
public void CountLooseObjectsCountsFilesInHexDirectories()
149+
{
150+
Directory.CreateDirectory(Path.Combine(this.testDir, "00"));
151+
File.WriteAllText(Path.Combine(this.testDir, "00", "abc123"), string.Empty);
152+
File.WriteAllText(Path.Combine(this.testDir, "00", "def456"), string.Empty);
153+
154+
Directory.CreateDirectory(Path.Combine(this.testDir, "ff"));
155+
File.WriteAllText(Path.Combine(this.testDir, "ff", "789abc"), string.Empty);
156+
157+
int count = this.cacheVerb.CountLooseObjects(this.testDir);
158+
Assert.AreEqual(3, count);
159+
}
160+
161+
[TestCase]
162+
public void CountLooseObjectsIgnoresNonHexDirectories()
163+
{
164+
// "pack" and "info" are valid directories in a git objects dir but not hex dirs
165+
Directory.CreateDirectory(Path.Combine(this.testDir, "pack"));
166+
File.WriteAllText(Path.Combine(this.testDir, "pack", "somefile"), string.Empty);
167+
168+
Directory.CreateDirectory(Path.Combine(this.testDir, "info"));
169+
File.WriteAllText(Path.Combine(this.testDir, "info", "somefile"), string.Empty);
170+
171+
// "ab" is a valid hex dir
172+
Directory.CreateDirectory(Path.Combine(this.testDir, "ab"));
173+
File.WriteAllText(Path.Combine(this.testDir, "ab", "object1"), string.Empty);
174+
175+
int count = this.cacheVerb.CountLooseObjects(this.testDir);
176+
Assert.AreEqual(1, count);
177+
}
178+
179+
private void CreateFileWithSize(string path, int size)
180+
{
181+
byte[] data = new byte[size];
182+
File.WriteAllBytes(path, data);
183+
}
184+
}
185+
}

GVFS/GVFS/CommandLine/CacheVerb.cs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
using CommandLine;
2+
using GVFS.Common;
3+
using GVFS.Common.FileSystem;
4+
using GVFS.Common.Tracing;
5+
using System;
6+
using System.IO;
7+
using System.Linq;
8+
9+
namespace GVFS.CommandLine
10+
{
11+
[Verb(CacheVerb.CacheVerbName, HelpText = "Display information about the GVFS shared object cache")]
12+
public class CacheVerb : GVFSVerb.ForExistingEnlistment
13+
{
14+
private const string CacheVerbName = "cache";
15+
16+
public CacheVerb()
17+
{
18+
}
19+
20+
protected override string VerbName
21+
{
22+
get { return CacheVerbName; }
23+
}
24+
25+
protected override void Execute(GVFSEnlistment enlistment)
26+
{
27+
using (ITracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "CacheVerb"))
28+
{
29+
string localCacheRoot;
30+
string gitObjectsRoot;
31+
this.GetLocalCachePaths(tracer, enlistment, out localCacheRoot, out gitObjectsRoot);
32+
33+
if (string.IsNullOrWhiteSpace(gitObjectsRoot))
34+
{
35+
this.ReportErrorAndExit("Could not determine git objects root. Is this a GVFS enlistment with a shared cache?");
36+
}
37+
38+
this.Output.WriteLine("Repo URL: " + enlistment.RepoUrl);
39+
this.Output.WriteLine("Cache root: " + (localCacheRoot ?? "(unknown)"));
40+
this.Output.WriteLine("Git objects: " + gitObjectsRoot);
41+
42+
string packRoot = Path.Combine(gitObjectsRoot, GVFSConstants.DotGit.Objects.Pack.Name);
43+
if (!Directory.Exists(packRoot))
44+
{
45+
this.Output.WriteLine();
46+
this.Output.WriteLine("Pack directory not found: " + packRoot);
47+
tracer.RelatedError("Pack directory not found: " + packRoot);
48+
return;
49+
}
50+
51+
int prefetchPackCount;
52+
long prefetchPackSize;
53+
int otherPackCount;
54+
long otherPackSize;
55+
long latestPrefetchTimestamp;
56+
this.GetPackSummary(packRoot, out prefetchPackCount, out prefetchPackSize, out otherPackCount, out otherPackSize, out latestPrefetchTimestamp);
57+
58+
int looseObjectCount = this.CountLooseObjects(gitObjectsRoot);
59+
60+
long totalSize = prefetchPackSize + otherPackSize;
61+
this.Output.WriteLine();
62+
this.Output.WriteLine("Total pack size: " + this.FormatSize(totalSize));
63+
this.Output.WriteLine("Prefetch packs: " + prefetchPackCount + " (" + this.FormatSize(prefetchPackSize) + ")");
64+
this.Output.WriteLine("Other packs: " + otherPackCount + " (" + this.FormatSize(otherPackSize) + ")");
65+
66+
if (latestPrefetchTimestamp > 0)
67+
{
68+
DateTimeOffset latestTime = DateTimeOffset.FromUnixTimeSeconds(latestPrefetchTimestamp).ToLocalTime();
69+
this.Output.WriteLine("Latest prefetch: " + latestTime.ToString("yyyy-MM-dd HH:mm:ss zzz"));
70+
}
71+
72+
this.Output.WriteLine("Loose objects: " + looseObjectCount.ToString("N0"));
73+
74+
EventMetadata metadata = new EventMetadata();
75+
metadata.Add("repoUrl", enlistment.RepoUrl);
76+
metadata.Add("localCacheRoot", localCacheRoot);
77+
metadata.Add("gitObjectsRoot", gitObjectsRoot);
78+
metadata.Add("prefetchPackCount", prefetchPackCount);
79+
metadata.Add("prefetchPackSize", prefetchPackSize);
80+
metadata.Add("otherPackCount", otherPackCount);
81+
metadata.Add("otherPackSize", otherPackSize);
82+
metadata.Add("latestPrefetchTimestamp", latestPrefetchTimestamp);
83+
metadata.Add("looseObjectCount", looseObjectCount);
84+
tracer.RelatedEvent(EventLevel.Informational, "CacheInfo", metadata, Keywords.Telemetry);
85+
}
86+
}
87+
88+
internal void GetPackSummary(
89+
string packRoot,
90+
out int prefetchPackCount,
91+
out long prefetchPackSize,
92+
out int otherPackCount,
93+
out long otherPackSize,
94+
out long latestPrefetchTimestamp)
95+
{
96+
prefetchPackCount = 0;
97+
prefetchPackSize = 0;
98+
otherPackCount = 0;
99+
otherPackSize = 0;
100+
latestPrefetchTimestamp = 0;
101+
102+
string[] packFiles = Directory.GetFiles(packRoot, "*.pack");
103+
104+
foreach (string packFile in packFiles)
105+
{
106+
long length = new FileInfo(packFile).Length;
107+
string fileName = Path.GetFileName(packFile);
108+
109+
if (fileName.StartsWith(GVFSConstants.PrefetchPackPrefix, StringComparison.OrdinalIgnoreCase))
110+
{
111+
prefetchPackCount++;
112+
prefetchPackSize += length;
113+
114+
long? timestamp = this.TryGetPrefetchTimestamp(packFile);
115+
if (timestamp.HasValue && timestamp.Value > latestPrefetchTimestamp)
116+
{
117+
latestPrefetchTimestamp = timestamp.Value;
118+
}
119+
}
120+
else
121+
{
122+
otherPackCount++;
123+
otherPackSize += length;
124+
}
125+
}
126+
}
127+
128+
internal int CountLooseObjects(string gitObjectsRoot)
129+
{
130+
int looseObjectCount = 0;
131+
132+
for (int i = 0; i < 256; i++)
133+
{
134+
string hexDir = Path.Combine(gitObjectsRoot, i.ToString("x2"));
135+
if (Directory.Exists(hexDir))
136+
{
137+
looseObjectCount += Directory.GetFiles(hexDir).Length;
138+
}
139+
}
140+
141+
return looseObjectCount;
142+
}
143+
144+
private long? TryGetPrefetchTimestamp(string packPath)
145+
{
146+
string filename = Path.GetFileName(packPath);
147+
string[] parts = filename.Split('-');
148+
if (parts.Length > 1 && long.TryParse(parts[1], out long timestamp))
149+
{
150+
return timestamp;
151+
}
152+
153+
return null;
154+
}
155+
156+
internal string FormatSize(long bytes)
157+
{
158+
if (bytes >= 1L << 30)
159+
{
160+
return string.Format("{0:F1} GB", bytes / (double)(1L << 30));
161+
}
162+
163+
if (bytes >= 1L << 20)
164+
{
165+
return string.Format("{0:F1} MB", bytes / (double)(1L << 20));
166+
}
167+
168+
if (bytes >= 1L << 10)
169+
{
170+
return string.Format("{0:F1} KB", bytes / (double)(1L << 10));
171+
}
172+
173+
return bytes + " bytes";
174+
}
175+
176+
private void GetLocalCachePaths(ITracer tracer, GVFSEnlistment enlistment, out string localCacheRoot, out string gitObjectsRoot)
177+
{
178+
localCacheRoot = null;
179+
gitObjectsRoot = null;
180+
181+
try
182+
{
183+
string error;
184+
if (RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot), out error))
185+
{
186+
RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error);
187+
RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error);
188+
}
189+
else
190+
{
191+
this.ReportErrorAndExit("Failed to read repo metadata: " + error);
192+
}
193+
}
194+
catch (Exception e)
195+
{
196+
this.ReportErrorAndExit("Failed to read repo metadata: " + e.Message);
197+
}
198+
finally
199+
{
200+
RepoMetadata.Shutdown();
201+
}
202+
}
203+
}
204+
}

GVFS/GVFS/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static void Main(string[] args)
2222
Type[] verbTypes = new Type[]
2323
{
2424
typeof(CacheServerVerb),
25+
typeof(CacheVerb),
2526
typeof(CloneVerb),
2627
typeof(ConfigVerb),
2728
typeof(DehydrateVerb),

0 commit comments

Comments
 (0)