diff --git a/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs index 2c1036c6..5fc74f18 100644 --- a/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs @@ -261,6 +261,23 @@ public void WhenMoreKeysRequestedThanCapacityOldestItemIsEvicted() valueFactory.timesCalled.Should().Be(capacity + 2); } + [Fact] + public void WhenMoreKeysRequestedThanCapacityEvictedMetricRecordsNumberEvicted() + { + // request 3 items, LRU is now full + for (int i = 0; i < capacity; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + } + + lru.Metrics.Evicted.Should().Be(0); + + // request 0, now item 1 is to be evicted + lru.GetOrAdd(4, valueFactory.Create); + + lru.Metrics.Evicted.Should().Be(1); + } + [Fact] public void WhenValueExpiresItIsDisposed() { diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs index c4218365..054e197c 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs @@ -494,6 +494,16 @@ public void WhenValueEvictedItemRemovedEventIsFired() removedItems[1].Reason.Should().Be(ItemRemovedReason.Evicted); } + [Fact] + public void WhenValuesAreEvictedEvictionMetricCountsEvicted() + { + this.Warmup(); + + this.lru.GetOrAdd(1, valueFactory.Create); + + this.lru.Metrics.Evicted.Should().Be(1); + } + [Fact] public void WhenItemRemovedEventIsUnregisteredEventIsNotFired() { diff --git a/BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs index f937926e..0a77c4af 100644 --- a/BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs @@ -35,6 +35,12 @@ public void MissesIsZero() counter.Misses.Should().Be(0); } + [Fact] + public void EvictedIsZero() + { + counter.Evicted.Should().Be(0); + } + [Fact] public void IsEnabledIsFalse() { diff --git a/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs index dbdeb7c7..5bbb6c86 100644 --- a/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/TelemetryPolicyTests.cs @@ -64,6 +64,22 @@ public void WhenTotalCountIsZeroRatioReturnsZero() telemetryPolicy.HitRatio.Should().Be(0.0); } + [Fact] + public void WhenItemRemovedIsEvictedIncrementEvictedCount() + { + telemetryPolicy.OnItemRemoved(1, 2, ItemRemovedReason.Evicted); + + telemetryPolicy.Evicted.Should().Be(1); + } + + [Fact] + public void WhenItemRemovedIsRemovedDontIncrementEvictedCount() + { + telemetryPolicy.OnItemRemoved(1, 2, ItemRemovedReason.Removed); + + telemetryPolicy.Evicted.Should().Be(0); + } + [Fact] public void WhenOnItemRemovedInvokedEventIsFired() { diff --git a/BitFaster.Caching/ICacheMetrics.cs b/BitFaster.Caching/ICacheMetrics.cs index e5539a06..fab33d76 100644 --- a/BitFaster.Caching/ICacheMetrics.cs +++ b/BitFaster.Caching/ICacheMetrics.cs @@ -33,6 +33,11 @@ public interface ICacheMetrics /// long Misses { get; } + /// + /// Gets the total number of evicted items. + /// + long Evicted { get; } + /// /// Gets a value indicating whether metrics are enabled. /// diff --git a/BitFaster.Caching/Lru/ClassicLru.cs b/BitFaster.Caching/Lru/ClassicLru.cs index 199b3cf3..200ece03 100644 --- a/BitFaster.Caching/Lru/ClassicLru.cs +++ b/BitFaster.Caching/Lru/ClassicLru.cs @@ -135,6 +135,7 @@ public V GetOrAdd(K key, Func valueFactory) { dictionary.TryRemove(first.Value.Key, out var removed); + Interlocked.Increment(ref this.metrics.evictedCount); Disposer.Dispose(removed.Value.Value); } @@ -179,6 +180,7 @@ public async Task GetOrAddAsync(K key, Func> valueFactory) { dictionary.TryRemove(first.Value.Key, out var removed); + Interlocked.Increment(ref this.metrics.evictedCount); Disposer.Dispose(removed.Value.Value); } @@ -267,6 +269,7 @@ public void AddOrUpdate(K key, V value) { dictionary.TryRemove(first.Value.Key, out var removed); + Interlocked.Increment(ref this.metrics.evictedCount); Disposer.Dispose(removed.Value.Value); } @@ -374,6 +377,7 @@ private class CacheMetrics : ICacheMetrics { public long requestHitCount; public long requestTotalCount; + public long evictedCount; public double HitRatio => (double)requestHitCount / (double)requestTotalCount; @@ -383,6 +387,8 @@ private class CacheMetrics : ICacheMetrics public long Misses => requestTotalCount - requestHitCount; + public long Evicted => evictedCount; + public bool IsEnabled => true; } } diff --git a/BitFaster.Caching/Lru/NoTelemetryPolicy.cs b/BitFaster.Caching/Lru/NoTelemetryPolicy.cs index e78910d8..0ab35749 100644 --- a/BitFaster.Caching/Lru/NoTelemetryPolicy.cs +++ b/BitFaster.Caching/Lru/NoTelemetryPolicy.cs @@ -17,6 +17,8 @@ public struct NoTelemetryPolicy : ITelemetryPolicy public long Misses => 0; + public long Evicted => 0; + public bool IsEnabled => false; [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/BitFaster.Caching/Lru/TelemetryPolicy.cs b/BitFaster.Caching/Lru/TelemetryPolicy.cs index 1ef28e29..55c74b19 100644 --- a/BitFaster.Caching/Lru/TelemetryPolicy.cs +++ b/BitFaster.Caching/Lru/TelemetryPolicy.cs @@ -12,6 +12,7 @@ public struct TelemetryPolicy : ITelemetryPolicy { private long hitCount; private long missCount; + private long evictedCount; private object eventSource; public double HitRatio => Total == 0 ? 0 : (double)hitCount / (double)Total; @@ -22,6 +23,8 @@ public struct TelemetryPolicy : ITelemetryPolicy public long Misses => this.missCount; + public long Evicted => this.evictedCount; + public bool IsEnabled => true; public EventHandler> ItemRemoved; @@ -38,6 +41,11 @@ public void IncrementHit() public void OnItemRemoved(K key, V value, ItemRemovedReason reason) { + if (reason == ItemRemovedReason.Evicted) + { + Interlocked.Increment(ref this.evictedCount); + } + // passing 'this' as source boxes the struct, and is anyway the wrong object this.ItemRemoved?.Invoke(this.eventSource, new ItemRemovedEventArgs(key, value, reason)); }