Skip to content

Commit bb4ff3c

Browse files
authored
Add event when item is evicted (#90)
* outline * cycle bench * docs etc * results * avoid boxing struct * seal * fix merge * rename hitcounter * default * item policy * docs * tel pol tests * docs
1 parent df5d64d commit bb4ff3c

26 files changed

+381
-105
lines changed

BitFaster.Caching.Benchmarks/DisposerBench.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace BitFaster.Caching.Benchmarks
1313
// https://github.com/dotnet/runtime/issues/4920
1414
[SimpleJob(RuntimeMoniker.Net48)]
1515
[SimpleJob(RuntimeMoniker.Net60)]
16-
[DisassemblyDiagnoser(printSource: true, maxDepth:3)]
16+
[DisassemblyDiagnoser(printSource: true, maxDepth: 3)]
1717
[MemoryDiagnoser]
1818
public class DisposerBench
1919
{

BitFaster.Caching.Benchmarks/Lru/LruCycleBench.cs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Runtime.CompilerServices;
45
using System.Text;
56
using System.Threading.Tasks;
67
using BenchmarkDotNet.Attributes;
@@ -16,13 +17,14 @@ namespace BitFaster.Caching.Benchmarks.Lru
1617
// DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
1718

1819

19-
//| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Code Size | Allocated |
20-
//|------------------- |---------:|---------:|---------:|------:|--------:|-------:|----------:|----------:|
21-
//| FastConcurrentLru | 23.25 us | 0.128 us | 0.114 us | 1.00 | 0.00 | 2.1362 | 5 KB | 9 KB |
22-
//| ConcurrentLru | 23.78 us | 0.116 us | 0.097 us | 1.02 | 0.01 | 2.1362 | 5 KB | 9 KB |
23-
//| FastConcurrentTLru | 32.17 us | 0.463 us | 0.433 us | 1.38 | 0.02 | 2.3193 | 6 KB | 10 KB |
24-
//| ConcurrentTLru | 32.52 us | 0.386 us | 0.361 us | 1.40 | 0.02 | 2.3193 | 6 KB | 10 KB |
25-
//| ClassicLru | 16.29 us | 0.195 us | 0.163 us | 0.70 | 0.01 | 3.2959 | 5 KB | 14 KB |
20+
//| Method | Mean | Error | StdDev | Ratio | Code Size | Gen 0 | Allocated |
21+
//|------------------- |---------:|---------:|---------:|------:|----------:|-------:|----------:|
22+
//| FastConcurrentLru | 22.86 us | 0.183 us | 0.162 us | 1.00 | 5 KB | 2.1362 | 9 KB |
23+
//| ConcurrentLru | 23.40 us | 0.092 us | 0.077 us | 1.02 | 5 KB | 2.1362 | 9 KB |
24+
//| ConcurrentLruEvent | 24.23 us | 0.097 us | 0.086 us | 1.06 | 5 KB | 3.0823 | 13 KB |
25+
//| FastConcurrentTLru | 31.70 us | 0.087 us | 0.077 us | 1.39 | 6 KB | 2.3193 | 10 KB |
26+
//| ConcurrentTLru | 31.85 us | 0.080 us | 0.071 us | 1.39 | 6 KB | 2.3193 | 10 KB |
27+
//| ClassicLru | 16.35 us | 0.091 us | 0.076 us | 0.72 | 4 KB | 3.2959 | 14 KB |
2628
[SimpleJob(RuntimeMoniker.Net48)]
2729
[SimpleJob(RuntimeMoniker.Net60)]
2830
[DisassemblyDiagnoser(printSource: true, maxDepth: 5)]
@@ -31,11 +33,26 @@ public class LruCycleBench
3133
{
3234
private static readonly ClassicLru<int, int> classicLru = new ClassicLru<int, int>(8, 9, EqualityComparer<int>.Default);
3335
private static readonly ConcurrentLru<int, int> concurrentLru = new ConcurrentLru<int, int>(8, 9, EqualityComparer<int>.Default);
36+
private static readonly ConcurrentLru<int, int> concurrentLruEvent = new ConcurrentLru<int, int>(8, 9, EqualityComparer<int>.Default);
3437
private static readonly ConcurrentTLru<int, int> concurrentTlru = new ConcurrentTLru<int, int>(8, 9, EqualityComparer<int>.Default, TimeSpan.FromMinutes(10));
3538
private static readonly FastConcurrentLru<int, int> fastConcurrentLru = new FastConcurrentLru<int, int>(8, 9, EqualityComparer<int>.Default);
3639
private static readonly FastConcurrentTLru<int, int> fastConcurrentTLru = new FastConcurrentTLru<int, int>(8, 9, EqualityComparer<int>.Default, TimeSpan.FromMinutes(1));
3740

38-
[Benchmark(Baseline = true)]
41+
[GlobalSetup]
42+
public void GlobalSetup()
43+
{
44+
concurrentLruEvent.ItemRemoved += OnItemRemoved;
45+
}
46+
47+
public static int field;
48+
49+
[MethodImpl(MethodImplOptions.NoOptimization)]
50+
private void OnItemRemoved(object sender, ItemRemovedEventArgs<int, int> e)
51+
{
52+
field = e.Key;
53+
}
54+
55+
[Benchmark(Baseline =true)]
3956
public void FastConcurrentLru()
4057
{
4158
Func<int, int> func = x => x;
@@ -53,6 +70,15 @@ public void ConcurrentLru()
5370
concurrentLru.GetOrAdd(i, func);
5471
}
5572

73+
[Benchmark()]
74+
public void ConcurrentLruEvent()
75+
{
76+
Func<int, int> func = x => x;
77+
78+
for (int i = 0; i < 128; i++)
79+
concurrentLruEvent.GetOrAdd(i, func);
80+
}
81+
5682
[Benchmark()]
5783
public void FastConcurrentTLru()
5884
{

BitFaster.Caching.Benchmarks/Lru/TLruTimeBenchmark.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ namespace BitFaster.Caching.Benchmarks.Lru
1414
[SimpleJob(RuntimeMoniker.Net60)]
1515
public class TLruTimeBenchmark
1616
{
17-
private static readonly TemplateConcurrentLru<int, int, TimeStampedLruItem<int, int>, TLruDateTimePolicy<int, int>, NullHitCounter> dateTimeTLru
18-
= new TemplateConcurrentLru<int, int, TimeStampedLruItem<int, int>, TLruDateTimePolicy<int, int>, NullHitCounter>
19-
(1, 3, EqualityComparer<int>.Default, new TLruDateTimePolicy<int, int>(TimeSpan.FromSeconds(1)), new NullHitCounter());
17+
private static readonly TemplateConcurrentLru<int, int, TimeStampedLruItem<int, int>, TLruDateTimePolicy<int, int>, NoTelemetryPolicy<int, int>> dateTimeTLru
18+
= new TemplateConcurrentLru<int, int, TimeStampedLruItem<int, int>, TLruDateTimePolicy<int, int>, NoTelemetryPolicy<int, int>>
19+
(1, 3, EqualityComparer<int>.Default, new TLruDateTimePolicy<int, int>(TimeSpan.FromSeconds(1)), default);
2020

21-
private static readonly TemplateConcurrentLru<int, int, TickCountLruItem<int, int>, TLruTicksPolicy<int, int>, NullHitCounter> tickCountTLru
22-
= new TemplateConcurrentLru<int, int, TickCountLruItem<int, int>, TLruTicksPolicy<int, int>, NullHitCounter>
23-
(1, 3, EqualityComparer<int>.Default, new TLruTicksPolicy<int, int>(TimeSpan.FromSeconds(1)), new NullHitCounter());
21+
private static readonly TemplateConcurrentLru<int, int, TickCountLruItem<int, int>, TLruTicksPolicy<int, int>, NoTelemetryPolicy<int, int>> tickCountTLru
22+
= new TemplateConcurrentLru<int, int, TickCountLruItem<int, int>, TLruTicksPolicy<int, int>, NoTelemetryPolicy<int, int>>
23+
(1, 3, EqualityComparer<int>.Default, new TLruTicksPolicy<int, int>(TimeSpan.FromSeconds(1)), default);
2424

25-
private static readonly TemplateConcurrentLru<int, int, LongTickCountLruItem<int, int>, TLruLongTicksPolicy<int, int>, NullHitCounter> stopwatchTLru
26-
= new TemplateConcurrentLru<int, int, LongTickCountLruItem<int, int>, TLruLongTicksPolicy<int, int>, NullHitCounter>
27-
(1, 3, EqualityComparer<int>.Default, new TLruLongTicksPolicy<int, int>(TimeSpan.FromSeconds(1)), new NullHitCounter());
25+
private static readonly TemplateConcurrentLru<int, int, LongTickCountLruItem<int, int>, TLruLongTicksPolicy<int, int>, NoTelemetryPolicy<int, int>> stopwatchTLru
26+
= new TemplateConcurrentLru<int, int, LongTickCountLruItem<int, int>, TLruLongTicksPolicy<int, int>, NoTelemetryPolicy<int, int>>
27+
(1, 3, EqualityComparer<int>.Default, new TLruLongTicksPolicy<int, int>(TimeSpan.FromSeconds(1)), default);
2828

2929
[Benchmark(Baseline = true)]
3030
public void DateTimeUtcNow()

BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ public class ConcurrentLruTests
2020
private ConcurrentLru<int, string> lru = new ConcurrentLru<int, string>(1, hotCap + warmCap + coldCap, EqualityComparer<int>.Default);
2121
private ValueFactory valueFactory = new ValueFactory();
2222

23+
private List<ItemRemovedEventArgs<int, int>> removedItems = new List<ItemRemovedEventArgs<int, int>>();
24+
25+
private void OnLruItemRemoved(object sender, ItemRemovedEventArgs<int, int> e)
26+
{
27+
removedItems.Add(e);
28+
}
29+
2330
public ConcurrentLruTests(ITestOutputHelper testOutputHelper)
2431
{
2532
this.testOutputHelper = testOutputHelper;
@@ -359,6 +366,44 @@ public void WhenValueExpiresItIsDisposed()
359366
disposableValueFactory.Items[1].IsDisposed.Should().BeFalse();
360367
}
361368

369+
[Fact]
370+
public void WhenValueEvictedItemRemovedEventIsFired()
371+
{
372+
var lruEvents = new ConcurrentLru<int, int>(1, 6, EqualityComparer<int>.Default);
373+
lruEvents.ItemRemoved += OnLruItemRemoved;
374+
375+
for (int i = 0; i < 6; i++)
376+
{
377+
lruEvents.GetOrAdd(i+1, i => i + 1);
378+
}
379+
380+
removedItems.Count.Should().Be(2);
381+
382+
removedItems[0].Key.Should().Be(1);
383+
removedItems[0].Value.Should().Be(2);
384+
removedItems[0].Reason.Should().Be(ItemRemovedReason.Evicted);
385+
386+
removedItems[1].Key.Should().Be(2);
387+
removedItems[1].Value.Should().Be(3);
388+
removedItems[1].Reason.Should().Be(ItemRemovedReason.Evicted);
389+
}
390+
391+
[Fact]
392+
public void WhenItemRemovedEventIsUnregisteredEventIsNotFired()
393+
{
394+
var lruEvents = new ConcurrentLru<int, int>(1, 6, EqualityComparer<int>.Default);
395+
396+
lruEvents.ItemRemoved += OnLruItemRemoved;
397+
lruEvents.ItemRemoved -= OnLruItemRemoved;
398+
399+
for (int i = 0; i < 6; i++)
400+
{
401+
lruEvents.GetOrAdd(i + 1, i => i + 1);
402+
}
403+
404+
removedItems.Count.Should().Be(0);
405+
}
406+
362407
[Fact]
363408
public void WhenKeyExistsTryRemoveRemovesItemAndReturnsTrue()
364409
{
@@ -380,6 +425,22 @@ public void WhenItemIsRemovedItIsDisposed()
380425
disposableValueFactory.Items[1].IsDisposed.Should().BeTrue();
381426
}
382427

428+
[Fact]
429+
public void WhenItemIsRemovedRemovedEventIsFired()
430+
{
431+
var lruEvents = new ConcurrentLru<int, int>(1, 6, EqualityComparer<int>.Default);
432+
lruEvents.ItemRemoved += OnLruItemRemoved;
433+
434+
lruEvents.GetOrAdd(1, i => i+2);
435+
436+
lruEvents.TryRemove(1).Should().BeTrue();
437+
438+
removedItems.Count().Should().Be(1);
439+
removedItems[0].Key.Should().Be(1);
440+
removedItems[0].Value.Should().Be(3);
441+
removedItems[0].Reason.Should().Be(ItemRemovedReason.Removed);
442+
}
443+
383444
[Fact]
384445
public void WhenKeyDoesNotExistTryRemoveReturnsFalse()
385446
{

BitFaster.Caching.UnitTests/Lru/ConcurrentTLruTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ public class ConcurrentTLruTests
1616

1717
private ValueFactory valueFactory = new ValueFactory();
1818

19+
private List<ItemRemovedEventArgs<int, int>> removedItems = new List<ItemRemovedEventArgs<int, int>>();
20+
21+
private void OnLruItemRemoved(object sender, ItemRemovedEventArgs<int, int> e)
22+
{
23+
removedItems.Add(e);
24+
}
25+
1926
public ConcurrentTLruTests()
2027
{
2128
lru = new ConcurrentTLru<int, string>(1, capacity, EqualityComparer<int>.Default, timeToLive);
@@ -47,6 +54,44 @@ public async Task WhenItemIsExpiredItIsRemoved()
4754
lru.TryGet(1, out var value).Should().BeFalse();
4855
}
4956

57+
[Fact]
58+
public void WhenValueEvictedItemRemovedEventIsFired()
59+
{
60+
var lruEvents = new ConcurrentTLru<int, int>(1, 6, EqualityComparer<int>.Default, timeToLive);
61+
lruEvents.ItemRemoved += OnLruItemRemoved;
62+
63+
for (int i = 0; i < 6; i++)
64+
{
65+
lruEvents.GetOrAdd(i + 1, i => i + 1);
66+
}
67+
68+
removedItems.Count.Should().Be(2);
69+
70+
removedItems[0].Key.Should().Be(1);
71+
removedItems[0].Value.Should().Be(2);
72+
removedItems[0].Reason.Should().Be(ItemRemovedReason.Evicted);
73+
74+
removedItems[1].Key.Should().Be(2);
75+
removedItems[1].Value.Should().Be(3);
76+
removedItems[1].Reason.Should().Be(ItemRemovedReason.Evicted);
77+
}
78+
79+
[Fact]
80+
public void WhenItemRemovedEventIsUnregisteredEventIsNotFired()
81+
{
82+
var lruEvents = new ConcurrentTLru<int, int>(1, 6, EqualityComparer<int>.Default, timeToLive);
83+
84+
lruEvents.ItemRemoved += OnLruItemRemoved;
85+
lruEvents.ItemRemoved -= OnLruItemRemoved;
86+
87+
for (int i = 0; i < 6; i++)
88+
{
89+
lruEvents.GetOrAdd(i + 1, i => i + 1);
90+
}
91+
92+
removedItems.Count.Should().Be(0);
93+
}
94+
5095
[Fact]
5196
public void WhenItemIsAddedThenRetrievedHitRatioIsHalf()
5297
{

BitFaster.Caching.UnitTests/Lru/HitCounterTests.cs

Lines changed: 0 additions & 41 deletions
This file was deleted.

BitFaster.Caching.UnitTests/Lru/NullHitCounterTests.cs renamed to BitFaster.Caching.UnitTests/Lru/NoTelemetryPolicyTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
namespace BitFaster.Caching.UnitTests.Lru
99
{
10-
public class NullHitCounterTests
10+
public class NoTelemetryPolicyTests
1111
{
12-
private NullHitCounter counter = new NullHitCounter();
12+
private NoTelemetryPolicy<int, int> counter = new NoTelemetryPolicy<int, int>();
1313

1414
[Fact]
1515
public void HitRatioIsZero()
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using FluentAssertions;
2+
using BitFaster.Caching.Lru;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Text;
6+
using Xunit;
7+
8+
namespace BitFaster.Caching.UnitTests.Lru
9+
{
10+
public class TelemetryPolicyTests
11+
{
12+
private TelemetryPolicy<int, int> telemetryPolicy = default;
13+
14+
[Fact]
15+
public void WhenHitCountAndTotalCountAreEqualRatioIs1()
16+
{
17+
telemetryPolicy.IncrementHit();
18+
19+
telemetryPolicy.HitRatio.Should().Be(1.0);
20+
}
21+
22+
[Fact]
23+
public void WhenHitCountIsEqualToMissCountRatioIsHalf()
24+
{
25+
telemetryPolicy.IncrementMiss();
26+
telemetryPolicy.IncrementHit();
27+
28+
telemetryPolicy.HitRatio.Should().Be(0.5);
29+
}
30+
31+
[Fact]
32+
public void WhenTotalCountIsZeroRatioReturnsZero()
33+
{
34+
telemetryPolicy.HitRatio.Should().Be(0.0);
35+
}
36+
37+
[Fact]
38+
public void WhenOnItemRemovedInvokedEventIsFired()
39+
{
40+
List<ItemRemovedEventArgs<int, int>> eventList = new();
41+
42+
telemetryPolicy.ItemRemoved += (source, args) => eventList.Add(args);
43+
44+
telemetryPolicy.OnItemRemoved(1, 2, ItemRemovedReason.Evicted);
45+
46+
eventList.Should().HaveCount(1);
47+
eventList[0].Key.Should().Be(1);
48+
eventList[0].Value.Should().Be(2);
49+
eventList[0].Reason.Should().Be(ItemRemovedReason.Evicted);
50+
}
51+
52+
[Fact]
53+
public void WhenEventSourceIsSetItemRemovedEventUsesSource()
54+
{
55+
List<object> eventSourceList = new();
56+
57+
telemetryPolicy.ItemRemoved += (source, args) => eventSourceList.Add(source);
58+
59+
telemetryPolicy.SetEventSource(this);
60+
telemetryPolicy.OnItemRemoved(1, 2, ItemRemovedReason.Evicted);
61+
62+
eventSourceList.Should().HaveCount(1);
63+
eventSourceList[0].Should().Be(this);
64+
}
65+
}
66+
}

BitFaster.Caching/Disposer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
namespace BitFaster.Caching
99
{
1010
/// <summary>
11-
/// A generic wrapper for object disposal. Enables JIT to inline/remove object disposal if statement reducing code size.
11+
/// A generic wrapper for object disposal.
1212
/// </summary>
1313
/// <typeparam name="T">The type of object to dispose</typeparam>
1414
public static class Disposer<T>

BitFaster.Caching/Lifetime.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace BitFaster.Caching
99
/// lifetime is disposed.
1010
/// </summary>
1111
/// <typeparam name="T">The type of value</typeparam>
12-
public class Lifetime<T> : IDisposable
12+
public sealed class Lifetime<T> : IDisposable
1313
{
1414
private readonly Action onDisposeAction;
1515
private readonly ReferenceCount<T> refCount;

0 commit comments

Comments
 (0)