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; + } + } +}