From 038459a04f64b98d79a67d486bb8c115fbbfd008 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Thu, 8 Jan 2026 18:48:16 -0800 Subject: [PATCH] initial --- .../Lfu/ConcurrentLfuSoakTests.cs | 20 +- .../Lfu/TimerWheelTests.cs | 324 +++++++++--------- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 14 +- BitFaster.Caching/Lfu/ConcurrentLfuCore.cs | 134 +++++++- BitFaster.Caching/Lfu/ConcurrentTLfu.cs | 12 +- BitFaster.Caching/Lfu/NodePolicy.cs | 7 +- BitFaster.Caching/Lfu/TimerWheel.cs | 7 +- 7 files changed, 323 insertions(+), 195 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs index f2165c2e..12c9f608 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using BitFaster.Caching.Buffers; using BitFaster.Caching.Lfu; +using BitFaster.Caching.Lru; using BitFaster.Caching.Scheduler; using FluentAssertions; using Xunit; @@ -387,15 +388,16 @@ private async Task RunIntegrityCheckAsync(ConcurrentLfu lfu, int it private static void RunIntegrityCheck(ConcurrentLfu cache, ITestOutputHelper output) { - new ConcurrentLfuIntegrityChecker, AccessOrderPolicy>(cache.Core).Validate(output); + new ConcurrentLfuIntegrityChecker, AccessOrderPolicy, TelemetryPolicy>(cache.Core).Validate(output); } } - internal class ConcurrentLfuIntegrityChecker + internal class ConcurrentLfuIntegrityChecker where N : LfuNode where P : struct, INodePolicy + where T : struct, ITelemetryPolicy { - private readonly ConcurrentLfuCore cache; + private readonly ConcurrentLfuCore cache; private readonly LfuNodeList windowLru; private readonly LfuNodeList probationLru; @@ -404,14 +406,14 @@ internal class ConcurrentLfuIntegrityChecker private readonly StripedMpscBuffer readBuffer; private readonly MpscBoundedBuffer writeBuffer; - 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 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(ConcurrentLfuCore).GetField("readBuffer", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo writeBufferField = typeof(ConcurrentLfuCore).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(ConcurrentLfuCore cache) + public ConcurrentLfuIntegrityChecker(ConcurrentLfuCore cache) { this.cache = cache; diff --git a/BitFaster.Caching.UnitTests/Lfu/TimerWheelTests.cs b/BitFaster.Caching.UnitTests/Lfu/TimerWheelTests.cs index 3300deac..f0eb7b8d 100644 --- a/BitFaster.Caching.UnitTests/Lfu/TimerWheelTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/TimerWheelTests.cs @@ -14,14 +14,14 @@ namespace BitFaster.Caching.UnitTests.Lfu { public class TimerWheelTests - { - private readonly ITestOutputHelper output; - + { + private readonly ITestOutputHelper output; + private readonly TimerWheel timerWheel; private readonly WheelEnumerator wheelEnumerator; private readonly LfuNodeList lfuNodeList; private readonly ExpireAfterPolicy policy; - private ConcurrentLfuCore, ExpireAfterPolicy> cache; + private ConcurrentLfuCore, ExpireAfterPolicy, NoTelemetryPolicy> cache; public TimerWheelTests(ITestOutputHelper testOutputHelper) { @@ -31,32 +31,32 @@ public TimerWheelTests(ITestOutputHelper testOutputHelper) wheelEnumerator = new(timerWheel, testOutputHelper); policy = new ExpireAfterPolicy(new TestExpiryCalculator()); cache = new( - Defaults.ConcurrencyLevel, 3, new ThreadPoolScheduler(), EqualityComparer.Default, () => { }, policy); - } - + Defaults.ConcurrencyLevel, 3, new ThreadPoolScheduler(), EqualityComparer.Default, () => { }, policy, default); + } + [Theory] - [MemberData(nameof(ScheduleData))] - public void WhenAdvanceExpiredNodesExpire(long clock, Duration duration, int expiredCount) - { - var items = new List>(); - timerWheel.time = clock; - - foreach (int timeout in new int[] { 25, 90, 240 }) - { - var node = AddNode(1, new DisposeTracker(), new Duration(clock) + Duration.FromSeconds(timeout)); - items.Add(node); + [MemberData(nameof(ScheduleData))] + public void WhenAdvanceExpiredNodesExpire(long clock, Duration duration, int expiredCount) + { + var items = new List>(); + timerWheel.time = clock; + + foreach (int timeout in new int[] { 25, 90, 240 }) + { + var node = AddNode(1, new DisposeTracker(), new Duration(clock) + Duration.FromSeconds(timeout)); + items.Add(node); timerWheel.Schedule(node); - } - - timerWheel.Advance(ref cache, new Duration(clock) + duration); - - var expired = items.Where(n => ((DisposeTracker)n.Value).Expired); - expired.Count().Should().Be(expiredCount); - - foreach (var node in expired) - { - node.GetTimestamp().Should().BeLessThanOrEqualTo(clock + duration.raw); - } + } + + timerWheel.Advance(ref cache, new Duration(clock) + duration); + + var expired = items.Where(n => ((DisposeTracker)n.Value).Expired); + expired.Count().Should().Be(expiredCount); + + foreach (var node in expired) + { + node.GetTimestamp().Should().BeLessThanOrEqualTo(clock + duration.raw); + } } [Theory] @@ -108,8 +108,8 @@ public void WhenAdvanceDifferentWheelsNodeIsRescheduled(long clock) lfuNodeList.Count.Should().Be(0); // verify discarded T120 wheelEnumerator.PositionOf(120).Should().Be(WheelPosition.None); - } - + } + [Fact] public void WhenAdvanceOverflowsAndItemIsExpiredItemIsEvicted() { @@ -167,21 +167,21 @@ public void WhenEmptyGetExpirationDelayIsMax(long clock) { timerWheel.time = clock; timerWheel.GetExpirationDelay().raw.Should().Be(long.MaxValue); - } - + } + [Theory] [MemberData(nameof(ClockData))] public void WhenScheduledMaxNodeIsInOuterWheel(long clock) - { + { var clockD = new Duration(clock); - timerWheel.time = clock; - - Duration tMax = clockD + new Duration(long.MaxValue); - - timerWheel.Schedule(AddNode(1, new DisposeTracker(), tMax)); - - var initialPosition = wheelEnumerator.PositionOf(1); - initialPosition.wheel.Should().Be(4); + timerWheel.time = clock; + + Duration tMax = clockD + new Duration(long.MaxValue); + + timerWheel.Schedule(AddNode(1, new DisposeTracker(), tMax)); + + var initialPosition = wheelEnumerator.PositionOf(1); + initialPosition.wheel.Should().Be(4); } [Theory] @@ -237,9 +237,9 @@ public void WhenScheduledInDifferentWheelsDelayIsCorrect(long clock) } [Fact] - public void WhenScheduledThenDescheduledNodeIsRemoved() - { - var node = AddNode(1, new DisposeTracker(), Duration.SinceEpoch()); + public void WhenScheduledThenDescheduledNodeIsRemoved() + { + var node = AddNode(1, new DisposeTracker(), Duration.SinceEpoch()); timerWheel.Schedule(node); wheelEnumerator.PositionOf(1).Should().NotBe(WheelPosition.None); @@ -248,13 +248,13 @@ public void WhenScheduledThenDescheduledNodeIsRemoved() wheelEnumerator.PositionOf(1).Should().Be(WheelPosition.None); node.GetNextInTimeOrder().Should().BeNull(); node.GetPreviousInTimeOrder().Should().BeNull(); - } - + } + [Fact] - public void WhenRescheduledLaterNodeIsMoved() - { - var time = Duration.SinceEpoch(); - var node = AddNode(1, new DisposeTracker(), time); + public void WhenRescheduledLaterNodeIsMoved() + { + var time = Duration.SinceEpoch(); + var node = AddNode(1, new DisposeTracker(), time); timerWheel.Schedule(node); var initial = wheelEnumerator.PositionOf(1); @@ -265,9 +265,9 @@ public void WhenRescheduledLaterNodeIsMoved() } [Fact] - public void WhenDetachedRescheduleIsNoOp() - { - var time = Duration.SinceEpoch(); + public void WhenDetachedRescheduleIsNoOp() + { + var time = Duration.SinceEpoch(); var node = AddNode(1, new DisposeTracker(), time); timerWheel.Reschedule(node); @@ -292,23 +292,23 @@ private TimeOrderNode AddNode(int key, IDisposable value, Dura new object[] { long.MaxValue }, }; - public static IEnumerable ScheduleData = CreateSchedule(); - - private static IEnumerable CreateSchedule() - { - var schedule = new List(); - - foreach (var clock in ClockData) - { - schedule.Add(new object[] { clock.First(), Duration.FromSeconds(10), 0 }); - schedule.Add(new object[] { clock.First(), Duration.FromMinutes(3), 2 }); - schedule.Add(new object[] { clock.First(), Duration.FromMinutes(10), 3 }); - } - - return schedule; - } - } - + public static IEnumerable ScheduleData = CreateSchedule(); + + private static IEnumerable CreateSchedule() + { + var schedule = new List(); + + foreach (var clock in ClockData) + { + schedule.Add(new object[] { clock.First(), Duration.FromSeconds(10), 0 }); + schedule.Add(new object[] { clock.First(), Duration.FromMinutes(3), 2 }); + schedule.Add(new object[] { clock.First(), Duration.FromMinutes(10), 3 }); + } + + return schedule; + } + } + public class DisposeTracker : IDisposable { public bool Expired { get; set; } @@ -327,97 +327,97 @@ public void Dispose() } } - internal class WheelEnumerator : IEnumerable>> - where K : notnull - { - private readonly TimerWheel timerWheel; - private readonly ITestOutputHelper testOutputHelper; - - public WheelEnumerator(TimerWheel timerWheel, ITestOutputHelper testOutputHelper) - { - this.timerWheel = timerWheel; - this.testOutputHelper = testOutputHelper; - } - - public void Dump(string tag = null) - { - this.testOutputHelper.WriteLine(tag); - int count = 0; - - foreach (KeyValuePair> kvp in this) - { - this.testOutputHelper.WriteLine($"[{kvp.Key.wheel},{kvp.Key.bucket}] {kvp.Value.Key}"); - count++; - } - - if (count == 0) - { - this.testOutputHelper.WriteLine("empty"); - } - } - - public WheelPosition PositionOf(K key) - { - var v = this.Where(kvp => EqualityComparer.Default.Equals(kvp.Value.Key, key)); - - if (v.Any()) - { - return v.First().Key; - } - - return WheelPosition.None; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((WheelEnumerator)this).GetEnumerator(); - } - - public IEnumerator>> GetEnumerator() - { - for (int w = 0; w < timerWheel.wheels.Length; w++) - { - var wheel = timerWheel.wheels[w]; - - for (int b = 0; b < wheel.Length; b++) - { - var sentinel = wheel[b]; - var node = sentinel.GetNextInTimeOrder(); - - while (node != sentinel) - { - yield return new KeyValuePair>(new WheelPosition(w, b), node); - node = node.GetNextInTimeOrder(); - } - } - } - } - } - - internal struct WheelPosition : IComparable - { - public readonly int wheel; - public readonly int bucket; - - public static readonly WheelPosition None = new(-1, -1); - - public WheelPosition(int wheel, int bucket) - { - this.wheel = wheel; - this.bucket = bucket; - } - - public static bool operator >(WheelPosition a, WheelPosition b) => a.wheel > b.wheel | (a.wheel == b.wheel && a.bucket > b.bucket); - public static bool operator <(WheelPosition a, WheelPosition b) => a.wheel < b.wheel | (a.wheel == b.wheel && a.bucket < b.bucket); - - public int CompareTo(WheelPosition that) - { - if (this.wheel == that.wheel) - { - return this.bucket.CompareTo(that.bucket); - } - - return this.wheel.CompareTo(that.wheel); - } + internal class WheelEnumerator : IEnumerable>> + where K : notnull + { + private readonly TimerWheel timerWheel; + private readonly ITestOutputHelper testOutputHelper; + + public WheelEnumerator(TimerWheel timerWheel, ITestOutputHelper testOutputHelper) + { + this.timerWheel = timerWheel; + this.testOutputHelper = testOutputHelper; + } + + public void Dump(string tag = null) + { + this.testOutputHelper.WriteLine(tag); + int count = 0; + + foreach (KeyValuePair> kvp in this) + { + this.testOutputHelper.WriteLine($"[{kvp.Key.wheel},{kvp.Key.bucket}] {kvp.Value.Key}"); + count++; + } + + if (count == 0) + { + this.testOutputHelper.WriteLine("empty"); + } + } + + public WheelPosition PositionOf(K key) + { + var v = this.Where(kvp => EqualityComparer.Default.Equals(kvp.Value.Key, key)); + + if (v.Any()) + { + return v.First().Key; + } + + return WheelPosition.None; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((WheelEnumerator)this).GetEnumerator(); + } + + public IEnumerator>> GetEnumerator() + { + for (int w = 0; w < timerWheel.wheels.Length; w++) + { + var wheel = timerWheel.wheels[w]; + + for (int b = 0; b < wheel.Length; b++) + { + var sentinel = wheel[b]; + var node = sentinel.GetNextInTimeOrder(); + + while (node != sentinel) + { + yield return new KeyValuePair>(new WheelPosition(w, b), node); + node = node.GetNextInTimeOrder(); + } + } + } + } + } + + internal struct WheelPosition : IComparable + { + public readonly int wheel; + public readonly int bucket; + + public static readonly WheelPosition None = new(-1, -1); + + public WheelPosition(int wheel, int bucket) + { + this.wheel = wheel; + this.bucket = bucket; + } + + public static bool operator >(WheelPosition a, WheelPosition b) => a.wheel > b.wheel | (a.wheel == b.wheel && a.bucket > b.bucket); + public static bool operator <(WheelPosition a, WheelPosition b) => a.wheel < b.wheel | (a.wheel == b.wheel && a.bucket < b.bucket); + + public int CompareTo(WheelPosition that) + { + if (this.wheel == that.wheel) + { + return this.bucket.CompareTo(that.bucket); + } + + return this.wheel.CompareTo(that.wheel); + } } } diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 9038b593..76194b9f 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -36,7 +36,7 @@ public sealed class ConcurrentLfu : ICacheExt, IAsyncCacheExt, where K : notnull { // Note: for performance reasons this is a mutable struct, it cannot be readonly. - private ConcurrentLfuCore, AccessOrderPolicy> core; + private ConcurrentLfuCore, AccessOrderPolicy, TelemetryPolicy> core; /// /// The default buffer size. @@ -49,7 +49,9 @@ public sealed class ConcurrentLfu : ICacheExt, IAsyncCacheExt, /// The capacity. public ConcurrentLfu(int capacity) { - this.core = new(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default, () => this.DrainBuffers(), default); + var telemetryPolicy = new TelemetryPolicy(); + telemetryPolicy.SetEventSource(this); + this.core = new(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default, () => this.DrainBuffers(), default, telemetryPolicy); } /// @@ -61,10 +63,12 @@ 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.DrainBuffers(), default); + var telemetryPolicy = new TelemetryPolicy(); + telemetryPolicy.SetEventSource(this); + this.core = new(concurrencyLevel, capacity, scheduler, comparer, () => this.DrainBuffers(), default, telemetryPolicy); } - internal ConcurrentLfuCore, AccessOrderPolicy> Core => core; + internal ConcurrentLfuCore, AccessOrderPolicy, TelemetryPolicy> Core => core; // structs cannot declare self referencing lambda functions, therefore pass this in from the ctor private void DrainBuffers() @@ -79,7 +83,7 @@ private void DrainBuffers() public Optional Metrics => core.Metrics; /// - public Optional> Events => Optional>.None(); + public Optional> Events => core.Events; /// public CachePolicy Policy => core.Policy; diff --git a/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs b/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs index 0e4a3c3c..5fba7880 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using BitFaster.Caching.Buffers; using BitFaster.Caching.Counters; +using BitFaster.Caching.Lru; using BitFaster.Caching.Scheduler; #if DEBUG @@ -39,10 +40,11 @@ namespace BitFaster.Caching.Lfu /// 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 : IBoundedPolicy where K : notnull where N : LfuNode where P : struct, INodePolicy + where T : struct, ITelemetryPolicy { private const int MaxWriteBufferRetries = 64; @@ -78,7 +80,16 @@ internal struct ConcurrentLfuCore : IBoundedPolicy internal P policy; - public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer, Action drainBuffers, P policy) + /// + /// The telemetry policy. + /// + /// + /// Since T is a struct, making it readonly will force the runtime to make defensive copies + /// if mutate methods are called. Therefore, field must be mutable to maintain count. + /// + internal T telemetryPolicy; + + public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer, Action drainBuffers, P policy, T telemetryPolicy) { if (capacity < 3) Throw.ArgOutOfRange(nameof(capacity)); @@ -108,6 +119,7 @@ public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler schedule this.drainBuffers = drainBuffers; this.policy = policy; + this.telemetryPolicy = telemetryPolicy; } // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ @@ -115,7 +127,9 @@ public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler schedule public int Capacity => this.capacity.Capacity; - public Optional Metrics => new(this.metrics); + public Optional Metrics => CreateMetrics(ref this); + + public Optional> Events => CreateEvents(ref this); public CachePolicy Policy => new(new Optional(this), Optional.None()); @@ -143,7 +157,7 @@ public void AddOrUpdate(K key, V value) public void Clear() { - Trim(int.MaxValue); + TrimInternal(int.MaxValue, ItemRemovedReason.Cleared); lock (maintenanceLock) { @@ -154,6 +168,11 @@ public void Clear() } public void Trim(int itemCount) + { + TrimInternal(itemCount, ItemRemovedReason.Trimmed); + } + + private void TrimInternal(int itemCount, ItemRemovedReason reason) { List> candidates; lock (maintenanceLock) @@ -177,7 +196,7 @@ public void Trim(int itemCount) foreach (var candidate in candidates) #endif { - this.TryRemove(candidate.Key); + this.TryRemoveInternal(candidate.Key, out _, reason); } } @@ -336,6 +355,7 @@ public bool TryRemove(KeyValuePair item) #endif { node.WasRemoved = true; + this.telemetryPolicy.OnItemRemoved(item.Key, node.Value, ItemRemovedReason.Removed); AfterWrite(node); return true; } @@ -347,10 +367,21 @@ public bool TryRemove(KeyValuePair item) } public bool TryRemove(K key, [MaybeNullWhen(false)] out V value) + { + return TryRemoveInternal(key, out value, ItemRemovedReason.Removed); + } + + public bool TryRemove(K key) + { + return this.TryRemove(key, out var _); + } + + private bool TryRemoveInternal(K key, [MaybeNullWhen(false)] out V value, ItemRemovedReason reason) { if (this.dictionary.TryRemove(key, out var node)) { node.WasRemoved = true; + this.telemetryPolicy.OnItemRemoved(key, node.Value, reason); AfterWrite(node); value = node.Value; return true; @@ -360,11 +391,6 @@ public bool TryRemove(K key, [MaybeNullWhen(false)] 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)) @@ -373,8 +399,17 @@ public bool TryUpdate(K key, V value) { if (!node.WasRemoved) { + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + var oldValue = node.Value; +#endif node.Value = value; + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + this.telemetryPolicy.OnItemUpdated(key, oldValue, value); +#endif + // It's ok for this to be lossy, since the node is already tracked // and we will just lose ordering/hit count, but not orphan the node. this.writeBuffer.TryAdd(node); @@ -839,6 +874,7 @@ internal void Evict(LfuNode evictee) ((ICollection>)this.dictionary).Remove(kvp); #endif evictee.list?.Remove(evictee); + this.telemetryPolicy.OnItemRemoved(evictee.Key, evictee.Value, ItemRemovedReason.Evicted); Disposer.Dispose(evictee.Value); this.metrics.evictedCount++; @@ -931,6 +967,84 @@ internal string Format() } } + private static Optional CreateMetrics(ref ConcurrentLfuCore lfu) + { + if (typeof(T) == typeof(NoTelemetryPolicy)) + { + return Optional.None(); + } + + return new(new Proxy(ref lfu)); + } + + private static Optional> CreateEvents(ref ConcurrentLfuCore lfu) + { + if (typeof(T) == typeof(NoTelemetryPolicy)) + { + return Optional>.None(); + } + + return new(new Proxy(ref lfu)); + } + + // To get JIT optimizations, policies must be structs. + // If the structs are returned directly via properties, they will be copied. Since + // telemetryPolicy is a mutable struct, copy is bad. One workaround is to store the + // state within the struct in an object. Since the struct points to the same object + // it becomes immutable. However, this object is then somewhere else on the + // heap, which slows down the policies with hit counter logic in benchmarks. Likely + // this approach keeps the structs data members in the same CPU cache line as the LFU. + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + [DebuggerDisplay("Hit = {Hits}, Miss = {Misses}, Upd = {Updated}, Evict = {Evicted}")] +#else + [DebuggerDisplay("Hit = {Hits}, Miss = {Misses}, Evict = {Evicted}")] +#endif + private class Proxy : ICacheMetrics, ICacheEvents, IBoundedPolicy + { + private readonly ConcurrentLfuCore lfu; + + public Proxy(ref ConcurrentLfuCore lfu) + { + this.lfu = lfu; + } + + public double HitRatio => lfu.telemetryPolicy.HitRatio; + + public long Total => lfu.telemetryPolicy.Total; + + public long Hits => lfu.telemetryPolicy.Hits; + + public long Misses => lfu.telemetryPolicy.Misses; + + public long Evicted => lfu.telemetryPolicy.Evicted; + + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + public long Updated => lfu.telemetryPolicy.Updated; +#endif + public int Capacity => lfu.Capacity; + + public event EventHandler> ItemRemoved + { + add { this.lfu.telemetryPolicy.ItemRemoved += value; } + remove { this.lfu.telemetryPolicy.ItemRemoved -= value; } + } + + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + public event EventHandler> ItemUpdated + { + add { this.lfu.telemetryPolicy.ItemUpdated += value; } + remove { this.lfu.telemetryPolicy.ItemUpdated -= value; } + } +#endif + public void Trim(int itemCount) + { + lfu.Trim(itemCount); + } + } + [DebuggerDisplay("Hit = {Hits}, Miss = {Misses}, Upd = {Updated}, Evict = {Evicted}")] internal class CacheMetrics : ICacheMetrics { diff --git a/BitFaster.Caching/Lfu/ConcurrentTLfu.cs b/BitFaster.Caching/Lfu/ConcurrentTLfu.cs index 439fddf1..151bb230 100644 --- a/BitFaster.Caching/Lfu/ConcurrentTLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentTLfu.cs @@ -13,16 +13,20 @@ internal sealed class ConcurrentTLfu : ICacheExt, IAsyncCacheExt, ExpireAfterPolicy> core; + private ConcurrentLfuCore, ExpireAfterPolicy, TelemetryPolicy> core; public ConcurrentTLfu(int capacity, IExpiryCalculator expiryCalculator) { - this.core = new(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default, () => this.DrainBuffers(), new(expiryCalculator)); + var telemetryPolicy = new TelemetryPolicy(); + telemetryPolicy.SetEventSource(this); + this.core = new(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default, () => this.DrainBuffers(), new(expiryCalculator), telemetryPolicy); } public ConcurrentTLfu(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer, IExpiryCalculator expiryCalculator) { - this.core = new(concurrencyLevel, capacity, scheduler, comparer, () => this.DrainBuffers(), new(expiryCalculator)); + var telemetryPolicy = new TelemetryPolicy(); + telemetryPolicy.SetEventSource(this); + this.core = new(concurrencyLevel, capacity, scheduler, comparer, () => this.DrainBuffers(), new(expiryCalculator), telemetryPolicy); } // structs cannot declare self referencing lambda functions, therefore pass this in from the ctor @@ -38,7 +42,7 @@ private void DrainBuffers() public Optional Metrics => core.Metrics; /// - public Optional> Events => Optional>.None(); + public Optional> Events => core.Events; /// public CachePolicy Policy => CreatePolicy(); diff --git a/BitFaster.Caching/Lfu/NodePolicy.cs b/BitFaster.Caching/Lfu/NodePolicy.cs index 89b1861c..cc74bf9f 100644 --- a/BitFaster.Caching/Lfu/NodePolicy.cs +++ b/BitFaster.Caching/Lfu/NodePolicy.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using BitFaster.Caching.Lru; namespace BitFaster.Caching.Lfu { @@ -15,7 +16,7 @@ internal interface INodePolicy void AfterRead(N node); void AfterWrite(N node); void OnEvict(N node); - void ExpireEntries

(ref ConcurrentLfuCore cache) where P : struct, INodePolicy; + void ExpireEntries(ref ConcurrentLfuCore cache) where P : struct, INodePolicy where T : struct, ITelemetryPolicy; } internal struct AccessOrderPolicy : INodePolicy> @@ -59,7 +60,7 @@ public void OnEvict(AccessOrderNode node) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ExpireEntries

(ref ConcurrentLfuCore, P> cache) where P : struct, INodePolicy> + public void ExpireEntries(ref ConcurrentLfuCore, P, T> cache) where P : struct, INodePolicy> where T : struct, ITelemetryPolicy { } } @@ -137,7 +138,7 @@ public void OnEvict(TimeOrderNode node) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ExpireEntries

(ref ConcurrentLfuCore, P> cache) where P : struct, INodePolicy> + public void ExpireEntries(ref ConcurrentLfuCore, P, T> cache) where P : struct, INodePolicy> where T : struct, ITelemetryPolicy { wheel.Advance(ref cache, Duration.SinceEpoch()); } diff --git a/BitFaster.Caching/Lfu/TimerWheel.cs b/BitFaster.Caching/Lfu/TimerWheel.cs index cc4675a5..2eaf9ae2 100644 --- a/BitFaster.Caching/Lfu/TimerWheel.cs +++ b/BitFaster.Caching/Lfu/TimerWheel.cs @@ -1,4 +1,5 @@ using System; +using BitFaster.Caching.Lru; namespace BitFaster.Caching.Lfu { @@ -61,9 +62,10 @@ public TimerWheel() ///

/// /// - public void Advance(ref ConcurrentLfuCore cache, Duration currentTime) + public void Advance(ref ConcurrentLfuCore cache, Duration currentTime) where N : LfuNode where P : struct, INodePolicy + where T : struct, ITelemetryPolicy { long previousTime = time; time = currentTime.raw; @@ -101,9 +103,10 @@ public void Advance(ref ConcurrentLfuCore cache, Duration curr } // Expires entries or reschedules into the proper bucket if still active. - private void Expire(ref ConcurrentLfuCore cache, int index, long previousTicks, long delta) + private void Expire(ref ConcurrentLfuCore cache, int index, long previousTicks, long delta) where N : LfuNode where P : struct, INodePolicy + where T : struct, ITelemetryPolicy { TimeOrderNode[] timerWheel = wheels[index]; int mask = timerWheel.Length - 1;