diff --git a/BitFaster.Caching.Benchmarks/DisposerBench.cs b/BitFaster.Caching.Benchmarks/DisposerBench.cs index cdb02b99..ecafbd61 100644 --- a/BitFaster.Caching.Benchmarks/DisposerBench.cs +++ b/BitFaster.Caching.Benchmarks/DisposerBench.cs @@ -13,7 +13,7 @@ namespace BitFaster.Caching.Benchmarks // https://github.com/dotnet/runtime/issues/4920 [SimpleJob(RuntimeMoniker.Net48)] [SimpleJob(RuntimeMoniker.Net60)] - [DisassemblyDiagnoser(printSource: true, maxDepth:3)] + [DisassemblyDiagnoser(printSource: true, maxDepth: 3)] [MemoryDiagnoser] public class DisposerBench { diff --git a/BitFaster.Caching.Benchmarks/Lru/LruCycleBench.cs b/BitFaster.Caching.Benchmarks/Lru/LruCycleBench.cs index 1b402524..edde4829 100644 --- a/BitFaster.Caching.Benchmarks/Lru/LruCycleBench.cs +++ b/BitFaster.Caching.Benchmarks/Lru/LruCycleBench.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -16,13 +17,14 @@ namespace BitFaster.Caching.Benchmarks.Lru // DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT - //| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Code Size | Allocated | - //|------------------- |---------:|---------:|---------:|------:|--------:|-------:|----------:|----------:| - //| FastConcurrentLru | 23.25 us | 0.128 us | 0.114 us | 1.00 | 0.00 | 2.1362 | 5 KB | 9 KB | - //| ConcurrentLru | 23.78 us | 0.116 us | 0.097 us | 1.02 | 0.01 | 2.1362 | 5 KB | 9 KB | - //| FastConcurrentTLru | 32.17 us | 0.463 us | 0.433 us | 1.38 | 0.02 | 2.3193 | 6 KB | 10 KB | - //| ConcurrentTLru | 32.52 us | 0.386 us | 0.361 us | 1.40 | 0.02 | 2.3193 | 6 KB | 10 KB | - //| ClassicLru | 16.29 us | 0.195 us | 0.163 us | 0.70 | 0.01 | 3.2959 | 5 KB | 14 KB | + //| Method | Mean | Error | StdDev | Ratio | Code Size | Gen 0 | Allocated | + //|------------------- |---------:|---------:|---------:|------:|----------:|-------:|----------:| + //| FastConcurrentLru | 22.86 us | 0.183 us | 0.162 us | 1.00 | 5 KB | 2.1362 | 9 KB | + //| ConcurrentLru | 23.40 us | 0.092 us | 0.077 us | 1.02 | 5 KB | 2.1362 | 9 KB | + //| ConcurrentLruEvent | 24.23 us | 0.097 us | 0.086 us | 1.06 | 5 KB | 3.0823 | 13 KB | + //| FastConcurrentTLru | 31.70 us | 0.087 us | 0.077 us | 1.39 | 6 KB | 2.3193 | 10 KB | + //| ConcurrentTLru | 31.85 us | 0.080 us | 0.071 us | 1.39 | 6 KB | 2.3193 | 10 KB | + //| ClassicLru | 16.35 us | 0.091 us | 0.076 us | 0.72 | 4 KB | 3.2959 | 14 KB | [SimpleJob(RuntimeMoniker.Net48)] [SimpleJob(RuntimeMoniker.Net60)] [DisassemblyDiagnoser(printSource: true, maxDepth: 5)] @@ -31,11 +33,26 @@ public class LruCycleBench { private static readonly ClassicLru classicLru = new ClassicLru(8, 9, EqualityComparer.Default); private static readonly ConcurrentLru concurrentLru = new ConcurrentLru(8, 9, EqualityComparer.Default); + private static readonly ConcurrentLru concurrentLruEvent = new ConcurrentLru(8, 9, EqualityComparer.Default); private static readonly ConcurrentTLru concurrentTlru = new ConcurrentTLru(8, 9, EqualityComparer.Default, TimeSpan.FromMinutes(10)); private static readonly FastConcurrentLru fastConcurrentLru = new FastConcurrentLru(8, 9, EqualityComparer.Default); private static readonly FastConcurrentTLru fastConcurrentTLru = new FastConcurrentTLru(8, 9, EqualityComparer.Default, TimeSpan.FromMinutes(1)); - [Benchmark(Baseline = true)] + [GlobalSetup] + public void GlobalSetup() + { + concurrentLruEvent.ItemRemoved += OnItemRemoved; + } + + public static int field; + + [MethodImpl(MethodImplOptions.NoOptimization)] + private void OnItemRemoved(object sender, ItemRemovedEventArgs e) + { + field = e.Key; + } + + [Benchmark(Baseline =true)] public void FastConcurrentLru() { Func func = x => x; @@ -53,6 +70,15 @@ public void ConcurrentLru() concurrentLru.GetOrAdd(i, func); } + [Benchmark()] + public void ConcurrentLruEvent() + { + Func func = x => x; + + for (int i = 0; i < 128; i++) + concurrentLruEvent.GetOrAdd(i, func); + } + [Benchmark()] public void FastConcurrentTLru() { diff --git a/BitFaster.Caching.Benchmarks/Lru/TLruTimeBenchmark.cs b/BitFaster.Caching.Benchmarks/Lru/TLruTimeBenchmark.cs index a8ed8f0b..6fede73c 100644 --- a/BitFaster.Caching.Benchmarks/Lru/TLruTimeBenchmark.cs +++ b/BitFaster.Caching.Benchmarks/Lru/TLruTimeBenchmark.cs @@ -14,17 +14,17 @@ namespace BitFaster.Caching.Benchmarks.Lru [SimpleJob(RuntimeMoniker.Net60)] public class TLruTimeBenchmark { - private static readonly TemplateConcurrentLru, TLruDateTimePolicy, NullHitCounter> dateTimeTLru - = new TemplateConcurrentLru, TLruDateTimePolicy, NullHitCounter> - (1, 3, EqualityComparer.Default, new TLruDateTimePolicy(TimeSpan.FromSeconds(1)), new NullHitCounter()); + private static readonly TemplateConcurrentLru, TLruDateTimePolicy, NoTelemetryPolicy> dateTimeTLru + = new TemplateConcurrentLru, TLruDateTimePolicy, NoTelemetryPolicy> + (1, 3, EqualityComparer.Default, new TLruDateTimePolicy(TimeSpan.FromSeconds(1)), default); - private static readonly TemplateConcurrentLru, TLruTicksPolicy, NullHitCounter> tickCountTLru - = new TemplateConcurrentLru, TLruTicksPolicy, NullHitCounter> - (1, 3, EqualityComparer.Default, new TLruTicksPolicy(TimeSpan.FromSeconds(1)), new NullHitCounter()); + private static readonly TemplateConcurrentLru, TLruTicksPolicy, NoTelemetryPolicy> tickCountTLru + = new TemplateConcurrentLru, TLruTicksPolicy, NoTelemetryPolicy> + (1, 3, EqualityComparer.Default, new TLruTicksPolicy(TimeSpan.FromSeconds(1)), default); - private static readonly TemplateConcurrentLru, TLruLongTicksPolicy, NullHitCounter> stopwatchTLru - = new TemplateConcurrentLru, TLruLongTicksPolicy, NullHitCounter> - (1, 3, EqualityComparer.Default, new TLruLongTicksPolicy(TimeSpan.FromSeconds(1)), new NullHitCounter()); + private static readonly TemplateConcurrentLru, TLruLongTicksPolicy, NoTelemetryPolicy> stopwatchTLru + = new TemplateConcurrentLru, TLruLongTicksPolicy, NoTelemetryPolicy> + (1, 3, EqualityComparer.Default, new TLruLongTicksPolicy(TimeSpan.FromSeconds(1)), default); [Benchmark(Baseline = true)] public void DateTimeUtcNow() diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs index 90d9cf70..e84008f1 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs @@ -20,6 +20,13 @@ public class ConcurrentLruTests private ConcurrentLru lru = new ConcurrentLru(1, hotCap + warmCap + coldCap, EqualityComparer.Default); private ValueFactory valueFactory = new ValueFactory(); + private List> removedItems = new List>(); + + private void OnLruItemRemoved(object sender, ItemRemovedEventArgs e) + { + removedItems.Add(e); + } + public ConcurrentLruTests(ITestOutputHelper testOutputHelper) { this.testOutputHelper = testOutputHelper; @@ -359,6 +366,44 @@ public void WhenValueExpiresItIsDisposed() disposableValueFactory.Items[1].IsDisposed.Should().BeFalse(); } + [Fact] + public void WhenValueEvictedItemRemovedEventIsFired() + { + var lruEvents = new ConcurrentLru(1, 6, EqualityComparer.Default); + lruEvents.ItemRemoved += OnLruItemRemoved; + + for (int i = 0; i < 6; i++) + { + lruEvents.GetOrAdd(i+1, i => i + 1); + } + + removedItems.Count.Should().Be(2); + + removedItems[0].Key.Should().Be(1); + removedItems[0].Value.Should().Be(2); + removedItems[0].Reason.Should().Be(ItemRemovedReason.Evicted); + + removedItems[1].Key.Should().Be(2); + removedItems[1].Value.Should().Be(3); + removedItems[1].Reason.Should().Be(ItemRemovedReason.Evicted); + } + + [Fact] + public void WhenItemRemovedEventIsUnregisteredEventIsNotFired() + { + var lruEvents = new ConcurrentLru(1, 6, EqualityComparer.Default); + + lruEvents.ItemRemoved += OnLruItemRemoved; + lruEvents.ItemRemoved -= OnLruItemRemoved; + + for (int i = 0; i < 6; i++) + { + lruEvents.GetOrAdd(i + 1, i => i + 1); + } + + removedItems.Count.Should().Be(0); + } + [Fact] public void WhenKeyExistsTryRemoveRemovesItemAndReturnsTrue() { @@ -380,6 +425,22 @@ public void WhenItemIsRemovedItIsDisposed() disposableValueFactory.Items[1].IsDisposed.Should().BeTrue(); } + [Fact] + public void WhenItemIsRemovedRemovedEventIsFired() + { + var lruEvents = new ConcurrentLru(1, 6, EqualityComparer.Default); + lruEvents.ItemRemoved += OnLruItemRemoved; + + lruEvents.GetOrAdd(1, i => i+2); + + lruEvents.TryRemove(1).Should().BeTrue(); + + removedItems.Count().Should().Be(1); + removedItems[0].Key.Should().Be(1); + removedItems[0].Value.Should().Be(3); + removedItems[0].Reason.Should().Be(ItemRemovedReason.Removed); + } + [Fact] public void WhenKeyDoesNotExistTryRemoveReturnsFalse() { diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentTLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentTLruTests.cs index 4157e492..c274ef3b 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentTLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentTLruTests.cs @@ -16,6 +16,13 @@ public class ConcurrentTLruTests private ValueFactory valueFactory = new ValueFactory(); + private List> removedItems = new List>(); + + private void OnLruItemRemoved(object sender, ItemRemovedEventArgs e) + { + removedItems.Add(e); + } + public ConcurrentTLruTests() { lru = new ConcurrentTLru(1, capacity, EqualityComparer.Default, timeToLive); @@ -47,6 +54,44 @@ public async Task WhenItemIsExpiredItIsRemoved() lru.TryGet(1, out var value).Should().BeFalse(); } + [Fact] + public void WhenValueEvictedItemRemovedEventIsFired() + { + var lruEvents = new ConcurrentTLru(1, 6, EqualityComparer.Default, timeToLive); + lruEvents.ItemRemoved += OnLruItemRemoved; + + for (int i = 0; i < 6; i++) + { + lruEvents.GetOrAdd(i + 1, i => i + 1); + } + + removedItems.Count.Should().Be(2); + + removedItems[0].Key.Should().Be(1); + removedItems[0].Value.Should().Be(2); + removedItems[0].Reason.Should().Be(ItemRemovedReason.Evicted); + + removedItems[1].Key.Should().Be(2); + removedItems[1].Value.Should().Be(3); + removedItems[1].Reason.Should().Be(ItemRemovedReason.Evicted); + } + + [Fact] + public void WhenItemRemovedEventIsUnregisteredEventIsNotFired() + { + var lruEvents = new ConcurrentTLru(1, 6, EqualityComparer.Default, timeToLive); + + lruEvents.ItemRemoved += OnLruItemRemoved; + lruEvents.ItemRemoved -= OnLruItemRemoved; + + for (int i = 0; i < 6; i++) + { + lruEvents.GetOrAdd(i + 1, i => i + 1); + } + + removedItems.Count.Should().Be(0); + } + [Fact] public void WhenItemIsAddedThenRetrievedHitRatioIsHalf() { diff --git a/BitFaster.Caching.UnitTests/Lru/HitCounterTests.cs b/BitFaster.Caching.UnitTests/Lru/HitCounterTests.cs deleted file mode 100644 index d973703e..00000000 --- a/BitFaster.Caching.UnitTests/Lru/HitCounterTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -using FluentAssertions; -using BitFaster.Caching.Lru; -using System; -using System.Collections.Generic; -using System.Text; -using Xunit; - -namespace BitFaster.Caching.UnitTests.Lru -{ - public class HitCounterTests - { - [Fact] - public void WhenHitCountAndTotalCountAreEqualRatioIs1() - { - HitCounter counter = new HitCounter(); - - counter.IncrementHit(); - - counter.HitRatio.Should().Be(1.0); - } - - [Fact] - public void WhenHitCountIsEqualToMissCountRatioIsHalf() - { - HitCounter counter = new HitCounter(); - - counter.IncrementMiss(); - counter.IncrementHit(); - - counter.HitRatio.Should().Be(0.5); - } - - [Fact] - public void WhenTotalCountIsZeroRatioReturnsZero() - { - HitCounter counter = new HitCounter(); - - counter.HitRatio.Should().Be(0.0); - } - } -} diff --git a/BitFaster.Caching.UnitTests/Lru/NullHitCounterTests.cs b/BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs similarity index 82% rename from BitFaster.Caching.UnitTests/Lru/NullHitCounterTests.cs rename to BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs index 14372a82..27814eab 100644 --- a/BitFaster.Caching.UnitTests/Lru/NullHitCounterTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs @@ -7,9 +7,9 @@ namespace BitFaster.Caching.UnitTests.Lru { - public class NullHitCounterTests + public class NoTelemetryPolicyTests { - private NullHitCounter counter = new NullHitCounter(); + private NoTelemetryPolicy counter = new NoTelemetryPolicy(); [Fact] public void HitRatioIsZero() diff --git a/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs new file mode 100644 index 00000000..007231dc --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs @@ -0,0 +1,66 @@ +using FluentAssertions; +using BitFaster.Caching.Lru; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lru +{ + public class TelemetryPolicyTests + { + private TelemetryPolicy telemetryPolicy = default; + + [Fact] + public void WhenHitCountAndTotalCountAreEqualRatioIs1() + { + telemetryPolicy.IncrementHit(); + + telemetryPolicy.HitRatio.Should().Be(1.0); + } + + [Fact] + public void WhenHitCountIsEqualToMissCountRatioIsHalf() + { + telemetryPolicy.IncrementMiss(); + telemetryPolicy.IncrementHit(); + + telemetryPolicy.HitRatio.Should().Be(0.5); + } + + [Fact] + public void WhenTotalCountIsZeroRatioReturnsZero() + { + telemetryPolicy.HitRatio.Should().Be(0.0); + } + + [Fact] + public void WhenOnItemRemovedInvokedEventIsFired() + { + List> eventList = new(); + + telemetryPolicy.ItemRemoved += (source, args) => eventList.Add(args); + + telemetryPolicy.OnItemRemoved(1, 2, ItemRemovedReason.Evicted); + + eventList.Should().HaveCount(1); + eventList[0].Key.Should().Be(1); + eventList[0].Value.Should().Be(2); + eventList[0].Reason.Should().Be(ItemRemovedReason.Evicted); + } + + [Fact] + public void WhenEventSourceIsSetItemRemovedEventUsesSource() + { + List eventSourceList = new(); + + telemetryPolicy.ItemRemoved += (source, args) => eventSourceList.Add(source); + + telemetryPolicy.SetEventSource(this); + telemetryPolicy.OnItemRemoved(1, 2, ItemRemovedReason.Evicted); + + eventSourceList.Should().HaveCount(1); + eventSourceList[0].Should().Be(this); + } + } +} diff --git a/BitFaster.Caching/Disposer.cs b/BitFaster.Caching/Disposer.cs index 6cd42ce6..440cd198 100644 --- a/BitFaster.Caching/Disposer.cs +++ b/BitFaster.Caching/Disposer.cs @@ -8,7 +8,7 @@ namespace BitFaster.Caching { /// - /// A generic wrapper for object disposal. Enables JIT to inline/remove object disposal if statement reducing code size. + /// A generic wrapper for object disposal. /// /// The type of object to dispose public static class Disposer diff --git a/BitFaster.Caching/Lifetime.cs b/BitFaster.Caching/Lifetime.cs index 74ebf899..378d3eea 100644 --- a/BitFaster.Caching/Lifetime.cs +++ b/BitFaster.Caching/Lifetime.cs @@ -9,7 +9,7 @@ namespace BitFaster.Caching /// lifetime is disposed. /// /// The type of value - public class Lifetime : IDisposable + public sealed class Lifetime : IDisposable { private readonly Action onDisposeAction; private readonly ReferenceCount refCount; diff --git a/BitFaster.Caching/Lru/ConcurrentLru.cs b/BitFaster.Caching/Lru/ConcurrentLru.cs index de133902..e0a5ccc7 100644 --- a/BitFaster.Caching/Lru/ConcurrentLru.cs +++ b/BitFaster.Caching/Lru/ConcurrentLru.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Lru { /// - public sealed class ConcurrentLru : TemplateConcurrentLru, LruPolicy, HitCounter> + public sealed class ConcurrentLru : TemplateConcurrentLru, LruPolicy, TelemetryPolicy> { /// /// Initializes a new instance of the ConcurrentLru class with the specified capacity that has the default @@ -15,7 +15,7 @@ public sealed class ConcurrentLru : TemplateConcurrentLru /// The maximum number of elements that the ConcurrentLru can contain. public ConcurrentLru(int capacity) - : base(Defaults.ConcurrencyLevel, capacity, EqualityComparer.Default, new LruPolicy(), new HitCounter()) + : base(Defaults.ConcurrencyLevel, capacity, EqualityComparer.Default, default, default) { } @@ -27,13 +27,22 @@ public ConcurrentLru(int capacity) /// The maximum number of elements that the ConcurrentLru can contain. /// The IEqualityComparer implementation to use when comparing keys. public ConcurrentLru(int concurrencyLevel, int capacity, IEqualityComparer comparer) - : base(concurrencyLevel, capacity, comparer, new LruPolicy(), new HitCounter()) + : base(concurrencyLevel, capacity, comparer, default, default) { } /// /// Gets the ratio of hits to misses, where a value of 1 indicates 100% hits. /// - public double HitRatio => this.hitCounter.HitRatio; + public double HitRatio => this.telemetryPolicy.HitRatio; + + /// + /// Occurs when an item is removed from the cache. + /// + public event EventHandler> ItemRemoved + { + add { this.telemetryPolicy.ItemRemoved += value; } + remove { this.telemetryPolicy.ItemRemoved -= value; } + } } } diff --git a/BitFaster.Caching/Lru/ConcurrentTLru.cs b/BitFaster.Caching/Lru/ConcurrentTLru.cs index 72fed08c..1bca36bc 100644 --- a/BitFaster.Caching/Lru/ConcurrentTLru.cs +++ b/BitFaster.Caching/Lru/ConcurrentTLru.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Lru { /// - public sealed class ConcurrentTLru : TemplateConcurrentLru, TLruLongTicksPolicy, HitCounter> + public sealed class ConcurrentTLru : TemplateConcurrentLru, TLruLongTicksPolicy, TelemetryPolicy> { /// /// Initializes a new instance of the ConcurrentTLru class with the specified capacity and time to live that has the default @@ -16,7 +16,7 @@ public sealed class ConcurrentTLru : TemplateConcurrentLruThe maximum number of elements that the ConcurrentTLru can contain. /// The time to live for cached values. public ConcurrentTLru(int capacity, TimeSpan timeToLive) - : base(Defaults.ConcurrencyLevel, capacity, EqualityComparer.Default, new TLruLongTicksPolicy(timeToLive), new HitCounter()) + : base(Defaults.ConcurrencyLevel, capacity, EqualityComparer.Default, new TLruLongTicksPolicy(timeToLive), default) { } @@ -29,13 +29,22 @@ public ConcurrentTLru(int capacity, TimeSpan timeToLive) /// The IEqualityComparer implementation to use when comparing keys. /// The time to live for cached values. public ConcurrentTLru(int concurrencyLevel, int capacity, IEqualityComparer comparer, TimeSpan timeToLive) - : base(concurrencyLevel, capacity, comparer, new TLruLongTicksPolicy(timeToLive), new HitCounter()) + : base(concurrencyLevel, capacity, comparer, new TLruLongTicksPolicy(timeToLive), default) { } /// /// Gets the ratio of hits to misses, where a value of 1 indicates 100% hits. /// - public double HitRatio => this.hitCounter.HitRatio; + public double HitRatio => this.telemetryPolicy.HitRatio; + + /// + /// Occurs when an item is removed from the cache. + /// + public event EventHandler> ItemRemoved + { + add { this.telemetryPolicy.ItemRemoved += value; } + remove { this.telemetryPolicy.ItemRemoved -= value; } + } } } diff --git a/BitFaster.Caching/Lru/FastConcurrentLru.cs b/BitFaster.Caching/Lru/FastConcurrentLru.cs index 10db549d..8539b313 100644 --- a/BitFaster.Caching/Lru/FastConcurrentLru.cs +++ b/BitFaster.Caching/Lru/FastConcurrentLru.cs @@ -5,7 +5,7 @@ namespace BitFaster.Caching.Lru { /// - public sealed class FastConcurrentLru : TemplateConcurrentLru, LruPolicy, NullHitCounter> + public sealed class FastConcurrentLru : TemplateConcurrentLru, LruPolicy, NoTelemetryPolicy> { /// /// Initializes a new instance of the FastConcurrentLru class with the specified capacity that has the default @@ -13,7 +13,7 @@ public sealed class FastConcurrentLru : TemplateConcurrentLru /// The maximum number of elements that the FastConcurrentLru can contain. public FastConcurrentLru(int capacity) - : base(Defaults.ConcurrencyLevel, capacity, EqualityComparer.Default, new LruPolicy(), new NullHitCounter()) + : base(Defaults.ConcurrencyLevel, capacity, EqualityComparer.Default, default, default) { } @@ -25,7 +25,7 @@ public FastConcurrentLru(int capacity) /// The maximum number of elements that the FastConcurrentLru can contain. /// The IEqualityComparer implementation to use when comparing keys. public FastConcurrentLru(int concurrencyLevel, int capacity, IEqualityComparer comparer) - : base(concurrencyLevel, capacity, comparer, new LruPolicy(), new NullHitCounter()) + : base(concurrencyLevel, capacity, comparer, default, default) { } } diff --git a/BitFaster.Caching/Lru/FastConcurrentTLru.cs b/BitFaster.Caching/Lru/FastConcurrentTLru.cs index 75e0545d..90bb70af 100644 --- a/BitFaster.Caching/Lru/FastConcurrentTLru.cs +++ b/BitFaster.Caching/Lru/FastConcurrentTLru.cs @@ -5,7 +5,7 @@ namespace BitFaster.Caching.Lru { /// - public sealed class FastConcurrentTLru : TemplateConcurrentLru, TLruLongTicksPolicy, NullHitCounter> + public sealed class FastConcurrentTLru : TemplateConcurrentLru, TLruLongTicksPolicy, NoTelemetryPolicy> { /// /// Initializes a new instance of the FastConcurrentTLru class with the specified capacity and time to live that has the default @@ -14,7 +14,7 @@ public sealed class FastConcurrentTLru : TemplateConcurrentLruThe maximum number of elements that the FastConcurrentTLru can contain. /// The time to live for cached values. public FastConcurrentTLru(int capacity, TimeSpan timeToLive) - : base(Defaults.ConcurrencyLevel, capacity, EqualityComparer.Default, new TLruLongTicksPolicy(timeToLive), new NullHitCounter()) + : base(Defaults.ConcurrencyLevel, capacity, EqualityComparer.Default, new TLruLongTicksPolicy(timeToLive), default) { } @@ -27,7 +27,7 @@ public FastConcurrentTLru(int capacity, TimeSpan timeToLive) /// The IEqualityComparer implementation to use when comparing keys. /// The time to live for cached values. public FastConcurrentTLru(int concurrencyLevel, int capacity, IEqualityComparer comparer, TimeSpan timeToLive) - : base(concurrencyLevel, capacity, comparer, new TLruLongTicksPolicy(timeToLive), new NullHitCounter()) + : base(concurrencyLevel, capacity, comparer, new TLruLongTicksPolicy(timeToLive), default) { } } diff --git a/BitFaster.Caching/Lru/IPolicy.cs b/BitFaster.Caching/Lru/IItemPolicy.cs similarity index 84% rename from BitFaster.Caching/Lru/IPolicy.cs rename to BitFaster.Caching/Lru/IItemPolicy.cs index 8989bd72..65593239 100644 --- a/BitFaster.Caching/Lru/IPolicy.cs +++ b/BitFaster.Caching/Lru/IItemPolicy.cs @@ -6,7 +6,7 @@ namespace BitFaster.Caching.Lru { - public interface IPolicy where I : LruItem + public interface IItemPolicy where I : LruItem { I CreateItem(K key, V value); diff --git a/BitFaster.Caching/Lru/IHitCounter.cs b/BitFaster.Caching/Lru/ITelemetryPolicy.cs similarity index 62% rename from BitFaster.Caching/Lru/IHitCounter.cs rename to BitFaster.Caching/Lru/ITelemetryPolicy.cs index 5dff4450..64246e73 100644 --- a/BitFaster.Caching/Lru/IHitCounter.cs +++ b/BitFaster.Caching/Lru/ITelemetryPolicy.cs @@ -6,12 +6,16 @@ namespace BitFaster.Caching.Lru { - public interface IHitCounter + public interface ITelemetryPolicy { void IncrementMiss(); void IncrementHit(); + void OnItemRemoved(K key, V value, ItemRemovedReason reason); + double HitRatio { get; } + + void SetEventSource(object source); } } diff --git a/BitFaster.Caching/Lru/ItemRemovedEventArgs.cs b/BitFaster.Caching/Lru/ItemRemovedEventArgs.cs new file mode 100644 index 00000000..b1103ec1 --- /dev/null +++ b/BitFaster.Caching/Lru/ItemRemovedEventArgs.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru +{ + /// + /// Provides data for the ItemRemoved event. + /// + /// The type of the removed item key. + /// The type of the removed item value. + public class ItemRemovedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the ItemRemovedEventArgs class using the specified key, value and reason. + /// + /// The key of the item that was removed from the cache. + /// The value of the item that was removed from the cache. + /// The reason the item was removed from the cache. + public ItemRemovedEventArgs(K key, V value, ItemRemovedReason reason) + { + this.Key = key; + this.Value = value; + this.Reason = reason; + } + + /// + /// Gets the key of the item that was removed from the cache. + /// + public K Key { get; } + + /// + /// Gets the value of the item that was removed from the cache. + /// + public V Value { get; } + + /// + /// Gets the reason the item was removed from the cache. + /// + public ItemRemovedReason Reason { get; } + } +} diff --git a/BitFaster.Caching/Lru/ItemRemovedReason.cs b/BitFaster.Caching/Lru/ItemRemovedReason.cs new file mode 100644 index 00000000..3fe26b25 --- /dev/null +++ b/BitFaster.Caching/Lru/ItemRemovedReason.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru +{ + /// + /// Specifies the reason an item was removed from the Cache. + /// + public enum ItemRemovedReason + { + /// + /// The item is removed from the cache by a remove method call. + /// + Removed, + + /// + /// The item is removed from the cache by the cache eviction policy. + /// + Evicted, + } +} diff --git a/BitFaster.Caching/Lru/LruPolicy.cs b/BitFaster.Caching/Lru/LruPolicy.cs index 3e68020e..1fde7381 100644 --- a/BitFaster.Caching/Lru/LruPolicy.cs +++ b/BitFaster.Caching/Lru/LruPolicy.cs @@ -10,7 +10,7 @@ namespace BitFaster.Caching.Lru /// /// Discards the least recently used items first. /// - public readonly struct LruPolicy : IPolicy> + public readonly struct LruPolicy : IItemPolicy> { [MethodImpl(MethodImplOptions.AggressiveInlining)] public LruItem CreateItem(K key, V value) diff --git a/BitFaster.Caching/Lru/NullHitCounter.cs b/BitFaster.Caching/Lru/NoTelemetryPolicy.cs similarity index 57% rename from BitFaster.Caching/Lru/NullHitCounter.cs rename to BitFaster.Caching/Lru/NoTelemetryPolicy.cs index 431729e0..2f6b2825 100644 --- a/BitFaster.Caching/Lru/NullHitCounter.cs +++ b/BitFaster.Caching/Lru/NoTelemetryPolicy.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Lru { - public struct NullHitCounter : IHitCounter + public struct NoTelemetryPolicy : ITelemetryPolicy { public double HitRatio => 0.0; @@ -20,5 +20,15 @@ public void IncrementMiss() public void IncrementHit() { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnItemRemoved(K key, V value, ItemRemovedReason reason) + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetEventSource(object source) + { + } } } diff --git a/BitFaster.Caching/Lru/HitCounter.cs b/BitFaster.Caching/Lru/TelemetryPolicy.cs similarity index 53% rename from BitFaster.Caching/Lru/HitCounter.cs rename to BitFaster.Caching/Lru/TelemetryPolicy.cs index 96553fdb..17c3c06d 100644 --- a/BitFaster.Caching/Lru/HitCounter.cs +++ b/BitFaster.Caching/Lru/TelemetryPolicy.cs @@ -8,15 +8,18 @@ namespace BitFaster.Caching.Lru { - public struct HitCounter : IHitCounter + public struct TelemetryPolicy : ITelemetryPolicy { private long hitCount; private long missCount; + private object eventSource; public double HitRatio => Total == 0 ? 0 : (double)hitCount / (double)Total; public long Total => this.hitCount + this.missCount; + public EventHandler> ItemRemoved; + public void IncrementMiss() { Interlocked.Increment(ref this.missCount); @@ -26,5 +29,16 @@ public void IncrementHit() { Interlocked.Increment(ref this.hitCount); } + + public void OnItemRemoved(K key, V value, ItemRemovedReason reason) + { + // passing 'this' as source boxes the struct, and is anyway the wrong object + this.ItemRemoved?.Invoke(this.eventSource, new ItemRemovedEventArgs(key, value, reason)); + } + + public void SetEventSource(object source) + { + this.eventSource = source; + } } } diff --git a/BitFaster.Caching/Lru/TemplateConcurrentLru.cs b/BitFaster.Caching/Lru/TemplateConcurrentLru.cs index eb4a7861..e5043d4b 100644 --- a/BitFaster.Caching/Lru/TemplateConcurrentLru.cs +++ b/BitFaster.Caching/Lru/TemplateConcurrentLru.cs @@ -27,10 +27,10 @@ namespace BitFaster.Caching.Lru /// 5. When warm is full, warm tail is moved to warm head or cold depending on WasAccessed. /// 6. When cold is full, cold tail is moved to warm head or removed from dictionary on depending on WasAccessed. /// - public class TemplateConcurrentLru : ICache + public class TemplateConcurrentLru : ICache where I : LruItem - where P : struct, IPolicy - where H : struct, IHitCounter + where P : struct, IItemPolicy + where T : struct, ITelemetryPolicy { private readonly ConcurrentDictionary dictionary; @@ -47,18 +47,18 @@ public class TemplateConcurrentLru : ICache private readonly int warmCapacity; private readonly int coldCapacity; - private readonly P policy; + private readonly P itemPolicy; // Since H 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. - protected H hitCounter; + protected T telemetryPolicy; public TemplateConcurrentLru( int concurrencyLevel, int capacity, IEqualityComparer comparer, P itemPolicy, - H hitCounter) + T telemetryPolicy) { if (capacity < 3) { @@ -82,8 +82,9 @@ public TemplateConcurrentLru( int dictionaryCapacity = this.hotCapacity + this.warmCapacity + this.coldCapacity + 1; this.dictionary = new ConcurrentDictionary(concurrencyLevel, dictionaryCapacity, comparer); - this.policy = itemPolicy; - this.hitCounter = hitCounter; + this.itemPolicy = itemPolicy; + this.telemetryPolicy = telemetryPolicy; + this.telemetryPolicy.SetEventSource(this); } // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ @@ -104,7 +105,7 @@ public bool TryGet(K key, out V value) } value = default; - this.hitCounter.IncrementMiss(); + this.telemetryPolicy.IncrementMiss(); return false; } @@ -113,17 +114,17 @@ public bool TryGet(K key, out V value) [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool GetOrDiscard(I item, out V value) { - if (this.policy.ShouldDiscard(item)) + if (this.itemPolicy.ShouldDiscard(item)) { this.Move(item, ItemDestination.Remove); - this.hitCounter.IncrementMiss(); + this.telemetryPolicy.IncrementMiss(); value = default; return false; } value = item.Value; - this.policy.Touch(item); - this.hitCounter.IncrementHit(); + this.itemPolicy.Touch(item); + this.telemetryPolicy.IncrementHit(); return true; } @@ -139,7 +140,7 @@ public V GetOrAdd(K key, Func valueFactory) // The value factory may be called concurrently for the same key, but the first write to the dictionary wins. // This is identical logic in ConcurrentDictionary.GetOrAdd method. - var newItem = this.policy.CreateItem(key, valueFactory(key)); + var newItem = this.itemPolicy.CreateItem(key, valueFactory(key)); if (this.dictionary.TryAdd(key, newItem)) { @@ -165,7 +166,7 @@ public async Task GetOrAddAsync(K key, Func> valueFactory) // The value factory may be called concurrently for the same key, but the first write to the dictionary wins. // This is identical logic in ConcurrentDictionary.GetOrAdd method. - var newItem = this.policy.CreateItem(key, await valueFactory(key).ConfigureAwait(false)); + var newItem = this.itemPolicy.CreateItem(key, await valueFactory(key).ConfigureAwait(false)); if (this.dictionary.TryAdd(key, newItem)) { @@ -198,6 +199,8 @@ public bool TryRemove(K key) existing.WasAccessed = false; existing.WasRemoved = true; + this.telemetryPolicy.OnItemRemoved(existing.Key, existing.Value, ItemRemovedReason.Removed); + // serialize dispose (common case dispose not thread safe) lock (existing) { @@ -252,7 +255,7 @@ public void AddOrUpdate(K key, V value) } // then try add - var newItem = this.policy.CreateItem(key, value); + var newItem = this.itemPolicy.CreateItem(key, value); if (this.dictionary.TryAdd(key, newItem)) { @@ -322,7 +325,7 @@ private void CycleHotUnchecked() if (this.hotQueue.TryDequeue(out var item)) { - var where = this.policy.RouteHot(item); + var where = this.itemPolicy.RouteHot(item); this.Move(item, where); } else @@ -346,7 +349,7 @@ private void CycleWarmUnchecked() if (this.warmQueue.TryDequeue(out var item)) { - var where = this.policy.RouteWarm(item); + var where = this.itemPolicy.RouteWarm(item); // When the warm queue is full, we allow an overflow of 1 item before redirecting warm items to cold. // This only happens when hit rate is high, in which case we can consider all items relatively equal in @@ -381,7 +384,7 @@ private void CycleColdUnchecked() if (this.coldQueue.TryDequeue(out var item)) { - var where = this.policy.RouteCold(item); + var where = this.itemPolicy.RouteCold(item); if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity) { @@ -423,6 +426,8 @@ private void Move(I item, ItemDestination where) { item.WasRemoved = true; + this.telemetryPolicy.OnItemRemoved(item.Key, item.Value, ItemRemovedReason.Evicted); + lock (item) { Disposer.Dispose(item.Value); diff --git a/BitFaster.Caching/Lru/TlruDateTimePolicy.cs b/BitFaster.Caching/Lru/TlruDateTimePolicy.cs index ecd7bd9c..88578818 100644 --- a/BitFaster.Caching/Lru/TlruDateTimePolicy.cs +++ b/BitFaster.Caching/Lru/TlruDateTimePolicy.cs @@ -11,7 +11,7 @@ namespace BitFaster.Caching.Lru /// Time aware Least Recently Used (TLRU) is a variant of LRU which discards the least /// recently used items first, and any item that has expired. /// - public readonly struct TLruDateTimePolicy : IPolicy> + public readonly struct TLruDateTimePolicy : IItemPolicy> { private readonly TimeSpan timeToLive; diff --git a/BitFaster.Caching/Lru/TlruLongTicksPolicy.cs b/BitFaster.Caching/Lru/TlruLongTicksPolicy.cs index 0cdbb34d..38620529 100644 --- a/BitFaster.Caching/Lru/TlruLongTicksPolicy.cs +++ b/BitFaster.Caching/Lru/TlruLongTicksPolicy.cs @@ -15,7 +15,7 @@ namespace BitFaster.Caching.Lru /// /// This class measures time using stopwatch. /// - public readonly struct TLruLongTicksPolicy : IPolicy> + public readonly struct TLruLongTicksPolicy : IItemPolicy> { private readonly long timeToLive; diff --git a/BitFaster.Caching/Lru/TlruTicksPolicy.cs b/BitFaster.Caching/Lru/TlruTicksPolicy.cs index c308d91d..f8805523 100644 --- a/BitFaster.Caching/Lru/TlruTicksPolicy.cs +++ b/BitFaster.Caching/Lru/TlruTicksPolicy.cs @@ -16,7 +16,7 @@ namespace BitFaster.Caching.Lru /// than DateTime.Now. However, if the process runs for longer than 24.8 days, the integer /// value will wrap and time measurement will become invalid. /// - public readonly struct TLruTicksPolicy : IPolicy> + public readonly struct TLruTicksPolicy : IItemPolicy> { private readonly int timeToLive; diff --git a/BitFaster.Caching/Scoped.cs b/BitFaster.Caching/Scoped.cs index 6379e442..dad6ee46 100644 --- a/BitFaster.Caching/Scoped.cs +++ b/BitFaster.Caching/Scoped.cs @@ -11,7 +11,7 @@ namespace BitFaster.Caching /// the wrapped object from being diposed until the calling code completes. /// /// The type of scoped value. - public class Scoped : IDisposable where T : IDisposable + public sealed class Scoped : IDisposable where T : IDisposable { private ReferenceCount refCount; private bool isDisposed;