• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

neon-sunset / fast-cache / 13239284728

10 Feb 2025 10:59AM UTC coverage: 92.017% (-0.9%) from 92.885%
13239284728

push

github

web-flow
Bump the nuget group across 1 directory with 3 updates (#132)

Bumps the nuget group with 3 updates in the / directory: [Microsoft.Extensions.Caching.Memory](https://github.com/dotnet/runtime), [ZiggyCreatures.FusionCache](https://github.com/ZiggyCreatures/FusionCache) and [xunit.runner.visualstudio](https://github.com/xunit/visualstudio.xunit).


Updates `Microsoft.Extensions.Caching.Memory` from 9.0.1 to 9.0.0
- [Release notes](https://github.com/dotnet/runtime/releases)
- [Commits](https://github.com/dotnet/runtime/compare/v9.0.1...v9.0.0)

Updates `ZiggyCreatures.FusionCache` from 2.0.0 to 2.1.0
- [Release notes](https://github.com/ZiggyCreatures/FusionCache/releases)
- [Commits](https://github.com/ZiggyCreatures/FusionCache/compare/v2.0.0...v2.1.0)

Updates `xunit.runner.visualstudio` from 3.0.1 to 3.0.2
- [Release notes](https://github.com/xunit/visualstudio.xunit/releases)
- [Commits](https://github.com/xunit/visualstudio.xunit/compare/3.0.1...3.0.2)

---
updated-dependencies:
- dependency-name: Microsoft.Extensions.Caching.Memory
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget
- dependency-name: ZiggyCreatures.FusionCache
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget
- dependency-name: xunit.runner.visualstudio
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

342 of 418 branches covered (81.82%)

Branch coverage included in aggregate %.

1779 of 1887 relevant lines covered (94.28%)

1107.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.86
/src/FastCache.Cached/CacheManager.cs
1
using System.Diagnostics;
2
using FastCache.Helpers;
3

4
namespace FastCache.Services;
5

6
public static class CacheManager
7
{
8
    private static readonly SemaphoreSlim FullGCLock = new(1, 1);
1✔
9

10
    private static long s_AggregatedEvictionsCount;
11

12
    /// <summary>
13
    /// Total atomic count of entries present in cache, including expired.
14
    /// </summary>
15
    /// <typeparam name="K">Cache entry key type. string, int or (int, int) for multi-key.</typeparam>
16
    /// <typeparam name="V">Cache entry value type</typeparam>
17
    public static int TotalCount<K, V>() where K : notnull => CacheStaticHolder<K, V>.Store.Count;
1✔
18

19
    /// <summary>
20
    /// Trigger full eviction for expired cache entries of type Cached[K, V].
21
    /// This operation is a no-op when eviction is suspended.
22
    /// </summary>
23
    public static void QueueFullEviction<K, V>() where K : notnull
24
    {
25
        _ = ExecuteFullEviction<K, V>(triggeredByGC: false);
2✔
26
    }
2✔
27

28
    /// <summary>
29
    /// Remove all cache entries of type Cached[K, V] from the cache
30
    /// </summary>
31
    public static void QueueFullClear<K, V>() where K : notnull
32
    {
33
        _ = ExecuteFullClear<K, V>();
1✔
34
    }
1✔
35

36
    /// <summary>
37
    /// Trigger full eviction for expired cache entries of type Cached[K, V].
38
    /// This operation is a no-op when eviction is suspended.
39
    /// Disclaimer: if there is an ongoing staggered full eviction (triggered by Gen2 GC), this method will await its completion, which can take significant time.
40
    /// </summary>
41
    /// <returns>One of: A task for a new full eviction that completes upon its execution; A task for an already ongoing eviction or clear.</returns>
42
    public static Task ExecuteFullEviction<K, V>() where K : notnull => ExecuteFullEviction<K, V>(triggeredByGC: false);
×
43

44
    /// <summary>
45
    /// Remove all cache entries of type Cached[K, V] from the cache.
46
    /// Disclaimer: if there is an ongoing staggered full eviction (triggered by Gen2 GC),
47
    /// this method will await its completion before proceeding with full clear, which can take significant time.
48
    /// For benchmarking purposes, consider suspending eviction first before calling this method.
49
    /// </summary>
50
    /// <returns>A task that completes upon full clear execution.</returns>
51
    public static async Task ExecuteFullClear<K, V>() where K : notnull
52
    {
53
#if FASTCACHE_DEBUG
54
        var countBefore = CacheStaticHolder<K, V>.Store.Count;
55
#endif
56

57
        var evictionJob = CacheStaticHolder<K, V>.EvictionJob;
1✔
58
        await evictionJob.FullEvictionLock.WaitAsync();
1✔
59

60
        static void Inner()
61
        {
62
            CacheStaticHolder<K, V>.Store.Clear();
1✔
63
            CacheStaticHolder<K, V>.QuickList.Reset();
1✔
64
        }
1✔
65

66
        await (evictionJob.ActiveFullEviction = Task.Run(Inner));
1✔
67

68
        evictionJob.ActiveFullEviction = null;
1✔
69
        evictionJob.FullEvictionLock.Release();
1✔
70

71
#if FASTCACHE_DEBUG
72
        Console.WriteLine(
73
            $"FastCache: Cache has been fully cleared for {typeof(K).Name}:{typeof(V).Name}. Was {countBefore}, now {CacheStaticHolder<K, V>.QuickList.AtomicCount}/{CacheStaticHolder<K, V>.Store.Count}");
74
#endif
75
    }
1✔
76

77
    /// <summary>
78
    /// Enumerates all not expired entries currently present in the cache.
79
    /// Cache changes introduced from other threads may not be visible to the enumerator.
80
    /// </summary>
81
    /// <typeparam name="K">Cache entry key type. string, int or (int, int) for multi-key.</typeparam>
82
    /// <typeparam name="V">Cache entry value type</typeparam>
83
    public static IEnumerable<Cached<K, V>> EnumerateEntries<K, V>() where K : notnull
84
    {
85
        foreach (var (key, inner) in CacheStaticHolder<K, V>.Store)
4,098✔
86
        {
87
            if (inner.IsNotExpired())
2,048✔
88
            {
89
                yield return new(key, inner.Value, found: true);
1,024✔
90
            }
91
        }
92
    }
1✔
93

94
    /// <summary>
95
    /// Trims cache store for a given percentage of its size. Will remove at least 1 item.
96
    /// </summary>
97
    /// <typeparam name="K">Cache entry key type. string, int or (int, int) for multi-key.</typeparam>
98
    /// <typeparam name="V">Cache entry value type</typeparam>
99
    /// <returns>True: trim is performed inline. False: the count to trim is above threshold and removal is queued to thread pool.</returns>
100
    public static bool Trim<K, V>(double percentage) where K : notnull
101
    {
102
        if (percentage is > 100.0 or <= double.Epsilon or double.NaN)
8!
103
        {
104
            ThrowHelpers.ArgumentOutOfRange(percentage, nameof(percentage));
8✔
105
        }
106

107
        var store = CacheStaticHolder<K, V>.Store;
×
108
        var count = store.Count;
×
109
        var target = (uint)count - (uint)(count * (percentage / 100.0));
×
110
        var toTrim = count - target;
×
111

112
        var trimmed = Trim<K, V>(target, percentage);
×
113
        if (trimmed >= toTrim)
×
114
        {
115
            return true;
×
116
        }
117
        toTrim -= trimmed;
×
118

119
        ThreadPool.QueueUserWorkItem(target =>
×
120
        {
×
121
            var store = CacheStaticHolder<K, V>.Store;
×
122
            foreach (var (key, _) in store)
×
123
            {
×
124
                if (target-- <= 0) break;
×
125
                store.TryRemove(key, out _);
×
126
            }
×
127
        }, toTrim, preferLocal: false);
×
128

129
        return false;
×
130
    }
131

132
    internal static uint Trim<K, V>(uint target, double percentage) where K : notnull
133
    {
134
        var store = CacheStaticHolder<K, V>.Store;
1✔
135
        var quickList = CacheStaticHolder<K, V>.QuickList;
1✔
136

137
        if (quickList.InProgress && store.Count < target)
1!
138
        {
139
            // Bail out early if a concurrent trim is already in progress and the count is below the limit.
140
            return target;
×
141
        }
142

143
        var trimCount = Math.Min(
1✔
144
            Math.Max(1, (uint)(store.Count * (percentage / 100.0))),
1✔
145
            Constants.InlineTrimCountLimit);
1✔
146

147
        var removedFromQuickList = quickList.Trim(trimCount);
1✔
148
        if (removedFromQuickList >= trimCount || store.Count <= target)
1!
149
        {
150
            return removedFromQuickList;
1✔
151
        }
152

153
        var removed = 0;
×
154
        var enumerator = store.GetEnumerator();
×
155
        var toRemoveFromStore = trimCount - removedFromQuickList;
×
156

157
        while (removed < toRemoveFromStore && enumerator.MoveNext())
×
158
        {
159
            store.TryRemove(enumerator.Current.Key, out _);
×
160
            removed++;
×
161
        }
162

163
        return (uint)removed + removedFromQuickList;
×
164
    }
165

166
    /// <summary>
167
    /// Suspends automatic eviction. Does not affect already in-flight operations.
168
    /// </summary>
169
    public static void SuspendEviction<K, V>() where K : notnull => CacheStaticHolder<K, V>.EvictionJob.Stop();
3✔
170

171
    /// <summary>
172
    /// Resumes eviction, next iteration will occur after a standard adaptive interval from now.
173
    /// Is a no-op if automatic eviction is disabled.
174
    /// </summary>
175
    public static void ResumeEviction<K, V>() where K : notnull => CacheStaticHolder<K, V>.EvictionJob.Resume();
2✔
176

177
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
178
    internal static void ReportEvictions(uint count)
179
    {
180
        if (!Constants.ConsiderFullGC)
5!
181
        {
182
            return;
×
183
        }
184

185
        Interlocked.Add(ref s_AggregatedEvictionsCount, count);
5✔
186
    }
5✔
187

188
    internal static async Task ExecuteFullEviction<K, V>(bool triggeredByGC) where K : notnull
189
    {
190
        var evictionJob = CacheStaticHolder<K, V>.EvictionJob;
33✔
191
        if (!evictionJob.IsActive)
33✔
192
        {
193
            return;
3✔
194
        }
195

196
    Retry:
197
        if (!evictionJob.FullEvictionLock.Wait(millisecondsTimeout: 0))
30✔
198
        {
199
            var activeEviction = evictionJob.ActiveFullEviction;
9✔
200
            if (activeEviction is null)
9✔
201
            {
202
                goto Retry;
203
            }
204

205
            await activeEviction;
9✔
206
            return;
9✔
207
        }
208

209
        evictionJob.ActiveFullEviction = !triggeredByGC
23✔
210
            ? Task.Run(ImmediateFullEviction<K, V>)
23✔
211
            : StaggeredFullEviction<K, V>();
23✔
212

213
        await evictionJob.ActiveFullEviction;
23✔
214

215
        evictionJob.ActiveFullEviction = null;
12✔
216
        evictionJob.FullEvictionLock.Release();
12✔
217
    }
22✔
218

219
    private static void ImmediateFullEviction<K, V>() where K : notnull
220
    {
221
        var (evictionJob, quickList) = (
2✔
222
            CacheStaticHolder<K, V>.EvictionJob,
2✔
223
            CacheStaticHolder<K, V>.QuickList);
2✔
224

225
        evictionJob.RescheduleConsideringExpiration();
2✔
226

227
        if (quickList.Evict(resize: true))
2!
228
        {
229
            return;
×
230
        }
231

232
#if FASTCACHE_DEBUG
233
        var stopwatch = Stopwatch.StartNew();
234
#endif
235
        var evictedFromCacheStore = EvictFromCacheStore<K, V>();
2✔
236

237
        if (Constants.ConsiderFullGC && evictedFromCacheStore > 0)
2✔
238
        {
239
            ReportEvictions(evictedFromCacheStore);
2✔
240
        }
241

242
#if FASTCACHE_DEBUG
243
        PrintEvicted<K, V>(evictedFromCacheStore, stopwatch.Elapsed);
244
#endif
245

246
        Task.Run(async static () => await ConsiderFullGC<V>());
4✔
247
    }
2✔
248

249
    private static async Task StaggeredFullEviction<K, V>() where K : notnull
250
    {
251
        var (quickList, evictionJob) = (
21✔
252
            CacheStaticHolder<K, V>.QuickList,
21✔
253
            CacheStaticHolder<K, V>.EvictionJob);
21✔
254

255
        if (quickList.Evict())
21✔
256
        {
257
            // When a lot of items are being added to cache, it triggers GC and its callbacks
258
            // which may decrease throughput by accessing the same memory locations
259
            // from multiple threads and wasting CPU time on repeated eviction cycles
260
            // over newly added items which is not profitable to do.
261
            // Delaying lock release for extra (quick list interval / 5) avoids the issue. 
262
            await Task.Delay(Constants.EvictionCooldownDelayOnGC);
14✔
263
            return;
7✔
264
        }
265

266
        evictionJob.EvictionGCNotificationsCount++;
7✔
267
        if (evictionJob.EvictionGCNotificationsCount < 2)
7✔
268
        {
269
            await Task.Delay(Constants.EvictionCooldownDelayOnGC);
4✔
270
            return;
3✔
271
        }
272

273
        await Task.Delay(Constants.CacheStoreEvictionDelay);
3✔
274

275
#if FASTCACHE_DEBUG
276
        var stopwatch = Stopwatch.StartNew();
277
#endif
278
        var evictedFromCacheStore = EvictFromCacheStore<K, V>();
×
279

280
        if (Constants.ConsiderFullGC && evictedFromCacheStore > 0)
×
281
        {
282
            ReportEvictions(evictedFromCacheStore);
×
283
        }
284

285
#if FASTCACHE_DEBUG
286
        PrintEvicted<K, V>(evictedFromCacheStore, stopwatch.Elapsed);
287
#endif
288

289
        await Task.Delay(Constants.EvictionCooldownDelayOnGC);
×
290
        evictionJob.EvictionGCNotificationsCount = 0;
×
291
    }
10✔
292

293
    private static uint EvictFromCacheStore<K, V>() where K : notnull
294
    {
295
        var (store, quicklist) = (
2✔
296
            CacheStaticHolder<K, V>.Store,
2✔
297
            CacheStaticHolder<K, V>.QuickList);
2✔
298

299
        var evictedCount = store.Count > Constants.ParallelEvictionThreshold
2!
300
            ? EvictFromCacheStoreParallel<K, V>()
2✔
301
            : EvictFromCacheStoreSingleThreaded<K, V>();
2✔
302

303
        quicklist.PullFromCacheStore();
2✔
304

305
        return evictedCount;
2✔
306
    }
307

308
    private static uint EvictFromCacheStoreSingleThreaded<K, V>() where K : notnull
309
    {
310
        var now = TimeUtils.Now;
2✔
311
        var store = CacheStaticHolder<K, V>.Store;
2✔
312
        uint totalRemoved = 0;
2✔
313

314
        foreach (var (key, value) in store)
5,124✔
315
        {
316
            if (now > value._timestamp)
2,560✔
317
            {
318
                store.TryRemove(key, out _);
1,536✔
319
                totalRemoved++;
1,536✔
320
            }
321
        }
322

323
        return totalRemoved;
2✔
324
    }
325

326
    private static uint EvictFromCacheStoreParallel<K, V>() where K : notnull
327
    {
328
        var now = TimeUtils.Now;
×
329
        var store = CacheStaticHolder<K, V>.Store;
×
330
        var countBefore = store.Count;
×
331

332
        void CheckAndRemove(KeyValuePair<K, CachedInner<V>> kvp)
333
        {
334
            var (key, timestamp) = (kvp.Key, kvp.Value._timestamp);
×
335

336
            if (now > timestamp)
×
337
            {
338
                store.TryRemove(key, out _);
×
339
            }
340
        }
×
341

342
        store
×
343
            .AsParallel()
×
344
            .AsUnordered()
×
345
            .ForAll(CheckAndRemove);
×
346

347
        // Perform dirty evictions count and discard the result if eviction
348
        // happened to overlap with significant amount of insertions.
349
        // The logic that consumes this value does not require precise reports.
350
        return (uint)Math.Max(countBefore - store.Count, 0);
×
351
    }
352

353
    private static async ValueTask ConsiderFullGC<T>()
354
    {
355
        if (!Constants.ConsiderFullGC)
2!
356
        {
357
            return;
×
358
        }
359

360
        if (Interlocked.Read(ref s_AggregatedEvictionsCount) <= Constants.AggregatedGCThreshold)
2!
361
        {
362
            return;
×
363
        }
364

365
        if (!FullGCLock.Wait(millisecondsTimeout: 0))
2✔
366
        {
367
            return;
1✔
368
        }
369

370
        await Task.Delay(Constants.DelayToFullGC);
1✔
371
#if FASTCACHE_DEBUG
372
        var sw = Stopwatch.StartNew();
373
#endif
374

375
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Default, blocking: false);
1✔
376

377
#if FASTCACHE_DEBUG
378
        Console.WriteLine($"FastCache: Full GC has been requested or ran, reported evictions count has been reset, was: {s_AggregatedEvictionsCount}. Source: {typeof(T).Name}. Elapsed:{sw.ElapsedMilliseconds} ms");
379
#endif
380
        Interlocked.Exchange(ref s_AggregatedEvictionsCount, 0);
1✔
381

382
        await Task.Delay(Constants.CooldownDelayAfterFullGC);
1✔
383
        FullGCLock.Release();
×
384
    }
1✔
385

386
#if FASTCACHE_DEBUG
387
    private static void PrintEvicted<K, V>(uint count, TimeSpan elapsed) where K : notnull
388
    {
389
        var size = CacheStaticHolder<K, V>.Store.Count;
390
        Console.WriteLine(
391
            $"FastCache: Evicted {count} of {typeof(K).Name}:{typeof(V).Name} from cache store. Size after: {size}, took {elapsed.TotalMilliseconds} ms.");
392
    }
393
#endif
394
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc