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

bitfaster / BitFaster.Caching / 24424754246

14 Apr 2026 09:52PM UTC coverage: 99.049% (-0.05%) from 99.094%
24424754246

push

github

web-flow
Bump Microsoft.SourceLink.GitHub from 8.0.0 to 10.0.201 (#757)

---
updated-dependencies:
- dependency-name: Microsoft.SourceLink.GitHub
  dependency-version: 10.0.201
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

1271 of 1304 branches covered (97.47%)

Branch coverage included in aggregate %.

5396 of 5427 relevant lines covered (99.43%)

58465387.67 hits per line

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

99.4
/BitFaster.Caching/Lru/ConcurrentLruCore.cs
1
using System;
2
using System.Collections;
3
using System.Collections.Concurrent;
4
using System.Collections.Generic;
5
using System.Diagnostics;
6
using System.Diagnostics.CodeAnalysis;
7
using System.Linq;
8
using System.Runtime.CompilerServices;
9
using System.Threading;
10
using System.Threading.Tasks;
11

12
namespace BitFaster.Caching.Lru
13
{
14
    /// <summary>
15
    /// A pseudo LRU based on the TU-Q eviction policy. The LRU list is composed of 3 segments: hot, warm and cold. 
16
    /// Cost of maintaining segments is amortized across requests. Items are only cycled when capacity is exceeded. 
17
    /// Pure read does not cycle items if all segments are within capacity constraints. There are no global locks. 
18
    /// On cache miss, a new item is added. Tail items in each segment are dequeued, examined, and are either enqueued 
19
    /// or discarded.
20
    /// The TU-Q scheme of hot, warm and cold is similar to that used in MemCached (https://memcached.org/blog/modern-lru/)
21
    /// and OpenBSD (https://flak.tedunangst.com/post/2Q-buffer-cache-algorithm), but does not use a background thread
22
    /// to maintain the internal queues.
23
    /// </summary>
24
    /// <remarks>
25
    /// Each segment has a capacity. When segment capacity is exceeded, items are moved as follows:
26
    /// <list type="number">
27
    ///   <item><description>New items are added to hot, WasAccessed = false.</description></item>
28
    ///   <item><description>When items are accessed, update WasAccessed = true.</description></item>
29
    ///   <item><description>When items are moved WasAccessed is set to false.</description></item>
30
    ///   <item><description>When hot is full, hot tail is moved to either Warm or Cold depending on WasAccessed.</description></item>
31
    ///   <item><description>When warm is full, warm tail is moved to warm head or cold depending on WasAccessed.</description></item>
32
    ///   <item><description>When cold is full, cold tail is moved to warm head or removed from dictionary on depending on WasAccessed.</description></item>
33
    ///</list>
34
    /// </remarks>
35
    public class ConcurrentLruCore<K, V, I, P, T> : ICacheExt<K, V>, IAsyncCacheExt<K, V>, IEnumerable<KeyValuePair<K, V>>
36
        where K : notnull
37
        where I : LruItem<K, V>
38
        where P : struct, IItemPolicy<K, V, I>
39
        where T : struct, ITelemetryPolicy<K, V>
40
    {
41
        private readonly ConcurrentDictionary<K, I> dictionary;
42

43
        private readonly ConcurrentQueue<I> hotQueue;
44
        private readonly ConcurrentQueue<I> warmQueue;
45
        private readonly ConcurrentQueue<I> coldQueue;
46

47
        // maintain count outside ConcurrentQueue, since ConcurrentQueue.Count holds a global lock
48
        private PaddedQueueCount counter;
49

50
        private readonly ICapacityPartition capacity;
51

52
        private readonly P itemPolicy;
53
        private bool isWarm = false;
2,593✔
54

55
        /// <summary>
56
        /// The telemetry policy.
57
        /// </summary>
58
        /// <remarks>
59
        /// Since T is a struct, making it readonly will force the runtime to make defensive copies
60
        /// if mutate methods are called. Therefore, field must be mutable to maintain count.
61
        /// </remarks>
62
        protected T telemetryPolicy;
63

64
        /// <summary>
65
        /// Initializes a new instance of the ConcurrentLruCore class with the specified concurrencyLevel, capacity, equality comparer, item policy and telemetry policy.
66
        /// </summary>
67
        /// <param name="concurrencyLevel">The concurrency level.</param>
68
        /// <param name="capacity">The capacity.</param>
69
        /// <param name="comparer">The equality comparer.</param>
70
        /// <param name="itemPolicy">The item policy.</param>
71
        /// <param name="telemetryPolicy">The telemetry policy.</param>
72
        /// <exception cref="ArgumentNullException"></exception>
73
        public ConcurrentLruCore(
2,593✔
74
            int concurrencyLevel,
2,593✔
75
            ICapacityPartition capacity,
2,593✔
76
            IEqualityComparer<K> comparer,
2,593✔
77
            P itemPolicy,
2,593✔
78
            T telemetryPolicy)
2,593✔
79
        {
2,593✔
80
            if (capacity == null)
2,593✔
81
                Throw.ArgNull(ExceptionArgument.capacity);
5✔
82

83
            if (comparer == null)
2,588✔
84
                Throw.ArgNull(ExceptionArgument.comparer);
5✔
85

86
            capacity.Validate();
2,583✔
87
            this.capacity = capacity;
2,578✔
88

89
            this.hotQueue = new ConcurrentQueue<I>();
2,578✔
90
            this.warmQueue = new ConcurrentQueue<I>();
2,578✔
91
            this.coldQueue = new ConcurrentQueue<I>();
2,578✔
92

93
            int dictionaryCapacity = ConcurrentDictionarySize.Estimate(this.Capacity);
2,578✔
94

95
            this.dictionary = new ConcurrentDictionary<K, I>(concurrencyLevel, dictionaryCapacity, comparer);
2,578✔
96
            this.itemPolicy = itemPolicy;
2,568✔
97
            this.telemetryPolicy = telemetryPolicy;
2,568✔
98
            this.telemetryPolicy.SetEventSource(this);
2,568✔
99
        }
2,568✔
100

101
        // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/
102
        ///<inheritdoc/>
103
        public int Count => this.dictionary.Where(i => !itemPolicy.ShouldDiscard(i.Value)).Count();
17,459,494✔
104

105
        ///<inheritdoc/>
106
        public int Capacity => this.capacity.Hot + this.capacity.Warm + this.capacity.Cold;
2,000,004,063✔
107

108
        ///<inheritdoc/>
109
        public Optional<ICacheMetrics> Metrics => CreateMetrics(this);
165✔
110

111
        ///<inheritdoc/>
112
        public Optional<ICacheEvents<K, V>> Events => CreateEvents(this);
1,352✔
113

114
        ///<inheritdoc/>
115
        public CachePolicy Policy => CreatePolicy(this);
346✔
116

117
        /// <summary>
118
        /// Gets the number of hot items.
119
        /// </summary>
120
        public int HotCount => Volatile.Read(ref this.counter.hot);
1,586,215✔
121

122
        /// <summary>
123
        /// Gets the number of warm items.
124
        /// </summary>
125
        public int WarmCount => Volatile.Read(ref this.counter.warm);
1,586,215✔
126

127
        /// <summary>
128
        /// Gets the number of cold items.
129
        /// </summary>
130
        public int ColdCount => Volatile.Read(ref this.counter.cold);
1,586,220✔
131

132
        /// <summary>
133
        /// Gets a collection containing the keys in the cache.
134
        /// </summary>
135
        public ICollection<K> Keys => this.dictionary.Keys;
1,050✔
136

137
#if NET9_0_OR_GREATER
138
        /// <inheritdoc/>
139
        public IEqualityComparer<K> Comparer => this.dictionary.Comparer;
22✔
140
#endif
141

142
        /// <summary>Returns an enumerator that iterates through the cache.</summary>
143
        /// <returns>An enumerator for the cache.</returns>
144
        /// <remarks>
145
        /// The enumerator returned from the cache is safe to use concurrently with
146
        /// reads and writes, however it does not represent a moment-in-time snapshot.  
147
        /// The contents exposed through the enumerator may contain modifications
148
        /// made after <see cref="GetEnumerator"/> was called.
149
        /// </remarks>
150
        public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
151
        {
315✔
152
            foreach (var kvp in this.dictionary)
1,620✔
153
            {
340✔
154
                if (!itemPolicy.ShouldDiscard(kvp.Value))
340✔
155
                {
325✔
156
                    yield return new KeyValuePair<K, V>(kvp.Key, kvp.Value.Value);
325✔
157
                }
320✔
158
            }
335✔
159
        }
310✔
160

161
        ///<inheritdoc/>
162
        public bool TryGet(K key, [MaybeNullWhen(false)] out V value)
163
        {
2,243,496,096✔
164
            if (dictionary.TryGetValue(key, out var item))
2,243,496,096✔
165
            {
2,013,221,825✔
166
                return GetOrDiscard(item, out value);
2,013,221,825✔
167
            }
168

169
            value = default;
230,274,271✔
170
            this.telemetryPolicy.IncrementMiss();
230,274,271✔
171
            return false;
230,274,271✔
172
        }
2,243,496,096✔
173

174
        // AggressiveInlining forces the JIT to inline policy.ShouldDiscard(). For LRU policy 
175
        // the first branch is completely eliminated due to JIT time constant propogation.
176
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
177
        private bool GetOrDiscard(I item, [MaybeNullWhen(false)] out V value)
178
        {
2,013,504,142✔
179
            if (this.itemPolicy.ShouldDiscard(item))
2,013,504,142✔
180
            {
4,497,247✔
181
                this.Move(item, ItemDestination.Remove, ItemRemovedReason.Evicted);
4,497,247✔
182
                this.telemetryPolicy.IncrementMiss();
4,497,247✔
183
                value = default;
4,497,247✔
184
                return false;
4,497,247✔
185
            }
186

187
            value = item.Value;
2,009,006,895✔
188

189
            this.itemPolicy.Touch(item);
2,009,006,895✔
190
            this.telemetryPolicy.IncrementHit();
2,009,006,895✔
191
            return true;
2,009,006,895✔
192
        }
2,013,504,142✔
193

194
        private bool TryAdd(K key, V value)
195
        {
253,924,743✔
196
            var newItem = this.itemPolicy.CreateItem(key, value);
253,924,743✔
197

198
            if (this.dictionary.TryAdd(key, newItem))
253,924,743✔
199
            {
247,499,826✔
200
                this.hotQueue.Enqueue(newItem);
247,499,826✔
201
                Cycle(Interlocked.Increment(ref counter.hot));
247,499,826✔
202
                return true;
247,499,826✔
203
            }
204

205
            Disposer<V>.Dispose(newItem.Value);
6,424,917✔
206
            return false;
6,424,917✔
207
        }
253,924,743✔
208

209
        ///<inheritdoc/>
210
        public V GetOrAdd(K key, Func<K, V> valueFactory)
211
        {
2,171,627,988✔
212
            while (true)
2,177,275,214✔
213
            {
2,177,275,214✔
214
                if (this.TryGet(key, out var value))
2,177,275,214✔
215
                {
2,001,962,904✔
216
                    return value;
2,001,962,904✔
217
                }
218

219
                // The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
220
                value = valueFactory(key);
175,312,310✔
221

222
                if (TryAdd(key, value))
175,312,310✔
223
                {
169,665,084✔
224
                    return value;
169,665,084✔
225
                }
226
            }
5,647,226✔
227
        }
2,171,627,988✔
228

229
        /// <summary>
230
        /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the 
231
        /// existing value if the key already exists.
232
        /// </summary>
233
        /// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
234
        /// <param name="key">The key of the element to add.</param>
235
        /// <param name="valueFactory">The factory function used to generate a value for the key.</param>
236
        /// <param name="factoryArgument">An argument value to pass into valueFactory.</param>
237
        /// <returns>The value for the key. This will be either the existing value for the key if the key is already 
238
        /// in the cache, or the new value if the key was not in the cache.</returns>
239
        public V GetOrAdd<TArg>(K key, Func<K, TArg, V> valueFactory, TArg factoryArgument)
240
        {
20,000,015✔
241
            while (true)
20,252,267✔
242
            {
20,252,267✔
243
                if (this.TryGet(key, out var value))
20,252,267✔
244
                {
722,567✔
245
                    return value;
722,567✔
246
                }
247

248
                // The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
249
                value = valueFactory(key, factoryArgument);
19,529,700✔
250

251
                if (TryAdd(key, value))
19,529,700✔
252
                {
19,277,448✔
253
                    return value;
19,277,448✔
254
                }
255
            }
252,252✔
256
        }
20,000,015✔
257

258
        ///<inheritdoc/>
259
        public async ValueTask<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
260
        {
20,000,380✔
261
            while (true)
20,178,710✔
262
            {
20,178,710✔
263
                if (this.TryGet(key, out var value))
20,178,710✔
264
                {
524,873✔
265
                    return value;
524,873✔
266
                }
267

268
                // The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
269
                // This is identical logic in ConcurrentDictionary.GetOrAdd method.
270
                value = await valueFactory(key).ConfigureAwait(false);
19,653,837✔
271

272
                if (TryAdd(key, value))
19,653,822✔
273
                {
19,475,492✔
274
                    return value;
19,475,492✔
275
                }
276
            }
178,330✔
277
        }
20,000,365✔
278

279
        /// <summary>
280
        /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the 
281
        /// existing value if the key already exists.
282
        /// </summary>
283
        /// <typeparam name="TArg">The type of an argument to pass into valueFactory.</typeparam>
284
        /// <param name="key">The key of the element to add.</param>
285
        /// <param name="valueFactory">The factory function used to asynchronously generate a value for the key.</param>
286
        /// <param name="factoryArgument">An argument value to pass into valueFactory.</param>
287
        /// <returns>A task that represents the asynchronous GetOrAdd operation.</returns>
288
        public async ValueTask<V> GetOrAddAsync<TArg>(K key, Func<K, TArg, Task<V>> valueFactory, TArg factoryArgument)
289
        {
20,000,345✔
290
            while (true)
20,211,653✔
291
            {
20,211,653✔
292
                if (this.TryGet(key, out var value))
20,211,653✔
293
                {
559,120✔
294
                    return value;
559,120✔
295
                }
296

297
                // The value factory may be called concurrently for the same key, but the first write to the dictionary wins.
298
                value = await valueFactory(key, factoryArgument).ConfigureAwait(false);
19,652,533✔
299

300
                if (TryAdd(key, value))
19,652,533✔
301
                {
19,441,225✔
302
                    return value;
19,441,225✔
303
                }
304
            }
211,308✔
305
        }
20,000,345✔
306

307
        /// <summary>
308
        /// Attempts to remove the specified key value pair.
309
        /// </summary>
310
        /// <param name="item">The item to remove.</param>
311
        /// <returns>true if the item was removed successfully; otherwise, false.</returns>
312
        public bool TryRemove(KeyValuePair<K, V> item)
313
        {
23,556,130✔
314
            if (this.dictionary.TryGetValue(item.Key, out var existing))
23,556,130✔
315
            {
2,747,201✔
316
                lock (existing)
2,747,201✔
317
                {
2,747,201✔
318
                    if (EqualityComparer<V>.Default.Equals(existing.Value, item.Value))
2,747,201✔
319
                    {
137,252✔
320
                        var kvp = new KeyValuePair<K, I>(item.Key, existing);
137,252✔
321
#if NET6_0_OR_GREATER
322
                    if (this.dictionary.TryRemove(kvp))
124,002✔
323
#else
324
                        // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
325
                        if (((ICollection<KeyValuePair<K, I>>)this.dictionary).Remove(kvp))
13,250✔
326
#endif
327
                        {
132,524✔
328
                            OnRemove(item.Key, kvp.Value, ItemRemovedReason.Removed);
132,524✔
329
                            return true;
132,524✔
330
                        }
331
                    }
4,728✔
332
                }
2,614,677✔
333

334
                // it existed, but we couldn't remove - this means value was replaced afer the TryGetValue (a race)
335
            }
2,614,677✔
336

337
            return false;
23,423,606✔
338
        }
23,556,130✔
339

340
        /// <summary>
341
        /// Attempts to remove and return the value that has the specified key.
342
        /// </summary>
343
        /// <param name="key">The key of the element to remove.</param>
344
        /// <param name="value">When this method returns, contains the object removed, or the default value of the value type if key does not exist.</param>
345
        /// <returns>true if the object was removed successfully; otherwise, false.</returns>
346
        public bool TryRemove(K key, [MaybeNullWhen(false)] out V value)
347
        {
20,000,298✔
348
            if (this.dictionary.TryRemove(key, out var item))
20,000,298✔
349
            {
30,935✔
350
                OnRemove(key, item, ItemRemovedReason.Removed);
30,935✔
351
                value = item.Value;
30,935✔
352
                return true;
30,935✔
353
            }
354

355
            value = default;
19,969,363✔
356
            return false;
19,969,363✔
357
        }
20,000,298✔
358

359
        ///<inheritdoc/>
360
        public bool TryRemove(K key)
361
        {
20,000,220✔
362
            return TryRemove(key, out _);
20,000,220✔
363
        }
20,000,220✔
364

365
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
366
        private void OnRemove(K key, I item, ItemRemovedReason reason)
367
        {
266,548,952✔
368
            // Mark as not accessed, it will later be cycled out of the queues because it can never be fetched 
369
            // from the dictionary. Note: Hot/Warm/Cold count will reflect the removed item until it is cycled 
370
            // from the queue.
371
            item.WasAccessed = false;
266,548,952✔
372
            item.WasRemoved = true;
266,548,952✔
373

374
            this.telemetryPolicy.OnItemRemoved(key, item.Value, reason);
266,548,952✔
375

376
            // serialize dispose (common case dispose not thread safe)
377
            lock (item)
266,548,952✔
378
            {
266,548,952✔
379
                Disposer<V>.Dispose(item.Value);
266,548,952✔
380
            }
266,548,952✔
381
        }
266,548,952✔
382

383
        ///<inheritdoc/>
384
        ///<remarks>Note: Calling this method does not affect LRU order.</remarks>
385
        public bool TryUpdate(K key, V value)
386
        {
63,352,658✔
387
            if (this.dictionary.TryGetValue(key, out var existing))
63,352,658✔
388
            {
5,055,827✔
389
                return this.TryUpdateValue(existing, value);
5,055,827✔
390
            }
391

392
            return false;
58,296,831✔
393
        }
63,352,658✔
394

395
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
396
        private bool TryUpdateValue(I existing, V value)
397
        {
5,055,833✔
398
            lock (existing)
5,055,833✔
399
            {
5,055,833✔
400
                if (!existing.WasRemoved)
5,055,833✔
401
                {
4,947,155✔
402
                    V oldValue = existing.Value;
4,947,155✔
403

404
                    existing.Value = value;
4,947,155✔
405

406
                    this.itemPolicy.Update(existing);
4,947,155✔
407
                    // backcompat: remove conditional compile
408
#if NETCOREAPP3_0_OR_GREATER
409
                    this.telemetryPolicy.OnItemUpdated(existing.Key, oldValue, existing.Value);
4,947,155✔
410
#endif
411
                    Disposer<V>.Dispose(oldValue);
4,947,155✔
412

413
                    return true;
4,947,155✔
414
                }
415
            }
108,678✔
416

417
            return false;
108,678✔
418
        }
5,055,833✔
419

420
        ///<inheritdoc/>
421
        ///<remarks>Note: Updates to existing items do not affect LRU order. Added items are at the top of the LRU.</remarks>
422
        public void AddOrUpdate(K key, V value)
423
        {
23,245,181✔
424
            while (true)
23,352,533✔
425
            {
23,352,533✔
426
                // first, try to update
427
                if (this.TryUpdate(key, value))
23,352,533✔
428
                {
4,162,319✔
429
                    return;
4,162,319✔
430
                }
431

432
                // then try add
433
                var newItem = this.itemPolicy.CreateItem(key, value);
19,190,214✔
434

435
                if (this.dictionary.TryAdd(key, newItem))
19,190,214✔
436
                {
19,082,862✔
437
                    this.hotQueue.Enqueue(newItem);
19,082,862✔
438
                    Cycle(Interlocked.Increment(ref counter.hot));
19,082,862✔
439
                    return;
19,082,862✔
440
                }
441

442
                // if both update and add failed there was a race, try again
443
            }
107,352✔
444
        }
23,245,181✔
445

446
        ///<inheritdoc/>
447
        public void Clear()
448
        {
1,562,660✔
449
            // don't overlap Clear/Trim/TrimExpired
450
            lock (this.dictionary)
1,562,660✔
451
            {
1,562,660✔
452
                // evaluate queue count, remove everything including items removed from the dictionary but
453
                // not the queues. This also avoids the expensive o(n) no lock count, or locking the dictionary.
454
                int queueCount = this.HotCount + this.WarmCount + this.ColdCount;
1,562,660✔
455
                this.TrimLiveItems(itemsRemoved: 0, queueCount, ItemRemovedReason.Cleared);
1,562,660✔
456
            }
1,562,660✔
457
        }
1,562,660✔
458

459
        /// <summary>
460
        /// Trim the specified number of items from the cache. Removes all discardable items per IItemPolicy.ShouldDiscard(), then 
461
        /// itemCount-discarded items in LRU order, if any.
462
        /// </summary>
463
        /// <param name="itemCount">The number of items to remove.</param>
464
        /// <returns>The number of items removed from the cache.</returns>
465
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="itemCount"/> is less than 0./</exception>
466
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="itemCount"/> is greater than capacity./</exception>
467
        /// <remarks>
468
        /// Note: Trim affects LRU order. Calling Trim resets the internal accessed status of items.
469
        /// </remarks>
470
        public void Trim(int itemCount)
471
        {
305✔
472
            int capacity = this.Capacity;
305✔
473

474
            if (itemCount < 1 || itemCount > capacity)
305✔
475
                Throw.ArgOutOfRange(nameof(itemCount), "itemCount must be greater than or equal to one, and less than the capacity of the cache.");
10✔
476

477
            // clamp itemCount to number of items actually in the cache
478
            itemCount = Math.Min(itemCount, this.HotCount + this.WarmCount + this.ColdCount);
295✔
479

480
            // don't overlap Clear/Trim/TrimExpired
481
            lock (this.dictionary)
295✔
482
            {
295✔
483
                // first scan each queue for discardable items and remove them immediately. Note this can remove > itemCount items.
484
                int itemsRemoved = TrimAllDiscardedItems();
295✔
485

486
                TrimLiveItems(itemsRemoved, itemCount, ItemRemovedReason.Trimmed);
295✔
487
            }
295✔
488
        }
295✔
489

490
        private void TrimExpired()
491
        {
46✔
492
            if (this.itemPolicy.CanDiscard())
46✔
493
            {
46✔
494
                lock (this.dictionary)
46✔
495
                {
46✔
496
                    this.TrimAllDiscardedItems();
46✔
497
                }
46✔
498
            }
46✔
499
        }
46✔
500

501
        /// <summary>
502
        /// Trim discarded items from all queues.
503
        /// </summary>
504
        /// <returns>The number of items removed.</returns>
505
        // backcompat: make internal
506
        protected int TrimAllDiscardedItems()
507
        {
341✔
508
            // don't overlap Clear/Trim/TrimExpired
509
            lock (this.dictionary)
341✔
510
            {
341✔
511
                int RemoveDiscardableItems(ConcurrentQueue<I> q, ref int queueCounter)
512
                {
1,023✔
513
                    int itemsRemoved = 0;
1,023✔
514
                    int localCount = queueCounter;
1,023✔
515

516
                    for (int i = 0; i < localCount; i++)
6,234✔
517
                    {
2,094✔
518
                        if (q.TryDequeue(out var item))
2,094✔
519
                        {
2,094✔
520
                            if (this.itemPolicy.ShouldDiscard(item))
2,094✔
521
                            {
286✔
522
                                Interlocked.Decrement(ref queueCounter);
286✔
523
                                this.Move(item, ItemDestination.Remove, ItemRemovedReason.Trimmed);
286✔
524
                                itemsRemoved++;
286✔
525
                            }
286✔
526
                            else if (item.WasRemoved)
1,808✔
527
                            {
30✔
528
                                Interlocked.Decrement(ref queueCounter);
30✔
529
                            }
30✔
530
                            else
531
                            {
1,778✔
532
                                q.Enqueue(item);
1,778✔
533
                            }
1,778✔
534
                        }
2,094✔
535
                    }
2,094✔
536

537
                    return itemsRemoved;
1,023✔
538
                }
1,023✔
539

540
                int coldRem = RemoveDiscardableItems(coldQueue, ref this.counter.cold);
341✔
541
                int warmRem = RemoveDiscardableItems(warmQueue, ref this.counter.warm);
341✔
542
                int hotRem = RemoveDiscardableItems(hotQueue, ref this.counter.hot);
341✔
543

544
                if (warmRem > 0)
341✔
545
                {
41✔
546
                    Volatile.Write(ref this.isWarm, false);
41✔
547
                }
41✔
548

549
                return coldRem + warmRem + hotRem;
341✔
550
            }
551
        }
341✔
552

553
        private void TrimLiveItems(int itemsRemoved, int itemCount, ItemRemovedReason reason)
554
        {
1,562,955✔
555
            // When items are touched, they are moved to warm by cycling. Therefore, to guarantee 
556
            // that we can remove itemCount items, we must cycle (2 * capacity.Warm) + capacity.Hot times.
557
            // If clear is called during trimming, it would be possible to get stuck in an infinite
558
            // loop here. The warm + hot limit also guards against this case.
559
            int trimWarmAttempts = 0;
1,562,955✔
560
            int maxWarmHotAttempts = (this.capacity.Warm * 2) + this.capacity.Hot;
1,562,955✔
561

562
            while (itemsRemoved < itemCount && trimWarmAttempts < maxWarmHotAttempts)
15,969,652✔
563
            {
14,406,697✔
564
                if (Volatile.Read(ref this.counter.cold) > 0)
14,406,697✔
565
                {
11,073,807✔
566
                    if (TryRemoveCold(reason) == (ItemDestination.Remove, 0))
11,073,807✔
567
                    {
11,073,205✔
568
                        itemsRemoved++;
11,073,205✔
569
                        trimWarmAttempts = 0;
11,073,205✔
570
                    }
11,073,205✔
571
                    else
572
                    {
602✔
573
                        TrimWarmOrHot(reason);
602✔
574
                    }
602✔
575
                }
11,073,807✔
576
                else
577
                {
3,332,890✔
578
                    TrimWarmOrHot(reason);
3,332,890✔
579
                    trimWarmAttempts++;
3,332,890✔
580
                }
3,332,890✔
581
            }
14,406,697✔
582

583
            if (Volatile.Read(ref this.counter.warm) < this.capacity.Warm)
1,562,955✔
584
            {
883,018✔
585
                Volatile.Write(ref this.isWarm, false);
883,018✔
586
            }
883,018✔
587
        }
1,562,955✔
588

589
        private void TrimWarmOrHot(ItemRemovedReason reason)
590
        {
3,333,492✔
591
            if (Volatile.Read(ref this.counter.warm) > 0)
3,333,492✔
592
            {
1,159,142✔
593
                CycleWarmUnchecked(reason);
1,159,142✔
594
            }
1,159,142✔
595
            else if (Volatile.Read(ref this.counter.hot) > 0)
2,174,350✔
596
            {
2,172,839✔
597
                CycleHotUnchecked(reason);
2,172,839✔
598
            }
2,172,839✔
599
        }
3,333,492✔
600

601
        private void Cycle(int hotCount)
602
        {
266,582,688✔
603
            if (isWarm)
266,582,688✔
604
            {
262,829,911✔
605
                (var dest, var count) = CycleHot(hotCount);
262,829,911✔
606

607
                int cycles = 0;
262,829,911✔
608
                while (cycles++ < 3 && dest != ItemDestination.Remove)
556,674,633✔
609
                {
293,844,722✔
610
                    if (dest == ItemDestination.Warm)
293,844,722✔
611
                    {
33,988,210✔
612
                        (dest, count) = CycleWarm(count);
33,988,210✔
613
                    }
33,988,210✔
614
                    else if (dest == ItemDestination.Cold)
259,856,512✔
615
                    {
259,856,512✔
616
                        (dest, count) = CycleCold(count);
259,856,512✔
617
                    }
259,856,512✔
618
                }
293,844,722✔
619

620
                // If nothing was removed yet, constrain the size of warm and cold by discarding the coldest item.
621
                if (dest != ItemDestination.Remove)
262,829,911✔
622
                {
5,228,068✔
623
                    if (dest == ItemDestination.Warm && count > this.capacity.Warm)
5,228,068✔
624
                    {
2,912,115✔
625
                        count = LastWarmToCold();
2,912,115✔
626
                    }
2,912,115✔
627

628
                    ConstrainCold(count, ItemRemovedReason.Evicted);
5,228,068✔
629
                }
5,228,068✔
630
            }
262,829,911✔
631
            else
632
            {
3,752,777✔
633
                // fill up the warm queue with new items until warm is full.
634
                // else during warmup the cache will only use the hot + cold queues until any item is requested twice.
635
                CycleDuringWarmup(hotCount);
3,752,777✔
636
            }
3,752,777✔
637
        }
266,582,688✔
638

639
        [MethodImpl(MethodImplOptions.NoInlining)]
640
        private void CycleDuringWarmup(int hotCount)
641
        {
3,752,777✔
642
            // do nothing until hot is full
643
            if (hotCount > this.capacity.Hot)
3,752,777✔
644
            {
1,587,163✔
645
                Interlocked.Decrement(ref this.counter.hot);
1,587,163✔
646

647
                if (this.hotQueue.TryDequeue(out var item))
1,587,163✔
648
                {
1,587,139✔
649
                    // special case: removed during warmup
650
                    if (item.WasRemoved)
1,587,139✔
651
                    {
108,686✔
652
                        return;
108,686✔
653
                    }
654

655
                    int count = this.Move(item, ItemDestination.Warm, ItemRemovedReason.Evicted);
1,478,453✔
656

657
                    // if warm is now full, overflow to cold and mark as warm
658
                    if (count > this.capacity.Warm)
1,478,453✔
659
                    {
319,281✔
660
                        Volatile.Write(ref this.isWarm, true);
319,281✔
661
                        count = LastWarmToCold();
319,281✔
662
                        ConstrainCold(count, ItemRemovedReason.Evicted);
319,281✔
663
                    }
319,281✔
664
                }
1,478,453✔
665
                else
666
                {
24✔
667
                    Interlocked.Increment(ref this.counter.hot);
24✔
668
                }
24✔
669
            }
1,478,477✔
670
        }
3,752,777✔
671

672
        private (ItemDestination, int) CycleHot(int hotCount)
673
        {
262,829,911✔
674
            if (hotCount > this.capacity.Hot)
262,829,911✔
675
            {
262,813,914✔
676
                return CycleHotUnchecked(ItemRemovedReason.Evicted);
262,813,914✔
677
            }
678

679
            return (ItemDestination.Remove, 0);
15,997✔
680
        }
262,829,911✔
681

682
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
683
        private (ItemDestination, int) CycleHotUnchecked(ItemRemovedReason removedReason)
684
        {
264,986,753✔
685
            Interlocked.Decrement(ref this.counter.hot);
264,986,753✔
686

687
            if (this.hotQueue.TryDequeue(out var item))
264,986,753✔
688
            {
264,986,713✔
689
                var where = this.itemPolicy.RouteHot(item);
264,986,713✔
690
                return (where, this.Move(item, where, removedReason));
264,986,713✔
691
            }
692
            else
693
            {
40✔
694
                Interlocked.Increment(ref this.counter.hot);
40✔
695
                return (ItemDestination.Remove, 0);
40✔
696
            }
697
        }
264,986,753✔
698

699
        private (ItemDestination, int) CycleWarm(int count)
700
        {
33,988,210✔
701
            if (count > this.capacity.Warm)
33,988,210✔
702
            {
33,973,690✔
703
                return CycleWarmUnchecked(ItemRemovedReason.Evicted);
33,973,690✔
704
            }
705

706
            return (ItemDestination.Remove, 0);
14,520✔
707
        }
33,988,210✔
708

709
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
710
        private (ItemDestination, int) CycleWarmUnchecked(ItemRemovedReason removedReason)
711
        {
35,132,832✔
712
            int wc = Interlocked.Decrement(ref this.counter.warm);
35,132,832✔
713

714
            if (this.warmQueue.TryDequeue(out var item))
35,132,832!
715
            {
35,132,832✔
716
                if (item.WasRemoved)
35,132,832✔
717
                {
959,498✔
718
                    return (ItemDestination.Remove, 0);
959,498✔
719
                }
720

721
                var where = this.itemPolicy.RouteWarm(item);
34,173,334✔
722

723
                // When the warm queue is full, we allow an overflow of 1 item before redirecting warm items to cold.
724
                // This only happens when hit rate is high, in which case we can consider all items relatively equal in
725
                // terms of which was least recently used.
726
                if (where == ItemDestination.Warm && wc <= this.capacity.Warm)
34,173,334✔
727
                {
8,684,764✔
728
                    return (ItemDestination.Warm, this.Move(item, where, removedReason));
8,684,764✔
729
                }
730
                else
731
                {
25,488,570✔
732
                    return (ItemDestination.Cold, this.Move(item, ItemDestination.Cold, removedReason));
25,488,570✔
733
                }
734
            }
735
            else
736
            {
×
737
                Interlocked.Increment(ref this.counter.warm);
×
738
                return (ItemDestination.Remove, 0);
×
739
            }
740
        }
35,132,832✔
741

742
        private (ItemDestination, int) CycleCold(int count)
743
        {
259,856,512✔
744
            if (count > this.capacity.Cold)
259,856,512✔
745
            {
252,355,346✔
746
                return TryRemoveCold(ItemRemovedReason.Evicted);
252,355,346✔
747
            }
748

749
            return (ItemDestination.Remove, 0);
7,501,166✔
750
        }
259,856,512✔
751

752
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
753
        private (ItemDestination, int) TryRemoveCold(ItemRemovedReason removedReason)
754
        {
263,429,153✔
755
            Interlocked.Decrement(ref this.counter.cold);
263,429,153✔
756

757
            if (this.coldQueue.TryDequeue(out var item))
263,429,153✔
758
            {
263,428,570✔
759
                var where = this.itemPolicy.RouteCold(item);
263,428,570✔
760

761
                if (where == ItemDestination.Warm && Volatile.Read(ref this.counter.warm) <= this.capacity.Warm)
263,428,570✔
762
                {
4,366,779✔
763
                    return (ItemDestination.Warm, this.Move(item, where, removedReason));
4,366,779✔
764
                }
765
                else
766
                {
259,061,791✔
767
                    this.Move(item, ItemDestination.Remove, removedReason);
259,061,791✔
768
                    return (ItemDestination.Remove, 0);
259,061,791✔
769
                }
770
            }
771
            else
772
            {
583✔
773
                return (ItemDestination.Cold, Interlocked.Increment(ref this.counter.cold));
583✔
774
            }
775
        }
263,429,153✔
776

777
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
778
        private int LastWarmToCold()
779
        {
3,231,396✔
780
            Interlocked.Decrement(ref this.counter.warm);
3,231,396✔
781

782
            if (this.warmQueue.TryDequeue(out var item))
3,231,396✔
783
            {
3,231,375✔
784
                var destination = item.WasRemoved ? ItemDestination.Remove : ItemDestination.Cold;
3,231,375✔
785
                return this.Move(item, destination, ItemRemovedReason.Evicted);
3,231,375✔
786
            }
787
            else
788
            {
21✔
789
                Interlocked.Increment(ref this.counter.warm);
21✔
790
                return 0;
21✔
791
            }
792
        }
3,231,396✔
793

794
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
795
        private void ConstrainCold(int coldCount, ItemRemovedReason removedReason)
796
        {
5,547,349✔
797
            if (coldCount > this.capacity.Cold && this.coldQueue.TryDequeue(out var item))
5,547,349✔
798
            {
5,209,566✔
799
                Interlocked.Decrement(ref this.counter.cold);
5,209,566✔
800
                this.Move(item, ItemDestination.Remove, removedReason);
5,209,566✔
801
            }
5,209,566✔
802
        }
5,547,349✔
803

804
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
805
        private int Move(I item, ItemDestination where, ItemRemovedReason removedReason)
806
        {
577,005,544✔
807
            item.WasAccessed = false;
577,005,544✔
808

809
            switch (where)
577,005,544✔
810
            {
811
                case ItemDestination.Warm:
812
                    this.warmQueue.Enqueue(item);
38,383,169✔
813
                    return Interlocked.Increment(ref this.counter.warm);
38,383,169✔
814
                case ItemDestination.Cold:
815
                    this.coldQueue.Enqueue(item);
268,645,392✔
816
                    return Interlocked.Increment(ref this.counter.cold);
268,645,392✔
817
                case ItemDestination.Remove:
818

819
                    var kvp = new KeyValuePair<K, I>(item.Key, item);
269,976,983✔
820

821
#if NET6_0_OR_GREATER
822
                    if (this.dictionary.TryRemove(kvp))
219,430,541✔
823
#else
824
                    // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
825
                    if (((ICollection<KeyValuePair<K, I>>)this.dictionary).Remove(kvp))
50,546,442✔
826
#endif
827
                    {
266,377,840✔
828
                        OnRemove(item.Key, item, removedReason);
266,377,840✔
829
                    }
266,377,840✔
830
                    break;
269,976,983✔
831
            }
832

833
            return 0;
269,976,983✔
834
        }
577,005,544✔
835

836
        /// <summary>Returns an enumerator that iterates through the cache.</summary>
837
        /// <returns>An enumerator for the cache.</returns>
838
        /// <remarks>
839
        /// The enumerator returned from the cache is safe to use concurrently with
840
        /// reads and writes, however it does not represent a moment-in-time snapshot.  
841
        /// The contents exposed through the enumerator may contain modifications
842
        /// made after <see cref="GetEnumerator"/> was called.
843
        /// </remarks>
844
        IEnumerator IEnumerable.GetEnumerator()
845
        {
15✔
846
            return ((ConcurrentLruCore<K, V, I, P, T>)this).GetEnumerator();
15✔
847
        }
15✔
848

849
#if DEBUG
850
        /// <summary>
851
        /// Format the LRU as a string by converting all the keys to strings.
852
        /// </summary>
853
        /// <returns>The LRU formatted as a string.</returns>
854
        internal string FormatLruString()
855
        {
75✔
856
            var sb = new System.Text.StringBuilder();
75✔
857

858
            sb.Append("Hot [");
75✔
859
            sb.Append(string.Join(",", this.hotQueue.Select(n => n.Key.ToString())));
280✔
860
            sb.Append("] Warm [");
75✔
861
            sb.Append(string.Join(",", this.warmQueue.Select(n => n.Key.ToString())));
250✔
862
            sb.Append("] Cold [");
75✔
863
            sb.Append(string.Join(",", this.coldQueue.Select(n => n.Key.ToString())));
225✔
864
            sb.Append(']');
75✔
865

866
            return sb.ToString();
75✔
867
        }
75✔
868
#endif
869

870
        private static CachePolicy CreatePolicy(ConcurrentLruCore<K, V, I, P, T> lru)
871
        {
346✔
872
            var p = new Proxy(lru);
346✔
873

874
            if (typeof(P) == typeof(AfterAccessPolicy<K, V>))
346✔
875
            {
55✔
876
                return new CachePolicy(new Optional<IBoundedPolicy>(p), Optional<ITimePolicy>.None(), new Optional<ITimePolicy>(p), Optional<IDiscreteTimePolicy>.None());
55✔
877
            }
878

879
            // IsAssignableFrom is a jit intrinsic https://github.com/dotnet/runtime/issues/4920
880
            if (typeof(IDiscreteItemPolicy<K, V>).IsAssignableFrom(typeof(P)))
291✔
881
            {
75✔
882
                return new CachePolicy(new Optional<IBoundedPolicy>(p), Optional<ITimePolicy>.None(), Optional<ITimePolicy>.None(), new Optional<IDiscreteTimePolicy>(new DiscreteExpiryProxy(lru)));
75✔
883
            }
884

885
            return new CachePolicy(new Optional<IBoundedPolicy>(p), lru.itemPolicy.CanDiscard() ? new Optional<ITimePolicy>(p) : Optional<ITimePolicy>.None());
216✔
886
        }
346✔
887

888
        private static Optional<ICacheMetrics> CreateMetrics(ConcurrentLruCore<K, V, I, P, T> lru)
889
        {
165✔
890
            if (typeof(T) == typeof(NoTelemetryPolicy<K, V>))
165✔
891
            {
25✔
892
                return Optional<ICacheMetrics>.None();
25✔
893
            }
894

895
            return new(new Proxy(lru));
140✔
896
        }
165✔
897

898
        private static Optional<ICacheEvents<K, V>> CreateEvents(ConcurrentLruCore<K, V, I, P, T> lru)
899
        {
1,352✔
900
            if (typeof(T) == typeof(NoTelemetryPolicy<K, V>))
1,352✔
901
            {
65✔
902
                return Optional<ICacheEvents<K, V>>.None();
65✔
903
            }
904

905
            return new(new Proxy(lru));
1,287✔
906
        }
1,352✔
907

908
#if NET9_0_OR_GREATER
909
        ///<inheritdoc/>
910
        public IAlternateLookup<TAlternateKey, K, V> GetAlternateLookup<TAlternateKey>()
911
            where TAlternateKey : notnull, allows ref struct
912
        {
24✔
913
            if (!this.dictionary.IsCompatibleKey<TAlternateKey, K, I>())
24✔
914
            {
3✔
915
                Throw.IncompatibleComparer();
3✔
916
            }
917

918
            return new AlternateLookup<TAlternateKey>(this);
21✔
919
        }
21✔
920

921
        ///<inheritdoc/>
922
        public bool TryGetAlternateLookup<TAlternateKey>([MaybeNullWhen(false)] out IAlternateLookup<TAlternateKey, K, V> lookup)
923
            where TAlternateKey : notnull, allows ref struct
924
        {
6✔
925
            if (this.dictionary.IsCompatibleKey<TAlternateKey, K, I>())
6✔
926
            {
3✔
927
                lookup = new AlternateLookup<TAlternateKey>(this);
3✔
928
                return true;
3✔
929
            }
930

931
            lookup = default;
3✔
932
            return false;
3✔
933
        }
6✔
934

935
        ///<inheritdoc/>
936
        public IAsyncAlternateLookup<TAlternateKey, K, V> GetAsyncAlternateLookup<TAlternateKey>()
937
            where TAlternateKey : notnull, allows ref struct
938
        {
5✔
939
            if (!this.dictionary.IsCompatibleKey<TAlternateKey, K, I>())
5✔
940
            {
1✔
941
                Throw.IncompatibleComparer();
1✔
942
            }
943

944
            return new AlternateLookup<TAlternateKey>(this);
4✔
945
        }
4✔
946

947
        ///<inheritdoc/>
948
        public bool TryGetAsyncAlternateLookup<TAlternateKey>([MaybeNullWhen(false)] out IAsyncAlternateLookup<TAlternateKey, K, V> lookup)
949
            where TAlternateKey : notnull, allows ref struct
950
        {
2✔
951
            if (this.dictionary.IsCompatibleKey<TAlternateKey, K, I>())
2✔
952
            {
1✔
953
                lookup = new AlternateLookup<TAlternateKey>(this);
1✔
954
                return true;
1✔
955
            }
956

957
            lookup = default;
1✔
958
            return false;
1✔
959
        }
2✔
960

961
        internal readonly struct AlternateLookup<TAlternateKey> : IAlternateLookup<TAlternateKey, K, V>, IAsyncAlternateLookup<TAlternateKey, K, V>
962
            where TAlternateKey : notnull, allows ref struct
963
        {
964
            internal AlternateLookup(ConcurrentLruCore<K, V, I, P, T> lru)
965
            {
29✔
966
                Debug.Assert(lru is not null);
29✔
967
                Debug.Assert(lru.dictionary.IsCompatibleKey<TAlternateKey, K, I>());
29✔
968
                this.Lru = lru;
29✔
969
                this.Alternate = lru.dictionary.GetAlternateLookup<TAlternateKey>();
29✔
970
                this.Comparer = lru.dictionary.GetAlternateComparer<TAlternateKey, K, I>();
29✔
971
            }
29✔
972

973
            internal ConcurrentLruCore<K, V, I, P, T> Lru { get; }
39,919,863✔
974

975
            internal ConcurrentDictionary<K, I>.AlternateLookup<TAlternateKey> Alternate { get; }
24,058,691✔
976

977
            internal IAlternateEqualityComparer<TAlternateKey, K> Comparer { get; }
19,776,358✔
978

979
            public bool TryGet(TAlternateKey key, [MaybeNullWhen(false)] out V value)
980
            {
20,058,674✔
981
                if (this.Alternate.TryGetValue(key, out var item))
20,058,674✔
982
                {
282,317✔
983
                    return this.Lru.GetOrDiscard(item, out value);
282,317✔
984
                }
985

986
                value = default;
19,776,357✔
987
                this.Lru.telemetryPolicy.IncrementMiss();
19,776,357✔
988
                return false;
19,776,357✔
989
            }
20,058,674✔
990

991
            public bool TryRemove(TAlternateKey key, [MaybeNullWhen(false)] out K actualKey, [MaybeNullWhen(false)] out V value)
992
            {
4,000,005✔
993
                if (this.Alternate.TryRemove(key, out actualKey, out var item))
4,000,005✔
994
                {
7,653✔
995
                    this.Lru.OnRemove(actualKey, item, ItemRemovedReason.Removed);
7,653✔
996
                    value = item.Value;
7,653✔
997
                    return true;
7,653✔
998
                }
999

1000
                actualKey = default;
3,992,352✔
1001
                value = default;
3,992,352✔
1002
                return false;
3,992,352✔
1003
            }
4,000,005✔
1004

1005
            public bool TryUpdate(TAlternateKey key, V value)
1006
            {
12✔
1007
                if (this.Alternate.TryGetValue(key, out var existing))
12✔
1008
                {
6✔
1009
                    return this.Lru.TryUpdateValue(existing, value);
6✔
1010
                }
1011

1012
                return false;
6✔
1013
            }
12✔
1014

1015
            public void AddOrUpdate(TAlternateKey key, V value)
1016
            {
6✔
1017
                K actualKey = default!;
6✔
1018
                bool hasActualKey = false;
6✔
1019

1020
                while (true)
6✔
1021
                {
6✔
1022
                    if (this.TryUpdate(key, value))
6✔
1023
                    {
3✔
1024
                        return;
3✔
1025
                    }
1026

1027
                    if (!hasActualKey)
3✔
1028
                    {
3✔
1029
                        actualKey = this.Comparer.Create(key);
3✔
1030
                        hasActualKey = true;
3✔
1031
                    }
3✔
1032

1033
                    if (this.Lru.TryAdd(actualKey, value))
3!
1034
                    {
3✔
1035
                        return;
3✔
1036
                    }
1037
                }
1038
            }
6✔
1039

1040
            public V GetOrAdd(TAlternateKey key, Func<K, V> valueFactory)
1041
            {
8,000,009✔
1042
                while (true)
8,046,147✔
1043
                {
8,046,147✔
1044
                    if (this.TryGet(key, out var value))
8,046,147✔
1045
                    {
137,403✔
1046
                        return value;
137,403✔
1047
                    }
1048

1049
                    K actualKey = this.Comparer.Create(key);
7,908,744✔
1050

1051
                    value = valueFactory(actualKey);
7,908,744✔
1052
                    if (this.Lru.TryAdd(actualKey, value))
7,908,744✔
1053
                    {
7,862,606✔
1054
                        return value;
7,862,606✔
1055
                    }
1056
                }
46,138✔
1057
            }
8,000,009✔
1058

1059
            public V GetOrAdd<TArg>(TAlternateKey key, Func<K, TArg, V> valueFactory, TArg factoryArgument)
1060
            {
4,000,001✔
1061
                while (true)
4,012,512✔
1062
                {
4,012,512✔
1063
                    if (this.TryGet(key, out var value))
4,012,512✔
1064
                    {
43,691✔
1065
                        return value;
43,691✔
1066
                    }
1067

1068
                    K actualKey = this.Comparer.Create(key);
3,968,821✔
1069

1070
                    value = valueFactory(actualKey, factoryArgument);
3,968,821✔
1071
                    if (this.Lru.TryAdd(actualKey, value))
3,968,821✔
1072
                    {
3,956,310✔
1073
                        return value;
3,956,310✔
1074
                    }
1075
                }
12,511✔
1076
            }
4,000,001✔
1077

1078
            public ValueTask<V> GetOrAddAsync(TAlternateKey key, Func<K, Task<V>> valueFactory)
1079
            {
4,000,002✔
1080
                if (this.TryGet(key, out var value))
4,000,002✔
1081
                {
35,927✔
1082
                    return new ValueTask<V>(value);
35,927✔
1083
                }
1084

1085
                K actualKey = this.Comparer.Create(key);
3,964,075✔
1086
                Task<V> task = valueFactory(actualKey);
3,964,075✔
1087

1088
                return GetOrAddAsyncSlow(actualKey, task);
3,964,075✔
1089
            }
4,000,002✔
1090

1091
            public ValueTask<V> GetOrAddAsync<TArg>(TAlternateKey key, Func<K, TArg, Task<V>> valueFactory, TArg factoryArgument)
1092
            {
4,000,002✔
1093
                if (this.TryGet(key, out var value))
4,000,002✔
1094
                {
65,287✔
1095
                    return new ValueTask<V>(value);
65,287✔
1096
                }
1097

1098
                K actualKey = this.Comparer.Create(key);
3,934,715✔
1099
                Task<V> task = valueFactory(actualKey, factoryArgument);
3,934,715✔
1100

1101
                return GetOrAddAsyncSlow(actualKey, task);
3,934,715✔
1102
            }
4,000,002✔
1103

1104
            // Since TAlternateKey can be a ref struct, we can't use async/await in the public GetOrAddAsync methods,
1105
            // so we delegate to this private async method after the value factory is invoked.
1106
            private async ValueTask<V> GetOrAddAsyncSlow(K actualKey, Task<V> task)
1107
            {
7,898,790✔
1108
                V value = await task.ConfigureAwait(false);
7,898,790✔
1109

1110
                while (true)
7,898,810✔
1111
                {
7,898,810✔
1112
                    if (this.Lru.TryAdd(actualKey, value))
7,898,810✔
1113
                    {
7,821,658✔
1114
                        return value;
7,821,658✔
1115
                    }
1116

1117
                    // Another thread added a value for this key first, retrieve it.
1118
                    if (this.Lru.TryGet(actualKey, out V? existing))
77,152✔
1119
                    {
77,132✔
1120
                        return existing;
77,132✔
1121
                    }
1122
                }
20✔
1123
            }
7,898,790✔
1124
        }
1125
#endif
1126

1127
        // To get JIT optimizations, policies must be structs.
1128
        // If the structs are returned directly via properties, they will be copied. Since  
1129
        // telemetryPolicy is a mutable struct, copy is bad. One workaround is to store the 
1130
        // state within the struct in an object. Since the struct points to the same object
1131
        // it becomes immutable. However, this object is then somewhere else on the 
1132
        // heap, which slows down the policies with hit counter logic in benchmarks. Likely
1133
        // this approach keeps the structs data members in the same CPU cache line as the LRU.
1134
        // backcompat: remove conditional compile
1135
#if NETCOREAPP3_0_OR_GREATER
1136
        [DebuggerDisplay("Hit = {Hits}, Miss = {Misses}, Upd = {Updated}, Evict = {Evicted}")]
1137
#else
1138
        [DebuggerDisplay("Hit = {Hits}, Miss = {Misses}, Evict = {Evicted}")]
1139
#endif
1140
        private class Proxy : ICacheMetrics, ICacheEvents<K, V>, IBoundedPolicy, ITimePolicy
1141
        {
1142
            private readonly ConcurrentLruCore<K, V, I, P, T> lru;
1143

1144
            public Proxy(ConcurrentLruCore<K, V, I, P, T> lru)
1,773✔
1145
            {
1,773✔
1146
                this.lru = lru;
1,773✔
1147
            }
1,773✔
1148

1149
            public double HitRatio => lru.telemetryPolicy.HitRatio;
10✔
1150

1151
            public long Total => lru.telemetryPolicy.Total;
5✔
1152

1153
            public long Hits => lru.telemetryPolicy.Hits;
30✔
1154

1155
            public long Misses => lru.telemetryPolicy.Misses;
30✔
1156

1157
            public long Evicted => lru.telemetryPolicy.Evicted;
40✔
1158

1159
            // backcompat: remove conditional compile
1160
#if NETCOREAPP3_0_OR_GREATER
1161
            public long Updated => lru.telemetryPolicy.Updated;
10✔
1162
#endif
1163
            public int Capacity => lru.Capacity;
135✔
1164

1165
            public TimeSpan TimeToLive => lru.itemPolicy.TimeToLive;
20✔
1166

1167
            public event EventHandler<ItemRemovedEventArgs<K, V>> ItemRemoved
1168
            {
1169
                add { this.lru.telemetryPolicy.ItemRemoved += value; }
225✔
1170
                remove { this.lru.telemetryPolicy.ItemRemoved -= value; }
30✔
1171
            }
1172

1173
            // backcompat: remove conditional compile
1174
#if NETCOREAPP3_0_OR_GREATER
1175
            public event EventHandler<ItemUpdatedEventArgs<K, V>> ItemUpdated
1176
            {
1177
                add { this.lru.telemetryPolicy.ItemUpdated += value; }
132✔
1178
                remove { this.lru.telemetryPolicy.ItemUpdated -= value; }
15✔
1179
            }
1180
#endif
1181
            public void Trim(int itemCount)
1182
            {
50✔
1183
                lru.Trim(itemCount);
50✔
1184
            }
50✔
1185

1186
            public void TrimExpired()
1187
            {
36✔
1188
                lru.TrimExpired();
36✔
1189
            }
36✔
1190
        }
1191

1192
        private class DiscreteExpiryProxy : IDiscreteTimePolicy
1193
        {
1194
            private readonly ConcurrentLruCore<K, V, I, P, T> lru;
1195

1196
            public DiscreteExpiryProxy(ConcurrentLruCore<K, V, I, P, T> lru)
75✔
1197
            {
75✔
1198
                this.lru = lru;
75✔
1199
            }
75✔
1200

1201
            public void TrimExpired()
1202
            {
10✔
1203
                lru.TrimExpired();
10✔
1204
            }
10✔
1205

1206
            public bool TryGetTimeToExpire<TKey>(TKey key, out TimeSpan timeToLive)
1207
            {
15✔
1208
                if (key is K k && lru.dictionary.TryGetValue(k, out var item))
15✔
1209
                {
5✔
1210
                    LongTickCountLruItem<K, V>? tickItem = item as LongTickCountLruItem<K, V>;
5✔
1211
                    timeToLive = (new Duration(tickItem!.TickCount) - Duration.SinceEpoch()).ToTimeSpan();
5✔
1212
                    return true;
5✔
1213
                }
1214

1215
                timeToLive = default;
10✔
1216
                return false;
10✔
1217
            }
15✔
1218
        }
1219
    }
1220
}
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