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

neon-sunset / fast-cache / 5782290874

pending completion
5782290874

Pull #58

github

web-flow
Bump Microsoft.NET.Test.Sdk from 17.6.3 to 17.7.0

Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.6.3 to 17.7.0.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md)
- [Commits](https://github.com/microsoft/vstest/compare/v17.6.3...v17.7.0)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #58: Bump Microsoft.NET.Test.Sdk from 17.6.3 to 17.7.0

364 of 418 branches covered (87.08%)

Branch coverage included in aggregate %.

1824 of 1891 relevant lines covered (96.46%)

1372.5 hits per line

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

90.38
/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
    /// <param name="percentage"></param>
100
    /// <returns>True: trim is performed inline. False: the count to trim is above threshold and removal is queued to thread pool.</returns>
101
    public static bool Trim<K, V>(double percentage) where K : notnull
102
    {
103
        if (percentage is > 100.0 or <= double.Epsilon or double.NaN)
10✔
104
        {
105
            ThrowHelpers.ArgumentOutOfRange(percentage, nameof(percentage));
8✔
106
        }
107

108
        if (CacheStaticHolder<K, V>.QuickList.InProgress)
2!
109
        {
110
            // Bail out early if the items are being removed via quick list.
111
            return false;
×
112
        }
113

114
        static void ExecuteTrim(uint trimCount, bool takeLock)
115
        {
116
            var removedFromQuickList = CacheStaticHolder<K, V>.QuickList.Trim(trimCount);
2✔
117
            if (removedFromQuickList >= trimCount)
2✔
118
            {
119
                return;
1✔
120
            }
121

122
            if (takeLock && !CacheStaticHolder<K, V>.QuickList.TryLock())
1!
123
            {
124
                return;
×
125
            }
126

127
            var removed = 0;
1✔
128
            var store = CacheStaticHolder<K, V>.Store;
1✔
129
            var enumerator = store.GetEnumerator();
1✔
130
            var toRemoveFromStore = trimCount - removedFromQuickList;
1✔
131

132
            while (removed < toRemoveFromStore && enumerator.MoveNext())
13✔
133
            {
134
                store.TryRemove(enumerator.Current.Key, out _);
12✔
135
                removed++;
12✔
136
            }
137

138
            if (takeLock)
1✔
139
            {
140
                CacheStaticHolder<K, V>.QuickList.Release();
1✔
141
            }
142
        }
1✔
143

144
        var trimCount = Math.Max(1, (uint)(CacheStaticHolder<K, V>.Store.Count * (percentage / 100.0)));
2✔
145
        if (trimCount <= Constants.InlineTrimCountThreshold)
2✔
146
        {
147
            ExecuteTrim(trimCount, takeLock: false);
1✔
148
            return true;
1✔
149
        }
150

151
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
152
        ThreadPool.QueueUserWorkItem(static count => ExecuteTrim(count, takeLock: true), trimCount, preferLocal: true);
2✔
153
#elif NETSTANDARD2_0
154
        ThreadPool.QueueUserWorkItem(static count => ExecuteTrim((uint)count, takeLock: true), trimCount);
155
#endif
156
        return false;
1✔
157
    }
158

159
    /// <summary>
160
    /// Suspends automatic eviction. Does not affect already in-flight operations.
161
    /// </summary>
162
    public static void SuspendEviction<K, V>() where K : notnull => CacheStaticHolder<K, V>.EvictionJob.Stop();
3✔
163

164
    /// <summary>
165
    /// Resumes eviction, next iteration will occur after a standard adaptive interval from now.
166
    /// Is a no-op if automatic eviction is disabled.
167
    /// </summary>
168
    public static void ResumeEviction<K, V>() where K : notnull => CacheStaticHolder<K, V>.EvictionJob.Resume();
2✔
169

170
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
171
    internal static void ReportEvictions(uint count)
172
    {
173
        if (!Constants.ConsiderFullGC)
6!
174
        {
175
            return;
×
176
        }
177

178
        Interlocked.Add(ref s_AggregatedEvictionsCount, count);
6✔
179
    }
6✔
180

181
    internal static async Task ExecuteFullEviction<K, V>(bool triggeredByGC) where K : notnull
182
    {
183
        var evictionJob = CacheStaticHolder<K, V>.EvictionJob;
39✔
184
        if (!evictionJob.IsActive)
39✔
185
        {
186
            return;
3✔
187
        }
188

189
    Retry:
190
        if (!evictionJob.FullEvictionLock.Wait(millisecondsTimeout: 0))
37!
191
        {
192
            var activeEviction = evictionJob.ActiveFullEviction;
×
193
            if (activeEviction is null)
×
194
            {
195
                goto Retry;
196
            }
197

198
            await activeEviction;
×
199
            return;
×
200
        }
201

202
        evictionJob.ActiveFullEviction = !triggeredByGC
37✔
203
            ? Task.Run(ImmediateFullEviction<K, V>)
37✔
204
            : StaggeredFullEviction<K, V>();
37✔
205

206
        await evictionJob.ActiveFullEviction;
37✔
207

208
        evictionJob.ActiveFullEviction = null;
31✔
209
        evictionJob.FullEvictionLock.Release();
31✔
210
    }
34✔
211

212
    private static void ImmediateFullEviction<K, V>() where K : notnull
213
    {
214
        var (evictionJob, quickList) = (
2✔
215
            CacheStaticHolder<K, V>.EvictionJob,
2✔
216
            CacheStaticHolder<K, V>.QuickList);
2✔
217

218
        evictionJob.RescheduleConsideringExpiration();
2✔
219

220
        if (quickList.Evict(resize: true))
2!
221
        {
222
            return;
×
223
        }
224

225
#if FASTCACHE_DEBUG
226
        var stopwatch = Stopwatch.StartNew();
227
#endif
228
        var evictedFromCacheStore = EvictFromCacheStore<K, V>();
2✔
229

230
        if (Constants.ConsiderFullGC && evictedFromCacheStore > 0)
2✔
231
        {
232
            ReportEvictions(evictedFromCacheStore);
2✔
233
        }
234

235
#if FASTCACHE_DEBUG
236
        PrintEvicted<K, V>(evictedFromCacheStore, stopwatch.Elapsed);
237
#endif
238

239
        Task.Run(async static () => await ConsiderFullGC<V>());
4✔
240
    }
2✔
241

242
    private static async Task StaggeredFullEviction<K, V>() where K : notnull
243
    {
244
        var (quickList, evictionJob) = (
35✔
245
            CacheStaticHolder<K, V>.QuickList,
35✔
246
            CacheStaticHolder<K, V>.EvictionJob);
35✔
247

248
        if (quickList.Evict())
35✔
249
        {
250
            // When a lot of items are being added to cache, it triggers GC and its callbacks
251
            // which may decrease throughput by accessing the same memory locations
252
            // from multiple threads and wasting CPU time on repeated eviction cycles
253
            // over newly added items which is not profitable to do.
254
            // Delaying lock release for extra (quick list interval / 5) avoids the issue. 
255
            await Task.Delay(Constants.EvictionCooldownDelayOnGC);
22✔
256
            return;
22✔
257
        }
258

259
        evictionJob.EvictionGCNotificationsCount++;
13✔
260
        if (evictionJob.EvictionGCNotificationsCount < 2)
13✔
261
        {
262
            await Task.Delay(Constants.EvictionCooldownDelayOnGC);
8✔
263
            return;
6✔
264
        }
265

266
        await Task.Delay(Constants.CacheStoreEvictionDelay);
5✔
267

268
#if FASTCACHE_DEBUG
269
        var stopwatch = Stopwatch.StartNew();
270
#endif
271
        var evictedFromCacheStore = EvictFromCacheStore<K, V>();
3✔
272

273
        if (Constants.ConsiderFullGC && evictedFromCacheStore > 0)
3✔
274
        {
275
            ReportEvictions(evictedFromCacheStore);
1✔
276
        }
277

278
#if FASTCACHE_DEBUG
279
        PrintEvicted<K, V>(evictedFromCacheStore, stopwatch.Elapsed);
280
#endif
281

282
        await Task.Delay(Constants.EvictionCooldownDelayOnGC);
3✔
283
        evictionJob.EvictionGCNotificationsCount = 0;
3✔
284
    }
29✔
285

286
    private static uint EvictFromCacheStore<K, V>() where K : notnull
287
    {
288
        var (store, quicklist) = (
5✔
289
            CacheStaticHolder<K, V>.Store,
5✔
290
            CacheStaticHolder<K, V>.QuickList);
5✔
291

292
        var evictedCount = store.Count > Constants.ParallelEvictionThreshold
5✔
293
            ? EvictFromCacheStoreParallel<K, V>()
5✔
294
            : EvictFromCacheStoreSingleThreaded<K, V>();
5✔
295

296
        quicklist.PullFromCacheStore();
5✔
297

298
        return evictedCount;
5✔
299
    }
300

301
    private static uint EvictFromCacheStoreSingleThreaded<K, V>() where K : notnull
302
    {
303
        var now = TimeUtils.Now;
2✔
304
        var store = CacheStaticHolder<K, V>.Store;
2✔
305
        uint totalRemoved = 0;
2✔
306

307
        foreach (var (key, value) in store)
5,124✔
308
        {
309
            if (now > value._timestamp)
2,560✔
310
            {
311
                store.TryRemove(key, out _);
1,536✔
312
                totalRemoved++;
1,536✔
313
            }
314
        }
315

316
        return totalRemoved;
2✔
317
    }
318

319
    private static uint EvictFromCacheStoreParallel<K, V>() where K : notnull
320
    {
321
        var now = TimeUtils.Now;
3✔
322
        var store = CacheStaticHolder<K, V>.Store;
3✔
323
        var countBefore = store.Count;
3✔
324

325
        void CheckAndRemove(KeyValuePair<K, CachedInner<V>> kvp)
326
        {
327
            var (key, timestamp) = (kvp.Key, kvp.Value._timestamp);
36,903✔
328

329
            if (now > timestamp)
36,903✔
330
            {
331
                store.TryRemove(key, out _);
4✔
332
            }
333
        }
36,903✔
334

335
        store
3✔
336
            .AsParallel()
3✔
337
            .AsUnordered()
3✔
338
            .ForAll(CheckAndRemove);
3✔
339

340
        // Perform dirty evictions count and discard the result if eviction
341
        // happened to overlap with significant amount of insertions.
342
        // The logic that consumes this value does not require precise reports.
343
        return (uint)Math.Max(countBefore - store.Count, 0);
3✔
344
    }
345

346
    private static async ValueTask ConsiderFullGC<T>()
347
    {
348
        if (!Constants.ConsiderFullGC)
2!
349
        {
350
            return;
×
351
        }
352

353
        if (Interlocked.Read(ref s_AggregatedEvictionsCount) <= Constants.AggregatedGCThreshold)
2!
354
        {
355
            return;
×
356
        }
357

358
        if (!FullGCLock.Wait(millisecondsTimeout: 0))
2✔
359
        {
360
            return;
1✔
361
        }
362

363
        await Task.Delay(Constants.DelayToFullGC);
1✔
364
#if FASTCACHE_DEBUG
365
        var sw = Stopwatch.StartNew();
366
#endif
367

368
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Default, blocking: false);
1✔
369

370
#if FASTCACHE_DEBUG
371
        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");
372
#endif
373
        Interlocked.Exchange(ref s_AggregatedEvictionsCount, 0);
1✔
374

375
        await Task.Delay(Constants.CooldownDelayAfterFullGC);
1✔
376
        FullGCLock.Release();
×
377
    }
1✔
378

379
#if FASTCACHE_DEBUG
380
    private static void PrintEvicted<K, V>(uint count, TimeSpan elapsed) where K : notnull
381
    {
382
        var size = CacheStaticHolder<K, V>.Store.Count;
383
        Console.WriteLine(
384
            $"FastCache: Evicted {count} of {typeof(K).Name}:{typeof(V).Name} from cache store. Size after: {size}, took {elapsed.TotalMilliseconds} ms.");
385
    }
386
#endif
387
}
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