diff --git a/BitFaster.Caching.Benchmarks/BitFaster.Caching.Benchmarks.csproj b/BitFaster.Caching.Benchmarks/BitFaster.Caching.Benchmarks.csproj
index 6a31dca0..ade8391f 100644
--- a/BitFaster.Caching.Benchmarks/BitFaster.Caching.Benchmarks.csproj
+++ b/BitFaster.Caching.Benchmarks/BitFaster.Caching.Benchmarks.csproj
@@ -9,6 +9,10 @@
1701;1702;CS8002
+
+ 1701;1702,CS8002
+
+
diff --git a/BitFaster.Caching.Benchmarks/Program.cs b/BitFaster.Caching.Benchmarks/Program.cs
index 317eb34e..e2676d8d 100644
--- a/BitFaster.Caching.Benchmarks/Program.cs
+++ b/BitFaster.Caching.Benchmarks/Program.cs
@@ -15,7 +15,7 @@ class Program
static void Main(string[] args)
{
var summary = BenchmarkRunner
- .Run(ManualConfig.Create(DefaultConfig.Instance)
+ .Run(ManualConfig.Create(DefaultConfig.Instance)
.AddJob(Job.RyuJitX64));
}
}
diff --git a/BitFaster.Caching.HitRateAnalysis/BitFaster.Caching.HitRateAnalysis.csproj b/BitFaster.Caching.HitRateAnalysis/BitFaster.Caching.HitRateAnalysis.csproj
index 8ad95104..b615e745 100644
--- a/BitFaster.Caching.HitRateAnalysis/BitFaster.Caching.HitRateAnalysis.csproj
+++ b/BitFaster.Caching.HitRateAnalysis/BitFaster.Caching.HitRateAnalysis.csproj
@@ -9,6 +9,10 @@
1701;1702;CS8002
+
+ 1701;1702,CS8002
+
+
diff --git a/BitFaster.Caching.UnitTests/Lru/TlruPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/TlruDateTimePolicyTests.cs
similarity index 95%
rename from BitFaster.Caching.UnitTests/Lru/TlruPolicyTests.cs
rename to BitFaster.Caching.UnitTests/Lru/TlruDateTimePolicyTests.cs
index 4a121cf9..90ca95d7 100644
--- a/BitFaster.Caching.UnitTests/Lru/TlruPolicyTests.cs
+++ b/BitFaster.Caching.UnitTests/Lru/TlruDateTimePolicyTests.cs
@@ -10,9 +10,9 @@
namespace BitFaster.Caching.UnitTests.Lru
{
- public class TLruPolicyTests
+ public class TLruDateTimePolicyTests
{
- private readonly TLruPolicy policy = new TLruPolicy(TimeSpan.FromSeconds(10));
+ private readonly TLruDateTimePolicy policy = new TLruDateTimePolicy(TimeSpan.FromSeconds(10));
[Fact]
public void CreateItemInitializesKeyAndValue()
diff --git a/BitFaster.Caching.UnitTests/Lru/TlruTicksPolicyTests.cs b/BitFaster.Caching.UnitTests/Lru/TlruTicksPolicyTests.cs
new file mode 100644
index 00000000..73fab8d7
--- /dev/null
+++ b/BitFaster.Caching.UnitTests/Lru/TlruTicksPolicyTests.cs
@@ -0,0 +1,121 @@
+using FluentAssertions;
+using FluentAssertions.Extensions;
+using BitFaster.Caching.Lru;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace BitFaster.Caching.UnitTests.Lru
+{
+ public class TLruTicksPolicyTests
+ {
+ private readonly TLruTicksPolicy policy = new TLruTicksPolicy(TimeSpan.FromSeconds(10));
+
+ [Fact]
+ public void CreateItemInitializesKeyAndValue()
+ {
+ var item = this.policy.CreateItem(1, 2);
+
+ item.Key.Should().Be(1);
+ item.Value.Should().Be(2);
+ }
+
+ [Fact]
+ public void CreateItemInitializesTimestampToNow()
+ {
+ var item = this.policy.CreateItem(1, 2);
+
+ item.TickCount.Should().BeCloseTo(Environment.TickCount, 20);
+ }
+
+ [Fact]
+ public void TouchUpdatesItemWasAccessed()
+ {
+ var item = this.policy.CreateItem(1, 2);
+ item.WasAccessed = false;
+
+ this.policy.Touch(item);
+
+ item.WasAccessed.Should().BeTrue();
+ }
+
+ [Fact]
+ public void WhenItemIsExpiredShouldDiscardIsTrue()
+ {
+ var item = this.policy.CreateItem(1, 2);
+ item.TickCount = Environment.TickCount - (int)TimeSpan.FromSeconds(11).ToEnvTicks();
+
+ this.policy.ShouldDiscard(item).Should().BeTrue();
+ }
+
+ [Fact]
+ public void WhenItemIsNotExpiredShouldDiscardIsFalse()
+ {
+ var item = this.policy.CreateItem(1, 2);
+ item.TickCount = Environment.TickCount - (int)TimeSpan.FromSeconds(9).ToEnvTicks();
+
+ this.policy.ShouldDiscard(item).Should().BeFalse();
+ }
+
+ [Theory]
+ [InlineData(false, true, ItemDestination.Remove)]
+ [InlineData(true, true, ItemDestination.Remove)]
+ [InlineData(true, false, ItemDestination.Warm)]
+ [InlineData(false, false, ItemDestination.Cold)]
+ public void RouteHot(bool wasAccessed, bool isExpired, ItemDestination expectedDestination)
+ {
+ var item = CreateItem(wasAccessed, isExpired);
+
+ this.policy.RouteHot(item).Should().Be(expectedDestination);
+ }
+
+ [Theory]
+ [InlineData(false, true, ItemDestination.Remove)]
+ [InlineData(true, true, ItemDestination.Remove)]
+ [InlineData(true, false, ItemDestination.Warm)]
+ [InlineData(false, false, ItemDestination.Cold)]
+ public void RouteWarm(bool wasAccessed, bool isExpired, ItemDestination expectedDestination)
+ {
+ var item = CreateItem(wasAccessed, isExpired);
+
+ this.policy.RouteWarm(item).Should().Be(expectedDestination);
+ }
+
+ [Theory]
+ [InlineData(false, true, ItemDestination.Remove)]
+ [InlineData(true, true, ItemDestination.Remove)]
+ [InlineData(true, false, ItemDestination.Warm)]
+ [InlineData(false, false, ItemDestination.Remove)]
+ public void RouteCold(bool wasAccessed, bool isExpired, ItemDestination expectedDestination)
+ {
+ var item = CreateItem(wasAccessed, isExpired);
+
+ this.policy.RouteCold(item).Should().Be(expectedDestination);
+ }
+
+ private TickCountLruItem CreateItem(bool wasAccessed, bool isExpired)
+ {
+ var item = this.policy.CreateItem(1, 2);
+
+ item.WasAccessed = wasAccessed;
+
+ if (isExpired)
+ {
+ item.TickCount = Environment.TickCount - TimeSpan.FromSeconds(11).ToEnvTicks();
+ }
+
+ return item;
+ }
+ }
+
+ public static class TimeSpanExtensions
+ {
+ public static int ToEnvTicks(this TimeSpan ts)
+ {
+ return (int)ts.TotalMilliseconds;
+ }
+ }
+}
diff --git a/BitFaster.Caching/Lru/ConcurrentTLru.cs b/BitFaster.Caching/Lru/ConcurrentTLru.cs
index 48fbc400..2168450e 100644
--- a/BitFaster.Caching/Lru/ConcurrentTLru.cs
+++ b/BitFaster.Caching/Lru/ConcurrentTLru.cs
@@ -6,10 +6,10 @@
namespace BitFaster.Caching.Lru
{
- public sealed class ConcurrentTLru : TemplateConcurrentLru, TLruPolicy, HitCounter>
+ public sealed class ConcurrentTLru : TemplateConcurrentLru, TLruDateTimePolicy, HitCounter>
{
public ConcurrentTLru(int concurrencyLevel, int capacity, IEqualityComparer comparer, TimeSpan timeToLive)
- : base(concurrencyLevel, capacity, comparer, new TLruPolicy(timeToLive), new HitCounter())
+ : base(concurrencyLevel, capacity, comparer, new TLruDateTimePolicy(timeToLive), new HitCounter())
{
}
diff --git a/BitFaster.Caching/Lru/FastConcurrentTLru.cs b/BitFaster.Caching/Lru/FastConcurrentTLru.cs
index 93bba30a..49f365ce 100644
--- a/BitFaster.Caching/Lru/FastConcurrentTLru.cs
+++ b/BitFaster.Caching/Lru/FastConcurrentTLru.cs
@@ -4,10 +4,10 @@
namespace BitFaster.Caching.Lru
{
- public sealed class FastConcurrentTLru : TemplateConcurrentLru, TLruPolicy, NullHitCounter>
+ public sealed class FastConcurrentTLru : TemplateConcurrentLru, TLruTicksPolicy, NullHitCounter>
{
public FastConcurrentTLru(int concurrencyLevel, int capacity, IEqualityComparer comparer, TimeSpan timeToLive)
- : base(concurrencyLevel, capacity, comparer, new TLruPolicy(timeToLive), new NullHitCounter())
+ : base(concurrencyLevel, capacity, comparer, new TLruTicksPolicy(timeToLive), new NullHitCounter())
{
}
}
diff --git a/BitFaster.Caching/Lru/TickCountLruItem.cs b/BitFaster.Caching/Lru/TickCountLruItem.cs
new file mode 100644
index 00000000..43eb06d3
--- /dev/null
+++ b/BitFaster.Caching/Lru/TickCountLruItem.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BitFaster.Caching.Lru
+{
+ public class TickCountLruItem : LruItem
+ {
+ public TickCountLruItem(K key, V value)
+ : base(key, value)
+ {
+ this.TickCount = Environment.TickCount;
+ }
+
+ public int TickCount { get; set; }
+ }
+}
diff --git a/BitFaster.Caching/Lru/TlruPolicy.cs b/BitFaster.Caching/Lru/TlruDateTimePolicy.cs
similarity index 94%
rename from BitFaster.Caching/Lru/TlruPolicy.cs
rename to BitFaster.Caching/Lru/TlruDateTimePolicy.cs
index 228329d1..ecd7bd9c 100644
--- a/BitFaster.Caching/Lru/TlruPolicy.cs
+++ b/BitFaster.Caching/Lru/TlruDateTimePolicy.cs
@@ -11,11 +11,11 @@ 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 TLruPolicy : IPolicy>
+ public readonly struct TLruDateTimePolicy : IPolicy>
{
private readonly TimeSpan timeToLive;
- public TLruPolicy(TimeSpan timeToLive)
+ public TLruDateTimePolicy(TimeSpan timeToLive)
{
this.timeToLive = timeToLive;
}
diff --git a/BitFaster.Caching/Lru/TlruTicksPolicy.cs b/BitFaster.Caching/Lru/TlruTicksPolicy.cs
new file mode 100644
index 00000000..c308d91d
--- /dev/null
+++ b/BitFaster.Caching/Lru/TlruTicksPolicy.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+
+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.
+ ///
+ ///
+ /// This class measures time using Environment.TickCount, which is significantly faster
+ /// 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>
+ {
+ private readonly int timeToLive;
+
+ public TLruTicksPolicy(TimeSpan timeToLive)
+ {
+ this.timeToLive = (int)timeToLive.TotalMilliseconds;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TickCountLruItem CreateItem(K key, V value)
+ {
+ return new TickCountLruItem(key, value);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Touch(TickCountLruItem item)
+ {
+ item.WasAccessed = true;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool ShouldDiscard(TickCountLruItem item)
+ {
+ if (Environment.TickCount - item.TickCount > this.timeToLive)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ItemDestination RouteHot(TickCountLruItem item)
+ {
+ if (this.ShouldDiscard(item))
+ {
+ return ItemDestination.Remove;
+ }
+
+ if (item.WasAccessed)
+ {
+ return ItemDestination.Warm;
+ }
+
+ return ItemDestination.Cold;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ItemDestination RouteWarm(TickCountLruItem item)
+ {
+ if (this.ShouldDiscard(item))
+ {
+ return ItemDestination.Remove;
+ }
+
+ if (item.WasAccessed)
+ {
+ return ItemDestination.Warm;
+ }
+
+ return ItemDestination.Cold;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ItemDestination RouteCold(TickCountLruItem item)
+ {
+ if (this.ShouldDiscard(item))
+ {
+ return ItemDestination.Remove;
+ }
+
+ if (item.WasAccessed)
+ {
+ return ItemDestination.Warm;
+ }
+
+ return ItemDestination.Remove;
+ }
+ }
+}