From 4789a9ec18a213be20f04b7f02f3900ca7a566c2 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 14:47:00 -0800 Subject: [PATCH 01/11] generic node --- .../Lfu/ConcurrentLfuSoakTests.cs | 24 +- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 211 ++++++++++++++++-- BitFaster.Caching/Lfu/LfuNode.cs | 9 +- BitFaster.Caching/Lfu/NodePolicy.cs | 22 ++ 4 files changed, 231 insertions(+), 35 deletions(-) create mode 100644 BitFaster.Caching/Lfu/NodePolicy.cs diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs index 86e0e075..6a24d833 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs @@ -310,11 +310,13 @@ private async Task RunIntegrityCheckAsync(ConcurrentLfu lfu, int it private static void RunIntegrityCheck(ConcurrentLfu cache, ITestOutputHelper output) { - new ConcurrentLfuIntegrityChecker(cache).Validate(output); + new ConcurrentLfuIntegrityChecker, AccessOrderPolicy>(cache).Validate(output); } } - public class ConcurrentLfuIntegrityChecker + internal class ConcurrentLfuIntegrityChecker + where N : LfuNode + where P : struct, INodePolicy { private readonly ConcurrentLfu cache; @@ -322,15 +324,15 @@ public class ConcurrentLfuIntegrityChecker private readonly LfuNodeList probationLru; private readonly LfuNodeList protectedLru; - private readonly StripedMpscBuffer> readBuffer; - private readonly MpscBoundedBuffer> writeBuffer; + private readonly StripedMpscBuffer readBuffer; + private readonly MpscBoundedBuffer writeBuffer; - private static FieldInfo windowLruField = typeof(ConcurrentLfu).GetField("windowLru", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo probationLruField = typeof(ConcurrentLfu).GetField("probationLru", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo protectedLruField = typeof(ConcurrentLfu).GetField("protectedLru", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo windowLruField = typeof(ConcurrentLfuCore).GetField("windowLru", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo probationLruField = typeof(ConcurrentLfuCore).GetField("probationLru", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo protectedLruField = typeof(ConcurrentLfuCore).GetField("protectedLru", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo readBufferField = typeof(ConcurrentLfu).GetField("readBuffer", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo writeBufferField = typeof(ConcurrentLfu).GetField("writeBuffer", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo readBufferField = typeof(ConcurrentLfuCore).GetField("readBuffer", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo writeBufferField = typeof(ConcurrentLfuCore).GetField("writeBuffer", BindingFlags.NonPublic | BindingFlags.Instance); public ConcurrentLfuIntegrityChecker(ConcurrentLfu cache) { @@ -341,8 +343,8 @@ public ConcurrentLfuIntegrityChecker(ConcurrentLfu cache) this.probationLru = (LfuNodeList)probationLruField.GetValue(cache); this.protectedLru = (LfuNodeList)protectedLruField.GetValue(cache); - this.readBuffer = (StripedMpscBuffer>)readBufferField.GetValue(cache); - this.writeBuffer = (MpscBoundedBuffer>)writeBufferField.GetValue(cache); + this.readBuffer = (StripedMpscBuffer)readBufferField.GetValue(cache); + this.writeBuffer = (MpscBoundedBuffer)writeBufferField.GetValue(cache); } public void Validate(ITestOutputHelper output) diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 6f19e3fb..efb5511a 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -20,6 +20,169 @@ namespace BitFaster.Caching.Lfu { + /// + /// Facade to hide generics. + /// + public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoundedPolicy + { + private readonly ConcurrentLfuCore, AccessOrderPolicy> core; + + /// + /// The default buffer size. + /// + public const int DefaultBufferSize = 128; + + /// + /// Initializes a new instance of the ConcurrentLfu class with the specified capacity. + /// + /// The capacity. + public ConcurrentLfu(int capacity) + { + this.core = new (capacity); + } + + /// + /// Initializes a new instance of the ConcurrentLfu class with the specified concurrencyLevel, capacity, scheduler, equality comparer and buffer size. + /// + /// The concurrency level. + /// The capacity. + /// The scheduler. + /// The equality comparer. + public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer) + { + this.core = new (concurrencyLevel, capacity, scheduler, comparer); + } + + /// + public int Count => core.Count; + + /// + public Optional Metrics => core.Metrics; + + /// + public Optional> Events => core.Events; + + /// + public CachePolicy Policy => core.Policy; + + /// + public ICollection Keys => core.Keys; + + /// + public int Capacity => core.Capacity; + + /// + public IScheduler Scheduler => core.Scheduler; + + /// + /// Synchronously perform all pending policy maintenance. Drain the read and write buffers then + /// use the eviction policy to preserve bounded size and remove expired items. + /// + /// + /// Note: maintenance is automatically performed asynchronously immediately following a read or write. + /// It is not necessary to call this method, is provided purely to enable tests to reach a consistent state. + /// + public void DoMaintenance() + { + core.DoMaintenance(); + } + + /// + public void AddOrUpdate(K key, V value) + { + core.AddOrUpdate(key, value); + } + + /// + public void Clear() + { + core.Clear(); + } + + /// + public IEnumerator> GetEnumerator() + { + return core.GetEnumerator(); + } + + /// + public V GetOrAdd(K key, Func valueFactory) + { + return core.GetOrAdd(key, valueFactory); + } + + /// + public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) + { + return core.GetOrAdd(key, valueFactory, factoryArgument); + } + + /// + public ValueTask GetOrAddAsync(K key, Func> valueFactory) + { + return core.GetOrAddAsync(key, valueFactory); + } + + /// + public ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) + { + return core.GetOrAddAsync(key, valueFactory, factoryArgument); + } + + /// + public void Trim(int itemCount) + { + core.Trim(itemCount); + } + + /// + public bool TryGet(K key, out V value) + { + return core.TryGet(key, out value); + } + + /// + public bool TryRemove(K key) + { + return core.TryRemove(key); + } + + /// + public bool TryRemove(KeyValuePair item) + { + return core.TryRemove(item); + } + + /// + public bool TryRemove(K key, out V value) + { + return core.TryRemove(key, out value); + } + + /// + public bool TryUpdate(K key, V value) + { + return core.TryUpdate(key, value); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return core.GetEnumerator(); + } + +#if DEBUG + /// + /// Format the LFU as a string by converting all the keys to strings. + /// + /// The LFU formatted as a string. + public string FormatLfuString() + { + return core.FormatLfuString(); + } +#endif + } + /// /// An approximate LFU based on the W-TinyLfu eviction policy. W-TinyLfu tracks items using a window LRU list, and /// a main space LRU divided into protected and probation segments. Reads and writes to the cache are stored in buffers @@ -40,9 +203,11 @@ namespace BitFaster.Caching.Lfu /// /// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes). /// https://github.com/ben-manes/caffeine - [DebuggerTypeProxy(typeof(ConcurrentLfu<,>.LfuDebugView))] + [DebuggerTypeProxy(typeof(ConcurrentLfuCore<,,,>.LfuDebugView))] [DebuggerDisplay("Count = {Count}/{Capacity}")] - public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoundedPolicy + internal sealed class ConcurrentLfuCore : ICache, IAsyncCache, IBoundedPolicy + where N : LfuNode + where P : struct, INodePolicy { private const int MaxWriteBufferRetries = 64; @@ -51,10 +216,10 @@ public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoun /// public const int DefaultBufferSize = 128; - private readonly ConcurrentDictionary> dictionary; + private readonly ConcurrentDictionary dictionary; - private readonly StripedMpscBuffer> readBuffer; - private readonly MpscBoundedBuffer> writeBuffer; + private readonly StripedMpscBuffer readBuffer; + private readonly MpscBoundedBuffer writeBuffer; private readonly CacheMetrics metrics = new(); @@ -72,13 +237,13 @@ public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoun private readonly IScheduler scheduler; private readonly Action drainBuffers; - private readonly LfuNode[] drainBuffer; + private readonly N[] drainBuffer; /// /// Initializes a new instance of the ConcurrentLfu class with the specified capacity. /// /// The capacity. - public ConcurrentLfu(int capacity) + public ConcurrentLfuCore(int capacity) : this(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default) { } @@ -90,18 +255,18 @@ public ConcurrentLfu(int capacity) /// The capacity. /// The scheduler. /// The equality comparer. - public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer) + public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer) { int dictionaryCapacity = ConcurrentDictionarySize.Estimate(capacity); - this.dictionary = new ConcurrentDictionary>(concurrencyLevel, dictionaryCapacity, comparer); + this.dictionary = new (concurrencyLevel, dictionaryCapacity, comparer); // cap concurrency at proc count * 2 int readStripes = Math.Min(BitOps.CeilingPowerOfTwo(concurrencyLevel), BitOps.CeilingPowerOfTwo(Environment.ProcessorCount * 2)); - this.readBuffer = new StripedMpscBuffer>(readStripes, DefaultBufferSize); + this.readBuffer = new (readStripes, DefaultBufferSize); // Cap the write buffer to the cache size, or 128. Whichever is smaller. int writeBufferSize = Math.Min(BitOps.CeilingPowerOfTwo(capacity), 128); - this.writeBuffer = new MpscBoundedBuffer>(writeBufferSize); + this.writeBuffer = new (writeBufferSize); this.cmSketch = new CmSketch(capacity, comparer); this.windowLru = new LfuNodeList(); @@ -113,7 +278,7 @@ public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, I this.scheduler = scheduler; this.drainBuffers = () => this.DrainBuffers(); - this.drainBuffer = new LfuNode[this.readBuffer.Capacity]; + this.drainBuffer = new N[this.readBuffer.Capacity]; } // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ @@ -150,7 +315,7 @@ public void AddOrUpdate(K key, V value) return; } - var node = new LfuNode(key, value); + var node = default(P).Create(key, value); if (this.dictionary.TryAdd(key, node)) { AfterWrite(node); @@ -205,7 +370,7 @@ public void Trim(int itemCount) private bool TryAdd(K key, V value) { - var node = new LfuNode(key, value); + var node = default(P).Create(key, value); if (this.dictionary.TryAdd(key, node)) { @@ -338,13 +503,13 @@ public bool TryRemove(KeyValuePair item) { if (EqualityComparer.Default.Equals(node.Value, item.Value)) { - var kvp = new KeyValuePair>(item.Key, node); + var kvp = new KeyValuePair(item.Key, node); #if NET6_0_OR_GREATER if (this.dictionary.TryRemove(kvp)) #else // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/ - if (((ICollection>>)this.dictionary).Remove(kvp)) + if (((ICollection>)this.dictionary).Remove(kvp)) #endif { node.WasRemoved = true; @@ -445,7 +610,7 @@ private static void TakeCandidatesInLruOrder(LfuNodeList lru, List node) + private void AfterWrite(N node) { for (int i = 0; i < MaxWriteBufferRetries; i++) { @@ -509,7 +674,7 @@ private void ScheduleAfterWrite() IEnumerator IEnumerable.GetEnumerator() { - return ((ConcurrentLfu)this).GetEnumerator(); + return ((ConcurrentLfuCore)this).GetEnumerator(); } private void TryScheduleDrain() @@ -570,7 +735,7 @@ private void DrainBuffers() } } - private bool Maintenance(LfuNode droppedWrite = null) + private bool Maintenance(N droppedWrite = null) { this.drainStatus.VolatileWrite(DrainStatus.ProcessingToIdle); @@ -975,9 +1140,9 @@ public string FormatLfuString() [ExcludeFromCodeCoverage] internal class LfuDebugView { - private readonly ConcurrentLfu lfu; + private readonly ConcurrentLfuCore lfu; - public LfuDebugView(ConcurrentLfu lfu) + public LfuDebugView(ConcurrentLfuCore lfu) { this.lfu = lfu; } @@ -986,9 +1151,9 @@ public LfuDebugView(ConcurrentLfu lfu) public ICacheMetrics Metrics => lfu.metrics; - public StripedMpscBuffer> ReadBuffer => this.lfu.readBuffer; + public StripedMpscBuffer ReadBuffer => this.lfu.readBuffer; - public MpscBoundedBuffer> WriteBuffer => this.lfu.writeBuffer; + public MpscBoundedBuffer WriteBuffer => this.lfu.writeBuffer; public KeyValuePair[] Items { diff --git a/BitFaster.Caching/Lfu/LfuNode.cs b/BitFaster.Caching/Lfu/LfuNode.cs index 40028176..f56db8e2 100644 --- a/BitFaster.Caching/Lfu/LfuNode.cs +++ b/BitFaster.Caching/Lfu/LfuNode.cs @@ -1,6 +1,6 @@ namespace BitFaster.Caching.Lfu { - internal sealed class LfuNode + internal class LfuNode { internal LfuNodeList list; internal LfuNode next; @@ -57,4 +57,11 @@ internal enum Position Probation, Protected, } + + internal sealed class AccessOrderNode : LfuNode + { + public AccessOrderNode(K k, V v) : base(k, v) + { + } + } } diff --git a/BitFaster.Caching/Lfu/NodePolicy.cs b/BitFaster.Caching/Lfu/NodePolicy.cs new file mode 100644 index 00000000..5e3a21f8 --- /dev/null +++ b/BitFaster.Caching/Lfu/NodePolicy.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace BitFaster.Caching.Lfu +{ + internal interface INodePolicy + where N : LfuNode + { + N Create(K key, V value); + } + + internal struct AccessOrderPolicy : INodePolicy> + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public AccessOrderNode Create(K key, V value) + { + return new AccessOrderNode(key, value); + } + } +} From 5bf76ac8c555193a44af299320dbb423c8cce994 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 14:50:11 -0800 Subject: [PATCH 02/11] core --- BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs | 6 +++--- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs index 6a24d833..5fceb2ec 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs @@ -310,7 +310,7 @@ private async Task RunIntegrityCheckAsync(ConcurrentLfu lfu, int it private static void RunIntegrityCheck(ConcurrentLfu cache, ITestOutputHelper output) { - new ConcurrentLfuIntegrityChecker, AccessOrderPolicy>(cache).Validate(output); + new ConcurrentLfuIntegrityChecker, AccessOrderPolicy>(cache.Core).Validate(output); } } @@ -318,7 +318,7 @@ internal class ConcurrentLfuIntegrityChecker where N : LfuNode where P : struct, INodePolicy { - private readonly ConcurrentLfu cache; + private readonly ConcurrentLfuCore cache; private readonly LfuNodeList windowLru; private readonly LfuNodeList probationLru; @@ -334,7 +334,7 @@ internal class ConcurrentLfuIntegrityChecker private static FieldInfo readBufferField = typeof(ConcurrentLfuCore).GetField("readBuffer", BindingFlags.NonPublic | BindingFlags.Instance); private static FieldInfo writeBufferField = typeof(ConcurrentLfuCore).GetField("writeBuffer", BindingFlags.NonPublic | BindingFlags.Instance); - public ConcurrentLfuIntegrityChecker(ConcurrentLfu cache) + public ConcurrentLfuIntegrityChecker(ConcurrentLfuCore cache) { this.cache = cache; diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index efb5511a..d7f3e43c 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -53,6 +53,8 @@ public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, I this.core = new (concurrencyLevel, capacity, scheduler, comparer); } + internal ConcurrentLfuCore, AccessOrderPolicy> Core => core; + /// public int Count => core.Count; From 325fb9edff64726254a43fe4ec4b8e590ffa5fce Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 20:48:06 -0800 Subject: [PATCH 03/11] struct --- .../Lfu/ConcurrentLfuSoakTests.cs | 1 - BitFaster.Caching/BitFaster.Caching.csproj | 2 +- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 125 ++++++++---------- 3 files changed, 59 insertions(+), 69 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs index 5fceb2ec..00ab7247 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs @@ -307,7 +307,6 @@ private async Task RunIntegrityCheckAsync(ConcurrentLfu lfu, int it RunIntegrityCheck(lfu, this.output); } - private static void RunIntegrityCheck(ConcurrentLfu cache, ITestOutputHelper output) { new ConcurrentLfuIntegrityChecker, AccessOrderPolicy>(cache.Core).Validate(output); diff --git a/BitFaster.Caching/BitFaster.Caching.csproj b/BitFaster.Caching/BitFaster.Caching.csproj index 248e6559..9c14368a 100644 --- a/BitFaster.Caching/BitFaster.Caching.csproj +++ b/BitFaster.Caching/BitFaster.Caching.csproj @@ -2,7 +2,7 @@ netstandard2.0;netcoreapp3.1;net6.0 - 9.0 + 11.0 Alex Peck BitFaster.Caching diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index d7f3e43c..794f682a 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -23,9 +23,12 @@ namespace BitFaster.Caching.Lfu /// /// Facade to hide generics. /// + [DebuggerTypeProxy(typeof(ConcurrentLfu<,>.LfuDebugView<>))] + [DebuggerDisplay("Count = {Count}/{Capacity}")] public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoundedPolicy { - private readonly ConcurrentLfuCore, AccessOrderPolicy> core; + // Note: for performance reasons this is a mutable struct, it cannot be readonly. + private ConcurrentLfuCore, AccessOrderPolicy> core; /// /// The default buffer size. @@ -38,7 +41,7 @@ public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoun /// The capacity. public ConcurrentLfu(int capacity) { - this.core = new (capacity); + this.core = new(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default, () => this.DrainBuffers()); } /// @@ -50,11 +53,17 @@ public ConcurrentLfu(int capacity) /// The equality comparer. public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer) { - this.core = new (concurrencyLevel, capacity, scheduler, comparer); + this.core = new (concurrencyLevel, capacity, scheduler, comparer, () => this.DrainBuffers()); } internal ConcurrentLfuCore, AccessOrderPolicy> Core => core; + // structs cannot declare self referencing lambda functions, therefore pass this in from the ctor + private void DrainBuffers() + { + this.core.DrainBuffers(); + } + /// public int Count => core.Count; @@ -183,6 +192,41 @@ public string FormatLfuString() return core.FormatLfuString(); } #endif + + [ExcludeFromCodeCoverage] + internal class LfuDebugView + where N : LfuNode + { + private readonly ConcurrentLfu lfu; + + public LfuDebugView(ConcurrentLfu lfu) + { + this.lfu = lfu; + } + + public string Maintenance => lfu.core.drainStatus.Format(); + + public ICacheMetrics Metrics => lfu.Metrics.Value; + + public StripedMpscBuffer ReadBuffer => this.lfu.core.readBuffer as StripedMpscBuffer; + + public MpscBoundedBuffer WriteBuffer => this.lfu.core.writeBuffer as MpscBoundedBuffer; + + public KeyValuePair[] Items + { + get + { + var items = new KeyValuePair[lfu.Count]; + + int index = 0; + foreach (var kvp in lfu) + { + items[index++] = kvp; + } + return items; + } + } + } } /// @@ -205,23 +249,19 @@ public string FormatLfuString() /// /// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes). /// https://github.com/ben-manes/caffeine - [DebuggerTypeProxy(typeof(ConcurrentLfuCore<,,,>.LfuDebugView))] - [DebuggerDisplay("Count = {Count}/{Capacity}")] - internal sealed class ConcurrentLfuCore : ICache, IAsyncCache, IBoundedPolicy + + internal struct ConcurrentLfuCore : ICache, IAsyncCache, IBoundedPolicy where N : LfuNode where P : struct, INodePolicy { private const int MaxWriteBufferRetries = 64; - /// - /// The default buffer size. - /// - public const int DefaultBufferSize = 128; + private const int DefaultBufferSize = 128; private readonly ConcurrentDictionary dictionary; - private readonly StripedMpscBuffer readBuffer; - private readonly MpscBoundedBuffer writeBuffer; + internal readonly StripedMpscBuffer readBuffer; + internal readonly MpscBoundedBuffer writeBuffer; private readonly CacheMetrics metrics = new(); @@ -233,7 +273,7 @@ internal sealed class ConcurrentLfuCore : ICache, IAsyncCache< private readonly LfuCapacityPartition capacity; - private readonly DrainStatus drainStatus = new(); + internal readonly DrainStatus drainStatus = new(); private readonly object maintenanceLock = new(); private readonly IScheduler scheduler; @@ -241,23 +281,7 @@ internal sealed class ConcurrentLfuCore : ICache, IAsyncCache< private readonly N[] drainBuffer; - /// - /// Initializes a new instance of the ConcurrentLfu class with the specified capacity. - /// - /// The capacity. - public ConcurrentLfuCore(int capacity) - : this(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default) - { - } - - /// - /// Initializes a new instance of the ConcurrentLfu class with the specified concurrencyLevel, capacity, scheduler, equality comparer and buffer size. - /// - /// The concurrency level. - /// The capacity. - /// The scheduler. - /// The equality comparer. - public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer) + public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer, Action drainBuffers) { int dictionaryCapacity = ConcurrentDictionarySize.Estimate(capacity); this.dictionary = new (concurrencyLevel, dictionaryCapacity, comparer); @@ -278,9 +302,10 @@ public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler schedule this.capacity = new LfuCapacityPartition(capacity); this.scheduler = scheduler; - this.drainBuffers = () => this.DrainBuffers(); this.drainBuffer = new N[this.readBuffer.Capacity]; + + this.drainBuffers = drainBuffers; } // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ @@ -713,7 +738,7 @@ private void TryScheduleDrain() } } - private void DrainBuffers() + internal void DrainBuffers() { bool done = false; @@ -1026,7 +1051,7 @@ private void ReFitProtected() } [DebuggerDisplay("{Format(),nq}")] - private class DrainStatus + internal class DrainStatus { public const int Idle = 0; public const int Required = 1; @@ -1138,40 +1163,6 @@ public string FormatLfuString() return sb.ToString(); } #endif - - [ExcludeFromCodeCoverage] - internal class LfuDebugView - { - private readonly ConcurrentLfuCore lfu; - - public LfuDebugView(ConcurrentLfuCore lfu) - { - this.lfu = lfu; - } - - public string Maintenance => lfu.drainStatus.Format(); - - public ICacheMetrics Metrics => lfu.metrics; - - public StripedMpscBuffer ReadBuffer => this.lfu.readBuffer; - - public MpscBoundedBuffer WriteBuffer => this.lfu.writeBuffer; - - public KeyValuePair[] Items - { - get - { - var items = new KeyValuePair[lfu.Count]; - - int index = 0; - foreach (var kvp in lfu) - { - items[index++] = kvp; - } - return items; - } - } - } } // Explicit layout cannot be a generic class member From 3353e3347fed556caeac358ec8b0bb3de1755da0 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 21:04:04 -0800 Subject: [PATCH 04/11] fix docs --- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 98 ++++++++------------------ 1 file changed, 29 insertions(+), 69 deletions(-) diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 794f682a..bd4a59e1 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -21,8 +21,25 @@ namespace BitFaster.Caching.Lfu { /// - /// Facade to hide generics. + /// An approximate LFU based on the W-TinyLfu eviction policy. W-TinyLfu tracks items using a window LRU list, and + /// a main space LRU divided into protected and probation segments. Reads and writes to the cache are stored in buffers + /// and later applied to the policy LRU lists in batches under a lock. Each read and write is tracked using a compact + /// popularity sketch to probalistically estimate item frequency. Items proceed through the LRU lists as follows: + /// + /// New items are added to the window LRU. When acessed window items move to the window MRU position. + /// When the window is full, candidate items are moved to the probation segment in LRU order. + /// When the main space is full, the access frequency of each window candidate is compared + /// to probation victims in LRU order. The item with the lowest frequency is evicted until the cache size is within bounds. + /// When a probation item is accessed, it is moved to the protected segment. If the protected segment is full, + /// the LRU protected item is demoted to probation. + /// When a protected item is accessed, it is moved to the protected MRU position. + /// + /// The size of the admission window and main space are adapted over time to iteratively improve hit rate using a + /// hill climbing algorithm. A larger window favors workloads with high recency bias, whereas a larger main space + /// favors workloads with frequency bias. /// + /// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes). + /// https://github.com/ben-manes/caffeine [DebuggerTypeProxy(typeof(ConcurrentLfu<,>.LfuDebugView<>))] [DebuggerDisplay("Count = {Count}/{Capacity}")] public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoundedPolicy @@ -158,13 +175,22 @@ public bool TryRemove(K key) return core.TryRemove(key); } - /// + /// + /// Attempts to remove the specified key value pair. + /// + /// The item to remove. + /// true if the item was removed successfully; otherwise, false. public bool TryRemove(KeyValuePair item) { return core.TryRemove(item); } - /// + /// + /// Attempts to remove and return the value that has the specified key. + /// + /// The key of the element to remove. + /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. + /// true if the object was removed successfully; otherwise, false. public bool TryRemove(K key, out V value) { return core.TryRemove(key, out value); @@ -309,30 +335,20 @@ public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler schedule } // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ - /// public int Count => this.dictionary.Skip(0).Count(); - /// public int Capacity => this.capacity.Capacity; - /// public Optional Metrics => new(this.metrics); - /// public Optional> Events => Optional>.None(); - /// public CachePolicy Policy => new(new Optional(this), Optional.None()); - /// public ICollection Keys => this.dictionary.Keys; - /// - /// Gets the scheduler. - /// public IScheduler Scheduler => scheduler; - /// public void AddOrUpdate(K key, V value) { while (true) @@ -351,7 +367,6 @@ public void AddOrUpdate(K key, V value) } } - /// public void Clear() { this.Trim(this.Count); @@ -364,10 +379,6 @@ public void Clear() } } - /// - /// Trim the specified number of items from the cache. - /// - /// The number of items to remove. public void Trim(int itemCount) { itemCount = Math.Min(itemCount, this.Count); @@ -409,7 +420,6 @@ private bool TryAdd(K key, V value) return false; } - /// public V GetOrAdd(K key, Func valueFactory) { while (true) @@ -427,16 +437,6 @@ public V GetOrAdd(K key, Func valueFactory) } } - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to generate a value for the key. - /// An argument value to pass into valueFactory. - /// The value for the key. This will be either the existing value for the key if the key is already - /// in the cache, or the new value if the key was not in the cache. public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) { while (true) @@ -454,7 +454,6 @@ public V GetOrAdd(K key, Func valueFactory, TArg factoryArgume } } - /// public async ValueTask GetOrAddAsync(K key, Func> valueFactory) { while (true) @@ -472,15 +471,6 @@ public async ValueTask GetOrAddAsync(K key, Func> valueFactory) } } - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to asynchronously generate a value for the key. - /// An argument value to pass into valueFactory. - /// A task that represents the asynchronous GetOrAdd operation. public async ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) { while (true) @@ -498,7 +488,6 @@ public async ValueTask GetOrAddAsync(K key, Func> valu } } - /// public bool TryGet(K key, out V value) { if (this.dictionary.TryGetValue(key, out var node)) @@ -519,11 +508,6 @@ public bool TryGet(K key, out V value) return false; } - /// - /// Attempts to remove the specified key value pair. - /// - /// The item to remove. - /// true if the item was removed successfully; otherwise, false. public bool TryRemove(KeyValuePair item) { if (this.dictionary.TryGetValue(item.Key, out var node)) @@ -549,12 +533,6 @@ public bool TryRemove(KeyValuePair item) return false; } - /// - /// Attempts to remove and return the value that has the specified key. - /// - /// The key of the element to remove. - /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. - /// true if the object was removed successfully; otherwise, false. public bool TryRemove(K key, out V value) { if (this.dictionary.TryRemove(key, out var node)) @@ -569,13 +547,11 @@ public bool TryRemove(K key, out V value) return false; } - /// public bool TryRemove(K key) { return this.TryRemove(key, out var _); } - /// public bool TryUpdate(K key, V value) { if (this.dictionary.TryGetValue(key, out var node)) @@ -592,27 +568,11 @@ public bool TryUpdate(K key, V value) return false; } - /// - /// Synchronously perform all pending policy maintenance. Drain the read and write buffers then - /// use the eviction policy to preserve bounded size and remove expired items. - /// - /// - /// Note: maintenance is automatically performed asynchronously immediately following a read or write. - /// It is not necessary to call this method, is provided purely to enable tests to reach a consistent state. - /// public void DoMaintenance() { DrainBuffers(); } - /// Returns an enumerator that iterates through the cache. - /// An enumerator for the cache. - /// - /// The enumerator returned from the cache is safe to use concurrently with - /// reads and writes, however it does not represent a moment-in-time snapshot. - /// The contents exposed through the enumerator may contain modifications - /// made after was called. - /// public IEnumerator> GetEnumerator() { foreach (var kvp in this.dictionary) From 77b8761e7f3a4937e28b01236b4be32740dc4558 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 21:05:40 -0800 Subject: [PATCH 05/11] lang10 --- BitFaster.Caching/BitFaster.Caching.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching/BitFaster.Caching.csproj b/BitFaster.Caching/BitFaster.Caching.csproj index 9c14368a..60aa9ccc 100644 --- a/BitFaster.Caching/BitFaster.Caching.csproj +++ b/BitFaster.Caching/BitFaster.Caching.csproj @@ -2,7 +2,7 @@ netstandard2.0;netcoreapp3.1;net6.0 - 11.0 + 10.0 Alex Peck BitFaster.Caching From b0e90cb56c2da77342eb20029c02d9724482a92e Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 21:28:23 -0800 Subject: [PATCH 06/11] enum --- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index bd4a59e1..eb95fd1b 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -205,7 +205,7 @@ public bool TryUpdate(K key, V value) /// IEnumerator IEnumerable.GetEnumerator() { - return core.GetEnumerator(); + return ((ConcurrentLfuCore, AccessOrderPolicy>)core).GetEnumerator(); } #if DEBUG From e36165069c3602234c1c79d916a945e63dd390d8 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 21:36:26 -0800 Subject: [PATCH 07/11] capacity --- .../Lfu/ConcurrentLfuTests.cs | 24 +++++++++++++++++++ BitFaster.Caching/Lfu/ConcurrentLfu.cs | 3 +++ 2 files changed, 27 insertions(+) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs index 791c534a..e60b72a3 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs @@ -26,6 +26,30 @@ public ConcurrentLfuTests(ITestOutputHelper output) this.output = output; } + [Fact] + public void WhenCapacityIsLessThan3CtorThrows() + { + Action constructor = () => { var x = new ConcurrentLfu(2); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenCapacityIsValidCacheIsCreated() + { + var x = new ConcurrentLfu(3); + + x.Capacity.Should().Be(3); + } + + [Fact] + public void WhenConcurrencyIsLessThan1CtorThrows() + { + Action constructor = () => { var x = new ConcurrentLfu(0, 20, new ForegroundScheduler(), EqualityComparer.Default); }; + + constructor.Should().Throw(); + } + [Fact] public void DefaultSchedulerIsThreadPool() { diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index eb95fd1b..654463a0 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -309,6 +309,9 @@ internal struct ConcurrentLfuCore : ICache, IAsyncCache, public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer, Action drainBuffers) { + if (capacity < 3) + Throw.ArgOutOfRange(nameof(capacity)); + int dictionaryCapacity = ConcurrentDictionarySize.Estimate(capacity); this.dictionary = new (concurrencyLevel, dictionaryCapacity, comparer); From 9c5d849548f21e58f9511bc8aa5e7a16a25d5118 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 22:34:09 -0800 Subject: [PATCH 08/11] remove metrics/events from core --- .../Lfu/ConcurrentLfuTests.cs | 8 +++-- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 36 +++++++------------ 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs index e60b72a3..e3ca7a46 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs @@ -1,10 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using BitFaster.Caching.Buffers; using BitFaster.Caching.Lfu; using BitFaster.Caching.Scheduler; using BitFaster.Caching.UnitTests.Lru; @@ -572,7 +570,11 @@ public void WhenItemsAddedGenericEnumerateContainsKvps() cache.GetOrAdd(1, k => k + 1); cache.GetOrAdd(2, k => k + 1); - cache.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 2), new KeyValuePair(2, 3) }); + var enumerator = cache.GetEnumerator(); + enumerator.MoveNext().Should().BeTrue(); + enumerator.Current.Should().Be(new KeyValuePair(1, 2)); + enumerator.MoveNext().Should().BeTrue(); + enumerator.Current.Should().Be(new KeyValuePair(2, 3)); } [Fact] diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 654463a0..b9a7f052 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -85,13 +85,13 @@ private void DrainBuffers() public int Count => core.Count; /// - public Optional Metrics => core.Metrics; + public Optional Metrics => new(this.core.metrics); /// - public Optional> Events => core.Events; + public Optional> Events => Optional>.None(); /// - public CachePolicy Policy => core.Policy; + public CachePolicy Policy => new(new Optional(this), Optional.None()); /// public ICollection Keys => core.Keys; @@ -127,12 +127,6 @@ public void Clear() core.Clear(); } - /// - public IEnumerator> GetEnumerator() - { - return core.GetEnumerator(); - } - /// public V GetOrAdd(K key, Func valueFactory) { @@ -202,10 +196,17 @@ public bool TryUpdate(K key, V value) return core.TryUpdate(key, value); } + /// + public IEnumerator> GetEnumerator() + { + return core.GetEnumerator(); + } + /// IEnumerator IEnumerable.GetEnumerator() { - return ((ConcurrentLfuCore, AccessOrderPolicy>)core).GetEnumerator(); + return core.GetEnumerator(); + //return ((ConcurrentLfuCore, AccessOrderPolicy>)core).GetEnumerator(); } #if DEBUG @@ -276,7 +277,7 @@ public KeyValuePair[] Items /// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes). /// https://github.com/ben-manes/caffeine - internal struct ConcurrentLfuCore : ICache, IAsyncCache, IBoundedPolicy + internal struct ConcurrentLfuCore : IBoundedPolicy where N : LfuNode where P : struct, INodePolicy { @@ -289,7 +290,7 @@ internal struct ConcurrentLfuCore : ICache, IAsyncCache, internal readonly StripedMpscBuffer readBuffer; internal readonly MpscBoundedBuffer writeBuffer; - private readonly CacheMetrics metrics = new(); + internal readonly CacheMetrics metrics = new(); private readonly CmSketch cmSketch; @@ -342,12 +343,6 @@ public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler schedule public int Capacity => this.capacity.Capacity; - public Optional Metrics => new(this.metrics); - - public Optional> Events => Optional>.None(); - - public CachePolicy Policy => new(new Optional(this), Optional.None()); - public ICollection Keys => this.dictionary.Keys; public IScheduler Scheduler => scheduler; @@ -662,11 +657,6 @@ private void ScheduleAfterWrite() } } - IEnumerator IEnumerable.GetEnumerator() - { - return ((ConcurrentLfuCore)this).GetEnumerator(); - } - private void TryScheduleDrain() { if (this.drainStatus.VolatileRead() >= DrainStatus.ProcessingToIdle) From 9215f83bdcea29242dca003b0725b6e02f4c0d3f Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 22:48:37 -0800 Subject: [PATCH 09/11] Revert "remove metrics/events from core" This reverts commit 9c5d849548f21e58f9511bc8aa5e7a16a25d5118. --- .../Lfu/ConcurrentLfuTests.cs | 8 ++--- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 36 ++++++++++++------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs index e3ca7a46..e60b72a3 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs @@ -1,8 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using BitFaster.Caching.Buffers; using BitFaster.Caching.Lfu; using BitFaster.Caching.Scheduler; using BitFaster.Caching.UnitTests.Lru; @@ -570,11 +572,7 @@ public void WhenItemsAddedGenericEnumerateContainsKvps() cache.GetOrAdd(1, k => k + 1); cache.GetOrAdd(2, k => k + 1); - var enumerator = cache.GetEnumerator(); - enumerator.MoveNext().Should().BeTrue(); - enumerator.Current.Should().Be(new KeyValuePair(1, 2)); - enumerator.MoveNext().Should().BeTrue(); - enumerator.Current.Should().Be(new KeyValuePair(2, 3)); + cache.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 2), new KeyValuePair(2, 3) }); } [Fact] diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index b9a7f052..654463a0 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -85,13 +85,13 @@ private void DrainBuffers() public int Count => core.Count; /// - public Optional Metrics => new(this.core.metrics); + public Optional Metrics => core.Metrics; /// - public Optional> Events => Optional>.None(); + public Optional> Events => core.Events; /// - public CachePolicy Policy => new(new Optional(this), Optional.None()); + public CachePolicy Policy => core.Policy; /// public ICollection Keys => core.Keys; @@ -127,6 +127,12 @@ public void Clear() core.Clear(); } + /// + public IEnumerator> GetEnumerator() + { + return core.GetEnumerator(); + } + /// public V GetOrAdd(K key, Func valueFactory) { @@ -196,17 +202,10 @@ public bool TryUpdate(K key, V value) return core.TryUpdate(key, value); } - /// - public IEnumerator> GetEnumerator() - { - return core.GetEnumerator(); - } - /// IEnumerator IEnumerable.GetEnumerator() { - return core.GetEnumerator(); - //return ((ConcurrentLfuCore, AccessOrderPolicy>)core).GetEnumerator(); + return ((ConcurrentLfuCore, AccessOrderPolicy>)core).GetEnumerator(); } #if DEBUG @@ -277,7 +276,7 @@ public KeyValuePair[] Items /// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes). /// https://github.com/ben-manes/caffeine - internal struct ConcurrentLfuCore : IBoundedPolicy + internal struct ConcurrentLfuCore : ICache, IAsyncCache, IBoundedPolicy where N : LfuNode where P : struct, INodePolicy { @@ -290,7 +289,7 @@ internal struct ConcurrentLfuCore : IBoundedPolicy internal readonly StripedMpscBuffer readBuffer; internal readonly MpscBoundedBuffer writeBuffer; - internal readonly CacheMetrics metrics = new(); + private readonly CacheMetrics metrics = new(); private readonly CmSketch cmSketch; @@ -343,6 +342,12 @@ public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler schedule public int Capacity => this.capacity.Capacity; + public Optional Metrics => new(this.metrics); + + public Optional> Events => Optional>.None(); + + public CachePolicy Policy => new(new Optional(this), Optional.None()); + public ICollection Keys => this.dictionary.Keys; public IScheduler Scheduler => scheduler; @@ -657,6 +662,11 @@ private void ScheduleAfterWrite() } } + IEnumerator IEnumerable.GetEnumerator() + { + return ((ConcurrentLfuCore)this).GetEnumerator(); + } + private void TryScheduleDrain() { if (this.drainStatus.VolatileRead() >= DrainStatus.ProcessingToIdle) From 3e3dc31daa001c4749d92e8c1f454cfc0e3d157c Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 23:05:00 -0800 Subject: [PATCH 10/11] rem interfaces --- .../Lfu/ConcurrentLfuTests.cs | 6 +++++- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 21 +++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs index e60b72a3..b806d867 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs @@ -572,7 +572,11 @@ public void WhenItemsAddedGenericEnumerateContainsKvps() cache.GetOrAdd(1, k => k + 1); cache.GetOrAdd(2, k => k + 1); - cache.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 2), new KeyValuePair(2, 3) }); + var enumerator = cache.GetEnumerator(); + enumerator.MoveNext().Should().BeTrue(); + enumerator.Current.Should().Be(new KeyValuePair(1, 2)); + enumerator.MoveNext().Should().BeTrue(); + enumerator.Current.Should().Be(new KeyValuePair(2, 3)); } [Fact] diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 654463a0..9065fa58 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -127,12 +127,6 @@ public void Clear() core.Clear(); } - /// - public IEnumerator> GetEnumerator() - { - return core.GetEnumerator(); - } - /// public V GetOrAdd(K key, Func valueFactory) { @@ -202,10 +196,16 @@ public bool TryUpdate(K key, V value) return core.TryUpdate(key, value); } + /// + public IEnumerator> GetEnumerator() + { + return core.GetEnumerator(); + } + /// IEnumerator IEnumerable.GetEnumerator() { - return ((ConcurrentLfuCore, AccessOrderPolicy>)core).GetEnumerator(); + return core.GetEnumerator(); } #if DEBUG @@ -276,7 +276,7 @@ public KeyValuePair[] Items /// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes). /// https://github.com/ben-manes/caffeine - internal struct ConcurrentLfuCore : ICache, IAsyncCache, IBoundedPolicy + internal struct ConcurrentLfuCore : IBoundedPolicy where N : LfuNode where P : struct, INodePolicy { @@ -662,11 +662,6 @@ private void ScheduleAfterWrite() } } - IEnumerator IEnumerable.GetEnumerator() - { - return ((ConcurrentLfuCore)this).GetEnumerator(); - } - private void TryScheduleDrain() { if (this.drainStatus.VolatileRead() >= DrainStatus.ProcessingToIdle) From cb8ce8d1dfc8f57f2aa03f676c3baedfa7a18f40 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 2 Dec 2023 23:33:16 -0800 Subject: [PATCH 11/11] default switch --- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 9065fa58..0faf7351 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -1026,8 +1026,8 @@ public bool ShouldDrain(bool delayable) { Idle => !delayable, Required => true, - ProcessingToIdle or ProcessingToRequired => false, - _ => false,// not reachable + // ProcessingToIdle or ProcessingToRequired => false, undefined not reachable + _ => false, }; }