Skip to content

Commit 6fb7ab0

Browse files
perf(audience): cache DiskStore.Count to avoid per-tick directory scans
Sample diagnostics panels poll QueueSize on a UI tick — at 10 Hz with a non-trivial spool that's a continuous Directory.GetFiles scan for a value that barely changes. Cache the count instead. - Seed at construction from the existing spool so first poll is correct without a pre-mutation branch. Lazy-seeding on first bump had a double-count bug: the seed scan already saw the just-written file, then the +1 delta pushed the cache past the real on-disk count. - Write / Delete / DeleteAll / ApplyAnonymousDowngrade maintain the delta via BumpCount (Interlocked.Add) and TryDelete now returns a bool so "actually removed" vs "already gone" can drive the decrement. - TryDelete folds DirectoryNotFoundException into the true (idempotent) branch — per the MS docs it fires on invalid paths (example: "unmapped drive"), and an unreachable path can't point to a real file — and keeps IOException / UnauthorizedAccessException in the false branch so the cache stays honest when a file survived the delete attempt. FileNotFoundException is deliberately absent: File.Delete silently succeeds when the file is gone, so there is nothing to catch. - Count() reads through Volatile.Read so a caller on a different thread sees the latest published value. Cache invariant: on-disk-count-at-ctor + Σ tracked deltas. Events written outside the DiskStore API (direct File.* calls from tests that plant fixtures) are not tracked and will drift the cache; such tests assert on the post-op filesystem state, not Count(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0879819 commit 6fb7ab0

1 file changed

Lines changed: 29 additions & 14 deletions

File tree

src/Packages/Audience/Runtime/Transport/DiskStore.cs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics.CodeAnalysis;
66
using System.IO;
77
using System.Linq;
8+
using System.Threading;
89

910
namespace Immutable.Audience
1011
{
@@ -14,10 +15,17 @@ internal sealed class DiskStore
1415
{
1516
private readonly string _queueDir;
1617

18+
// Cached queue file count: on-disk count at construction, plus
19+
// tracked deltas from Write/Delete. Tests that plant files
20+
// outside the DiskStore API will drift this and should assert
21+
// on filesystem state, not Count().
22+
private int _cachedCount;
23+
1724
internal DiskStore(string persistentDataPath)
1825
{
1926
_queueDir = Path.Combine(persistentDataPath, "imtbl_audience", "queue");
2027
Directory.CreateDirectory(_queueDir);
28+
_cachedCount = Directory.GetFiles(_queueDir, "*.json").Length;
2129
}
2230

2331
// Atomically writes json as a new event file.
@@ -29,16 +37,19 @@ internal void Write(string json)
2937

3038
File.WriteAllText(tmpPath, json);
3139

40+
var replaced = false;
3241
try
3342
{
3443
File.Move(tmpPath, finalPath);
3544
}
3645
catch (IOException)
3746
{
38-
// Destination already exists (unlikely but safe to handle)
3947
File.Delete(finalPath);
4048
File.Move(tmpPath, finalPath);
49+
replaced = true;
4150
}
51+
52+
if (!replaced) BumpCount(+1);
4253
}
4354

4455
// Returns up to maxSize file paths, oldest first. Stale files
@@ -71,7 +82,7 @@ internal IReadOnlyList<string> ReadBatch(int maxSize)
7182
var fileTime = new DateTime(ticks, DateTimeKind.Utc);
7283
if (fileTime < cutoff)
7384
{
74-
TryDelete(path);
85+
if (TryDelete(path)) BumpCount(-1);
7586
continue;
7687
}
7788
}
@@ -86,17 +97,21 @@ internal IReadOnlyList<string> ReadBatch(int maxSize)
8697
internal void Delete(IEnumerable<string> paths)
8798
{
8899
foreach (var path in paths)
89-
TryDelete(path);
100+
if (TryDelete(path)) BumpCount(-1);
90101
}
91102

92-
// Total number of event files currently on disk.
93-
internal int Count() => Directory.GetFiles(_queueDir, "*.json").Length;
103+
// Total number of event files currently on disk. Reads the cached
104+
// count seeded at construction; mutating ops maintain it.
105+
internal int Count() => Volatile.Read(ref _cachedCount);
106+
107+
private void BumpCount(int delta) => Interlocked.Add(ref _cachedCount, delta);
94108

95-
private static void TryDelete(string path)
109+
private static bool TryDelete(string path)
96110
{
97-
try { File.Delete(path); }
98-
catch (IOException) { }
99-
catch (UnauthorizedAccessException) { }
111+
try { File.Delete(path); return true; }
112+
catch (DirectoryNotFoundException) { return true; }
113+
catch (IOException) { return false; }
114+
catch (UnauthorizedAccessException) { return false; }
100115
}
101116
internal void DeleteAll()
102117
{
@@ -105,7 +120,7 @@ internal void DeleteAll()
105120
catch (DirectoryNotFoundException) { return; }
106121

107122
foreach (var path in paths)
108-
TryDelete(path);
123+
if (TryDelete(path)) BumpCount(-1);
109124
}
110125

111126
// Drops queued identify/alias files, strips userId from track files.
@@ -126,13 +141,13 @@ private void ApplyAnonymousDowngradeToFile(string path)
126141
!msg.TryGetValue(MessageFields.Type, out var typeObj) ||
127142
!(typeObj is string type))
128143
{
129-
TryDelete(path);
144+
if (TryDelete(path)) BumpCount(-1);
130145
return;
131146
}
132147

133148
if (IsIdentityMessage(type))
134149
{
135-
TryDelete(path);
150+
if (TryDelete(path)) BumpCount(-1);
136151
return;
137152
}
138153

@@ -176,11 +191,11 @@ private void RewriteTrackWithoutUserId(string path, Dictionary<string, object> m
176191
catch (IOException)
177192
{
178193
// Delete rather than leave the old userId-bearing payload.
179-
TryDelete(path);
194+
if (TryDelete(path)) BumpCount(-1);
180195
}
181196
catch (UnauthorizedAccessException)
182197
{
183-
TryDelete(path);
198+
if (TryDelete(path)) BumpCount(-1);
184199
}
185200
}
186201

0 commit comments

Comments
 (0)