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

neon-sunset / fast-cache / 3885379482

pending completion
3885379482

push

github

GitHub
Bump RangeExtensions from 2.1.0 to 2.1.1 (#30)

375 of 446 branches covered (84.08%)

Branch coverage included in aggregate %.

1618 of 1727 relevant lines covered (93.69%)

1456.5 hits per line

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

80.59
/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
    /// </summary>
22
    public static void QueueFullEviction<K, V>() where K : notnull => QueueFullEviction<K, V>(triggeredByTimer: true);
2✔
23

24
    /// <summary>
25
    /// Remove all cache entries of type Cached[K, V] from the cache
26
    /// </summary>
27
    public static void QueueFullClear<K, V>() where K : notnull
28
    {
29
        ThreadPool.QueueUserWorkItem(async static _ =>
1✔
30
        {
1✔
31
            var evictionJob = CacheStaticHolder<K, V>.EvictionJob;
1✔
32
            await evictionJob.FullEvictionLock.WaitAsync();
1✔
33

1✔
34
#if FASTCACHE_DEBUG
1✔
35
            var countBefore = CacheStaticHolder<K, V>.Store.Count;
1✔
36
#endif
1✔
37

1✔
38
            CacheStaticHolder<K, V>.Store.Clear();
1✔
39
            CacheStaticHolder<K, V>.QuickList.Reset();
1✔
40

1✔
41
            evictionJob.FullEvictionLock.Release();
1✔
42

1✔
43
#if FASTCACHE_DEBUG
1✔
44
            Console.WriteLine(
1✔
45
                $"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}");
1✔
46
#endif
1✔
47
        });
2✔
48
    }
1✔
49

50
    /// <summary>
51
    /// Enumerates all not expired entries currently present in the cache.
52
    /// Cache changes introduced from other threads may not be visible to the enumerator.
53
    /// </summary>
54
    /// <typeparam name="K">Cache entry key type. string, int or (int, int) for multi-key.</typeparam>
55
    /// <typeparam name="V">Cache entry value type</typeparam>
56
    public static IEnumerable<Cached<K, V>> EnumerateEntries<K, V>() where K : notnull
57
    {
58
        foreach (var (key, inner) in CacheStaticHolder<K, V>.Store)
4,098✔
59
        {
60
            if (inner.IsNotExpired())
2,048✔
61
            {
62
                yield return new(key, inner.Value, found: true);
1,024✔
63
            }
64
        }
65
    }
1✔
66

67
    /// <summary>
68
    /// Trims cache store for a given percentage of its size. Will remove at least 1 item.
69
    /// </summary>
70
    /// <typeparam name="K">Cache entry key type. string, int or (int, int) for multi-key.</typeparam>
71
    /// <typeparam name="V">Cache entry value type</typeparam>
72
    /// <param name="percentage"></param>
73
    /// <returns>True: trim is performed inline. False: the count to trim is above threshold and removal is queued to thread pool.</returns>
74
    public static bool Trim<K, V>(double percentage) where K : notnull
75
    {
76
        if (percentage is > 100.0 or <= double.Epsilon or double.NaN)
10✔
77
        {
78
            ThrowHelpers.ArgumentOutOfRange(percentage, nameof(percentage));
8✔
79
        }
80

81
        if (CacheStaticHolder<K, V>.QuickList.InProgress)
2!
82
        {
83
            // Bail out early if the items are being removed via quick list.
84
            return false;
×
85
        }
86

87
        static void ExecuteTrim(uint trimCount, bool takeLock)
88
        {
89
            var removedFromQuickList = CacheStaticHolder<K, V>.QuickList.Trim(trimCount);
2✔
90
            if (removedFromQuickList >= trimCount)
2✔
91
            {
92
                return;
1✔
93
            }
94

95
            if (takeLock && !CacheStaticHolder<K, V>.QuickList.TryLock())
1!
96
            {
97
                return;
×
98
            }
99

100
            var removed = 0;
1✔
101
            var store = CacheStaticHolder<K, V>.Store;
1✔
102
            var enumerator = store.GetEnumerator();
1✔
103
            var toRemoveFromStore = trimCount - removedFromQuickList;
1✔
104

105
            while (removed < toRemoveFromStore && enumerator.MoveNext())
13✔
106
            {
107
                store.TryRemove(enumerator.Current.Key, out _);
12✔
108
                removed++;
12✔
109
            }
110

111
            if (takeLock)
1✔
112
            {
113
                CacheStaticHolder<K, V>.QuickList.Release();
1✔
114
            }
115
        }
1✔
116

117
        var trimCount = Math.Max(1, (uint)(CacheStaticHolder<K, V>.Store.Count * (percentage / 100.0)));
2✔
118
        if (trimCount <= Constants.InlineTrimCountThreshold)
2✔
119
        {
120
            ExecuteTrim(trimCount, takeLock: false);
1✔
121
            return true;
1✔
122
        }
123

124
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
125
        ThreadPool.QueueUserWorkItem(static count => ExecuteTrim(count, takeLock: true), trimCount, preferLocal: false);
2✔
126
#elif NETSTANDARD2_0
127
        ThreadPool.QueueUserWorkItem(static count => ExecuteTrim((uint)count, takeLock: true), trimCount);
128
#endif
129
        return false;
1✔
130
    }
131

132
    /// <summary>
133
    /// Suspends automatic eviction. Does not affect already in-flight operations.
134
    /// </summary>
135
    public static void SuspendEviction<K, V>() where K : notnull => CacheStaticHolder<K, V>.EvictionJob.Stop();
3✔
136

137
    /// <summary>
138
    /// Resumes eviction, next iteration will occur after a standard adaptive interval from now.
139
    /// Is a no-op if automatic eviction is disabled.
140
    /// </summary>
141
    public static void ResumeEviction<K, V>() where K : notnull => CacheStaticHolder<K, V>.EvictionJob.Resume();
2✔
142

143
    internal static void ReportEvictions<T>(uint count)
144
    {
145
        if (!Constants.ConsiderFullGC)
5!
146
        {
147
            return;
×
148
        }
149

150
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
151
        var countTowardsEvictions = RuntimeHelpers.IsReferenceOrContainsReferences<T>();
5✔
152
#elif NETSTANDARD2_0
153
        // Inaccurate but this is an optional feature so this tradeoff is ok for legacy/uncommon tragets.
154
        var countTowardsEvictions = !typeof(T).IsValueType;
155
#endif
156

157
        if (countTowardsEvictions)
5✔
158
        {
159
            Interlocked.Add(ref s_AggregatedEvictionsCount, count);
5✔
160
        }
161
    }
5✔
162

163
    internal static void QueueFullEviction<K, V>(bool triggeredByTimer) where K : notnull
164
    {
165
        if (!CacheStaticHolder<K, V>.EvictionJob.IsActive)
35✔
166
        {
167
            return;
3✔
168
        }
169

170
        if (triggeredByTimer)
33✔
171
        {
172
            ThreadPool.QueueUserWorkItem(static _ =>
2✔
173
            {
2✔
174
                try
2✔
175
                {
2✔
176
                    ImmediateFullEviction<K, V>();
2✔
177
                }
2✔
178
                catch
×
179
                {
2✔
180
#if DEBUG
2✔
181
                    throw;
2✔
182
#endif
2✔
183
                }
×
184
            });
4✔
185
        }
186
        else
187
        {
188
            ThreadPool.QueueUserWorkItem(async static _ =>
31✔
189
            {
31✔
190
                try
31✔
191
                {
31✔
192
                    await StaggeredFullEviction<K, V>();
31✔
193
                }
18✔
194
                catch
×
195
                {
31✔
196
#if DEBUG
31✔
197
                    throw;
31✔
198
#endif
31✔
199
                }
×
200
            });
49✔
201
        }
202
    }
31✔
203

204
    private static void ImmediateFullEviction<K, V>() where K : notnull
205
    {
206
        var evictionJob = CacheStaticHolder<K, V>.EvictionJob;
2✔
207

208
        if (!evictionJob.FullEvictionLock.Wait(millisecondsTimeout: 0))
2!
209
        {
210
            return;
×
211
        }
212

213
        evictionJob.RescheduleConsideringExpiration();
2✔
214

215
        if (CacheStaticHolder<K, V>.QuickList.Evict(resize: true))
2!
216
        {
217
            evictionJob.FullEvictionLock.Release();
×
218
            return;
×
219
        }
220

221
#if FASTCACHE_DEBUG
222
        var stopwatch = Stopwatch.StartNew();
223
#endif
224
        var evictedFromCacheStore = EvictFromCacheStore<K, V>();
2✔
225

226
        if (Constants.ConsiderFullGC && evictedFromCacheStore > 0)
2✔
227
        {
228
            ReportEvictions<V>(evictedFromCacheStore);
2✔
229
        }
230

231
#if FASTCACHE_DEBUG
232
        PrintEvicted<K, V>(evictedFromCacheStore, stopwatch.Elapsed);
233
#endif
234

235
        ThreadPool.QueueUserWorkItem(async static _ => await ConsiderFullGC<V>());
4✔
236

237
        evictionJob.FullEvictionLock.Release();
2✔
238
    }
2✔
239

240
    private static async ValueTask StaggeredFullEviction<K, V>() where K : notnull
241
    {
242
        var evictionJob = CacheStaticHolder<K, V>.EvictionJob;
31✔
243

244
        if (!evictionJob.FullEvictionLock.Wait(millisecondsTimeout: 0))
31✔
245
        {
246
            return;
9✔
247
        }
248

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

261
        evictionJob.EvictionGCNotificationsCount++;
12✔
262
        if (evictionJob.EvictionGCNotificationsCount < 2)
12✔
263
        {
264
            await Task.Delay(Constants.EvictionCooldownDelayOnGC);
7✔
265
            evictionJob.FullEvictionLock.Release();
5✔
266
            return;
5✔
267
        }
268

269
        await Task.Delay(Constants.CacheStoreEvictionDelay);
5✔
270

271
#if FASTCACHE_DEBUG
272
        var stopwatch = Stopwatch.StartNew();
273
#endif
274
        var evictedFromCacheStore = EvictFromCacheStore<K, V>();
×
275

276
        if (Constants.ConsiderFullGC && evictedFromCacheStore > 0)
×
277
        {
278
            ReportEvictions<V>(evictedFromCacheStore);
×
279
        }
280

281
#if FASTCACHE_DEBUG
282
        PrintEvicted<K, V>(evictedFromCacheStore, stopwatch.Elapsed);
283
#endif
284

285
        await Task.Delay(Constants.EvictionCooldownDelayOnGC);
×
286

287
        evictionJob.EvictionGCNotificationsCount = 0;
×
288
        evictionJob.FullEvictionLock.Release();
×
289
    }
18✔
290

291
    private static uint EvictFromCacheStore<K, V>() where K : notnull
292
    {
293
        var evictedCount = CacheStaticHolder<K, V>.Store.Count > Constants.ParallelEvictionThreshold
2!
294
            ? EvictFromCacheStoreParallel<K, V>()
2✔
295
            : EvictFromCacheStoreSingleThreaded<K, V>();
2✔
296

297
        CacheStaticHolder<K, V>.QuickList.PullFromCacheStore();
2✔
298

299
        return evictedCount;
2✔
300
    }
301

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

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

317
        return totalRemoved;
2✔
318
    }
319

320
    private static uint EvictFromCacheStoreParallel<K, V>() where K : notnull
321
    {
322
        var now = TimeUtils.Now;
×
323
        uint totalRemoved = 0;
×
324

325
        void CheckAndRemove(KeyValuePair<K, CachedInner<V>> kvp)
326
        {
327
            var (key, timestamp) = (kvp.Key, kvp.Value._timestamp);
×
328
            ref var count = ref totalRemoved;
×
329

330
            if (now > timestamp)
×
331
            {
332
                CacheStaticHolder<K, V>.Store.TryRemove(key, out _);
×
333
                count++;
×
334
            }
335
        }
×
336

337
        CacheStaticHolder<K, V>.Store
×
338
            .AsParallel()
×
339
            .AsUnordered()
×
340
            .ForAll(CheckAndRemove);
×
341

342
        return totalRemoved;
×
343
    }
344

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

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

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

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

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

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

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

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