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

neon-sunset / fast-cache / 7444307552

08 Jan 2024 07:15AM UTC coverage: 93.08% (-0.3%) from 93.426%
7444307552

Pull #87

github

web-flow
Bump BenchmarkDotNet from 0.13.11 to 0.13.12

Bumps [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) from 0.13.11 to 0.13.12.
- [Release notes](https://github.com/dotnet/BenchmarkDotNet/releases)
- [Commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.11...v0.13.12)

---
updated-dependencies:
- dependency-name: BenchmarkDotNet
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #87: Bump BenchmarkDotNet from 0.13.11 to 0.13.12

356 of 418 branches covered (0.0%)

Branch coverage included in aggregate %.

1796 of 1894 relevant lines covered (94.83%)

1470.48 hits per line

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

76.44
/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
        ThreadPool.QueueUserWorkItem(static count => ExecuteTrim(count, takeLock: true), trimCount, preferLocal: true);
2✔
152

153
        return false;
1✔
154
    }
155

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

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

167
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
168
    internal static void ReportEvictions(uint count)
169
    {
170
        if (!Constants.ConsiderFullGC)
5!
171
        {
172
            return;
×
173
        }
174

175
        Interlocked.Add(ref s_AggregatedEvictionsCount, count);
5✔
176
    }
5✔
177

178
    internal static async Task ExecuteFullEviction<K, V>(bool triggeredByGC) where K : notnull
179
    {
180
        var evictionJob = CacheStaticHolder<K, V>.EvictionJob;
13✔
181
        if (!evictionJob.IsActive)
13✔
182
        {
183
            return;
1✔
184
        }
185

186
    Retry:
187
        if (!evictionJob.FullEvictionLock.Wait(millisecondsTimeout: 0))
13!
188
        {
189
            var activeEviction = evictionJob.ActiveFullEviction;
×
190
            if (activeEviction is null)
×
191
            {
192
                goto Retry;
193
            }
194

195
            await activeEviction;
×
196
            return;
×
197
        }
198

199
        evictionJob.ActiveFullEviction = !triggeredByGC
13✔
200
            ? Task.Run(ImmediateFullEviction<K, V>)
13✔
201
            : StaggeredFullEviction<K, V>();
13✔
202

203
        await evictionJob.ActiveFullEviction;
13✔
204

205
        evictionJob.ActiveFullEviction = null;
13✔
206
        evictionJob.FullEvictionLock.Release();
13✔
207
    }
13✔
208

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

215
        evictionJob.RescheduleConsideringExpiration();
2✔
216

217
        if (quickList.Evict(resize: true))
2!
218
        {
219
            return;
×
220
        }
221

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

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

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

236
        Task.Run(async static () => await ConsiderFullGC<V>());
4✔
237
    }
2✔
238

239
    private static async Task StaggeredFullEviction<K, V>() where K : notnull
240
    {
241
        var (quickList, evictionJob) = (
11✔
242
            CacheStaticHolder<K, V>.QuickList,
11✔
243
            CacheStaticHolder<K, V>.EvictionJob);
11✔
244

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

256
        evictionJob.EvictionGCNotificationsCount++;
5✔
257
        if (evictionJob.EvictionGCNotificationsCount < 2)
5!
258
        {
259
            await Task.Delay(Constants.EvictionCooldownDelayOnGC);
5✔
260
            return;
5✔
261
        }
262

263
        await Task.Delay(Constants.CacheStoreEvictionDelay);
×
264

265
#if FASTCACHE_DEBUG
266
        var stopwatch = Stopwatch.StartNew();
267
#endif
268
        var evictedFromCacheStore = EvictFromCacheStore<K, V>();
×
269

270
        if (Constants.ConsiderFullGC && evictedFromCacheStore > 0)
×
271
        {
272
            ReportEvictions(evictedFromCacheStore);
×
273
        }
274

275
#if FASTCACHE_DEBUG
276
        PrintEvicted<K, V>(evictedFromCacheStore, stopwatch.Elapsed);
277
#endif
278

279
        await Task.Delay(Constants.EvictionCooldownDelayOnGC);
×
280
        evictionJob.EvictionGCNotificationsCount = 0;
×
281
    }
11✔
282

283
    private static uint EvictFromCacheStore<K, V>() where K : notnull
284
    {
285
        var (store, quicklist) = (
2✔
286
            CacheStaticHolder<K, V>.Store,
2✔
287
            CacheStaticHolder<K, V>.QuickList);
2✔
288

289
        var evictedCount = store.Count > Constants.ParallelEvictionThreshold
2!
290
            ? EvictFromCacheStoreParallel<K, V>()
2✔
291
            : EvictFromCacheStoreSingleThreaded<K, V>();
2✔
292

293
        quicklist.PullFromCacheStore();
2✔
294

295
        return evictedCount;
2✔
296
    }
297

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

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

313
        return totalRemoved;
2✔
314
    }
315

316
    private static uint EvictFromCacheStoreParallel<K, V>() where K : notnull
317
    {
318
        var now = TimeUtils.Now;
×
319
        var store = CacheStaticHolder<K, V>.Store;
×
320
        var countBefore = store.Count;
×
321

322
        void CheckAndRemove(KeyValuePair<K, CachedInner<V>> kvp)
323
        {
324
            var (key, timestamp) = (kvp.Key, kvp.Value._timestamp);
×
325

326
            if (now > timestamp)
×
327
            {
328
                store.TryRemove(key, out _);
×
329
            }
330
        }
×
331

332
        store
×
333
            .AsParallel()
×
334
            .AsUnordered()
×
335
            .ForAll(CheckAndRemove);
×
336

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

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

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

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

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

365
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Default, blocking: false);
×
366

367
#if FASTCACHE_DEBUG
368
        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");
369
#endif
370
        Interlocked.Exchange(ref s_AggregatedEvictionsCount, 0);
×
371

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

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

© 2025 Coveralls, Inc