From f3c08aa5d020478dbbe92c4ed763d9198d76a438 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 19 Aug 2022 18:57:13 -0700 Subject: [PATCH 1/8] notes --- .../Lfu/LfuCapacityPartitionTests.cs | 35 +++++++ BitFaster.Caching/Lfu/ConcurrentLfu.cs | 30 +++++- BitFaster.Caching/Lfu/LfuCapacityPartition.cs | 94 +++++++++++++++++-- 3 files changed, 151 insertions(+), 8 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs b/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs index cdce6fe9..d21be62f 100644 --- a/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs @@ -37,5 +37,40 @@ public void CtorSetsExpectedCapacity(int capacity, int expectedWindow, int expec partition.Protected.Should().Be(expectedProtected); partition.Probation.Should().Be(expectedProbation); } + + // Objective: calculate partitions based on hit rate changes. Assume ConcurrentLru will evict things + // scenario + // 1. start out by always trying to increase window size in iteration 1 + // 2. if hit rate increases in iteration 2, increase hit window again + // 3. if hit rate decreases in teration 2, decrease window + // 4. if hit rate continues to increase, apply decay until stable + [Fact] + public void TestOptimize() + { + int max = 100; + var partition = new LfuCapacityPartition(max); + var metrics = new TestMetrics(); + + metrics.Hits += 1000; + metrics.Misses += 2000; + + partition.Optimize(metrics, 10 * max); + + } + + private class TestMetrics : ICacheMetrics + { + public double HitRatio => (double)Hits / (double)Total; + + public long Total => Hits + Misses; + + public long Hits { get; set; } + + public long Misses { get; set; } + + public long Evicted { get; set; } + + public long Updated { get; set; } + } } } diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index f228de60..85e2fffa 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -20,6 +20,7 @@ using System.Threading.Tasks; using BitFaster.Caching.Lru; using BitFaster.Caching.Scheduler; +using static BitFaster.Caching.Lfu.LfuCapacityPartition; namespace BitFaster.Caching.Lfu { @@ -73,6 +74,8 @@ public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler) this.dictionary = new ConcurrentDictionary>>(concurrencyLevel, capacity, comparer); this.readBuffer = new StripedBuffer>>(concurrencyLevel, BufferSize); + + // TODO: how big should this be in total? We shouldn't allow more than some capacity % of writes in the buffer this.writeBuffer = new StripedBuffer>>(concurrencyLevel, BufferSize); this.cmSketch = new CmSketch(1, comparer); @@ -396,7 +399,13 @@ private bool Maintenance() ArrayPool>>.Shared.Return(localDrainBuffer); #endif - // TODO: hill climb + // Caffeine does two phase window then main eviction after draining queues. + // see void evictEntries() line 663 + this.capacity.Optimize(this.metrics, this.cmSketch.ResetSampleSize); + + // this should be some generalized eviction logic that forces items to fit within + // defined queue sizes + Rebalance(); // Reset to idle if either // 1. We drained both input buffers (all work done) @@ -530,6 +539,25 @@ private void PromoteProbation(LinkedListNode> node) } } + private void Rebalance() + { + //if (change == PartitionChange.IncreaseWindow) + //{ + // // move n items from protected to probation to window + + // // dequeue from protected + //} + //else if (change == PartitionChange.DecreaseWindow) + //{ + // // move n items from window to probation + + // var candidate = this.windowLru.First; + // this.windowLru.RemoveFirst(); + // this.probationLru.AddLast(candidate); + // candidate.Value.Position = Position.Probation; + //} + } + [DebuggerDisplay("{Format()}")] private class DrainStatus { diff --git a/BitFaster.Caching/Lfu/LfuCapacityPartition.cs b/BitFaster.Caching/Lfu/LfuCapacityPartition.cs index d5e76b66..11670ea2 100644 --- a/BitFaster.Caching/Lfu/LfuCapacityPartition.cs +++ b/BitFaster.Caching/Lfu/LfuCapacityPartition.cs @@ -7,13 +7,32 @@ namespace BitFaster.Caching.Lfu { public class LfuCapacityPartition { - private readonly int windowCapacity; - private readonly int protectedCapacity; - private readonly int probationCapacity; + private readonly int max; + + private int windowCapacity; + private int protectedCapacity; + private int probationCapacity; + + private double previousHitRate; + private long previousHitCount; + private long previousMissCount; + + private double mainRatio = DefaultMainPercentage; + private double stepSize; + + const double HILL_CLIMBER_RESTART_THRESHOLD = 0.05d; + const double HILL_CLIMBER_STEP_PERCENT = 0.0625d; + const double HILL_CLIMBER_STEP_DECAY_RATE = 0.98d; + + const double DefaultMainPercentage = 0.99d; public LfuCapacityPartition(int totalCapacity) { - (windowCapacity, protectedCapacity, probationCapacity) = ComputeQueueCapacity(totalCapacity); + this.max = totalCapacity; + (windowCapacity, protectedCapacity, probationCapacity) = ComputeQueueCapacity(totalCapacity, DefaultMainPercentage); + InitializeStepSize(totalCapacity); + + previousHitRate = 0.5; } public int Window => this.windowCapacity; @@ -22,16 +41,77 @@ public LfuCapacityPartition(int totalCapacity) public int Probation => this.probationCapacity; - public int Capacity => this.windowCapacity + this.protectedCapacity + this.probationCapacity; + public int Capacity => this.max; + + public enum PartitionChange + { + None, + IncreaseWindow, + DecreaseWindow, + } + + public void Optimize(ICacheMetrics metrics, int sampleThreshold) + { + long newHits = metrics.Hits; + long newMisses = metrics.Misses; + + long sampleHits = newHits - previousHitCount; + long sampleMisses = newMisses - previousMissCount; + long sampleCount = sampleHits + sampleMisses; + + if (sampleCount < sampleThreshold) + { + return; + } + + double sampleHitRate = (double)sampleHits / sampleCount; + + double hitRateChange = previousHitRate - sampleHitRate; + double amount = (hitRateChange >= 0) ? stepSize : -stepSize; + double nextStepSize = (Math.Abs(hitRateChange) >= HILL_CLIMBER_RESTART_THRESHOLD) + ? HILL_CLIMBER_STEP_PERCENT * Capacity * (amount >= 0 ? 1 : -1) + : HILL_CLIMBER_STEP_DECAY_RATE * amount; + + stepSize = nextStepSize; + + previousHitCount = newHits; + previousMissCount = newMisses; + previousHitRate = sampleHitRate; + + // Apply changes to the ratio of window to main. Window = recency-biased main = frequency-biased. + // Then in concurrentLfu, move items to preserve queue ratio + + // 6.35 + // 100 - 6.35 = 93.65 + // / 100 = 0.9365 + // * .99 = 0.927135 + + // => + + // 0.0635 starting step size + // + + // TODO: this should only adjust sizes of mainprotected and window + + mainRatio += amount; + (windowCapacity, protectedCapacity, probationCapacity) = ComputeQueueCapacity(max, mainRatio); + + return PartitionChange.None; + } + + private void InitializeStepSize(int cacheSize) + { + stepSize = -HILL_CLIMBER_STEP_PERCENT * cacheSize; + } - private static (int window, int mainProtected, int mainProbation) ComputeQueueCapacity(int capacity) + private static (int window, int mainProtected, int mainProbation) ComputeQueueCapacity(int capacity, double mainPercentage) { if (capacity < 3) { throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than or equal to 3."); } - int window = capacity - (int)(0.99 * capacity); + int window = capacity - (int)(mainPercentage * capacity); int mainProtected = (int)(0.8 * (capacity - window)); int mainProbation = capacity - window - mainProtected; From fc5b6b777a1e0b5560bf43496eea0b98f6181072 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 20 Aug 2022 13:20:32 -0700 Subject: [PATCH 2/8] notes --- BitFaster.Caching/Lfu/LfuCapacityPartition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching/Lfu/LfuCapacityPartition.cs b/BitFaster.Caching/Lfu/LfuCapacityPartition.cs index 11670ea2..594ffea5 100644 --- a/BitFaster.Caching/Lfu/LfuCapacityPartition.cs +++ b/BitFaster.Caching/Lfu/LfuCapacityPartition.cs @@ -96,7 +96,7 @@ public void Optimize(ICacheMetrics metrics, int sampleThreshold) mainRatio += amount; (windowCapacity, protectedCapacity, probationCapacity) = ComputeQueueCapacity(max, mainRatio); - return PartitionChange.None; + //return PartitionChange.None; } private void InitializeStepSize(int cacheSize) From 82448339ff9547e7220ea9d31696acd7ea20ed72 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 20 Aug 2022 18:36:06 -0700 Subject: [PATCH 3/8] poc --- .../Lfu/LfuCapacityPartitionTests.cs | 46 +++++++++++------- BitFaster.Caching/Lfu/LfuCapacityPartition.cs | 48 ------------------- 2 files changed, 29 insertions(+), 65 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs b/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs index b43edac8..91ec3f13 100644 --- a/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs @@ -70,8 +70,9 @@ public void TestOptimize() } + this.output.WriteLine("Decrease hit rate"); - for (int i = 0; i < 10; i++) + for (int i = 0; i < 2; i++) { metrics.Hits += 0001; metrics.Misses += 1000; @@ -82,33 +83,44 @@ public void TestOptimize() } - //metrics.Hits += 1000; - //metrics.Misses += 2000; + this.output.WriteLine("Increase hit rate"); - //partition.OptimizePartitioning(metrics, 10 * max); + for (int i = 0; i < 1; i++) + { + metrics.Hits += 1000; + metrics.Misses += 2000; - //partition.Window.Should().Be(8); + partition.OptimizePartitioning(metrics, 10 * max); - //metrics.Hits += 1000; - //metrics.Misses += 2000; + this.output.WriteLine($"W: {partition.Window} P: {partition.Protected}"); + + } + + this.output.WriteLine("Decrease hit rate"); + + for (int i = 0; i < 1; i++) + { + metrics.Hits += 0001; + metrics.Misses += 1000; - //partition.OptimizePartitioning(metrics, 10 * max); + partition.OptimizePartitioning(metrics, 10 * max); - //partition.Window.Should().Be(13); + this.output.WriteLine($"W: {partition.Window} P: {partition.Protected}"); - //metrics.Hits += 1000; - //metrics.Misses += 2000; + } - //partition.OptimizePartitioning(metrics, 10 * max); + this.output.WriteLine("Increase hit rate"); - //partition.Window.Should().Be(19); + for (int i = 0; i < 5; i++) + { + metrics.Hits += 1000; + metrics.Misses += 2000; - //metrics.Hits += 1000; - //metrics.Misses += 2000; + partition.OptimizePartitioning(metrics, 10 * max); - //partition.OptimizePartitioning(metrics, 10 * max); + this.output.WriteLine($"W: {partition.Window} P: {partition.Protected}"); - //partition.Window.Should().Be(24); + } } private class TestMetrics : ICacheMetrics diff --git a/BitFaster.Caching/Lfu/LfuCapacityPartition.cs b/BitFaster.Caching/Lfu/LfuCapacityPartition.cs index efd787e0..c0248a51 100644 --- a/BitFaster.Caching/Lfu/LfuCapacityPartition.cs +++ b/BitFaster.Caching/Lfu/LfuCapacityPartition.cs @@ -46,54 +46,6 @@ public LfuCapacityPartition(int totalCapacity) public int Capacity => this.max; // Apply changes to the ratio of window to main, window = recency-biased, main = frequency-biased. - public void OptimizePartitioning2(ICacheMetrics metrics, int sampleThreshold) - { - long newHits = metrics.Hits; - long newMisses = metrics.Misses; - - long sampleHits = newHits - previousHitCount; - long sampleMisses = newMisses - previousMissCount; - long sampleCount = sampleHits + sampleMisses; - - if (sampleCount < sampleThreshold) - { - return; - } - - double sampleHitRate = (double)sampleHits / sampleCount; - - double hitRateChange = previousHitRate - sampleHitRate; - double amount = (hitRateChange >= 0) ? stepSize : -stepSize; - double nextStepSize = (Math.Abs(hitRateChange) >= HillClimberRestartThreshold) - ? HillClimberStepPercent * Capacity * (amount >= 0 ? 1 : -1) - : HillClimberStepDecayRate * amount; - - stepSize = nextStepSize; - - previousHitCount = newHits; - previousMissCount = newMisses; - previousHitRate = sampleHitRate; - - // amount is actually how much to increment/decrement the window, expressed as a fraction of capacity - //Adjust(amount); - - - // 1.0625 = 100 + 6.25 / 100 - double x = (100 + amount) / 100.0; - - // 0.0625 - - mainRatio *= x; - mainRatio = Clamp(mainRatio, MinMainPercentage, MaxMainPercentage); - - (windowCapacity, protectedCapacity, probationCapacity) = ComputeQueueCapacity(max, mainRatio); - } - - private void InitializeStepSize2(int cacheSize) - { - stepSize = -HillClimberStepPercent * cacheSize; - } - public void OptimizePartitioning(ICacheMetrics metrics, int sampleThreshold) { long newHits = metrics.Hits; From ff13136f40f724538308fef2f14ce87bbdf99222 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 20 Aug 2022 19:13:34 -0700 Subject: [PATCH 4/8] adapt test --- .../Lfu/ConcurrentLfuTests.cs | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs index d9a0104f..3afe1c09 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs @@ -80,7 +80,7 @@ public void WhenNewItemsAreAddedTheyArePromotedBasedOnFrequency() LogLru(); for (int k = 0; k < 2; k++) - { + { for (int j = 0; j < 6; j++) { for (int i = 0; i < 15; i++) @@ -264,6 +264,71 @@ public void WriteUpdatesProtectedLruOrder() cache.TryGet(7, out var _).Should().BeTrue(); } + // TODO: there is a race condition here: this is sometimes failing with + // null ref inside the EvictFromMain method + [Fact] + public void Adapt() + { + // reset sample size is 200, so do 200 cache hits + + for (int i = 0; i < 20; i++) + { + cache.GetOrAdd(i, k => k); + } + + // W [19] Protected [] Probation [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18] + cache.PendingMaintenance(); + LogLru(); + + for (int i = 0; i < 15; i++) + { + cache.GetOrAdd(i, k => k); + } + + // W [19] Protected [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14] Probation [15,16,17,18] + cache.PendingMaintenance(); + LogLru(); + + + + for (int j = 0; j < 10; j++) + for (int i = 0; i < 20; i++) + { + cache.GetOrAdd(i, k => k); + } + + cache.PendingMaintenance(); + LogLru(); + + // then miss + for (int i = 0; i < 201; i++) + { + cache.GetOrAdd(i + 100, k => k); + } + + cache.PendingMaintenance(); + LogLru(); + + // then miss + for (int i = 0; i < 201; i++) + { + cache.GetOrAdd(i + 200, k => k); + } + + cache.PendingMaintenance(); + LogLru(); + + for (int i = 0; i < 200; i++) + { + cache.GetOrAdd(1, k => k); + } + + cache.PendingMaintenance(); + LogLru(); + + // TODO: how to verify this? + } + [Fact] public void ReadSchedulesMaintenanceWhenBufferIsFull() { From 9ec08f826618a8e281745f659d6035bd8fad4f61 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 21 Aug 2022 15:50:01 -0700 Subject: [PATCH 5/8] fix gap --- .../Lfu/ConcurrentLfuTests.cs | 15 +++++++++++---- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs index 3afe1c09..11ea99e3 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs @@ -269,8 +269,10 @@ public void WriteUpdatesProtectedLruOrder() [Fact] public void Adapt() { - // reset sample size is 200, so do 200 cache hits + var scheduler = new TestScheduler(); + cache = new ConcurrentLfu(1, 20, scheduler); + // First completely fill the cache, push entries into protected for (int i = 0; i < 20; i++) { cache.GetOrAdd(i, k => k); @@ -290,7 +292,8 @@ public void Adapt() LogLru(); - + // The reset sample size is 200, so do 200 cache hits + // W [19] Protected [12,13,14,15,16,17,18,0,1,2,3,4,5,6,7] Probation [8,9,10,11] for (int j = 0; j < 10; j++) for (int i = 0; i < 20; i++) { @@ -300,7 +303,8 @@ public void Adapt() cache.PendingMaintenance(); LogLru(); - // then miss + // then miss 200 times + // W [300] Protected [12,13,14,15,16,17,18,0,1,2,3,4,5,6,7] Probation [9,10,11,227] for (int i = 0; i < 201; i++) { cache.GetOrAdd(i + 100, k => k); @@ -309,7 +313,8 @@ public void Adapt() cache.PendingMaintenance(); LogLru(); - // then miss + // then miss 200 more times (window adaptation) + // W [399,400] Protected [14,15,16,17,18,0,1,2,3,4,5,6,7,227] Probation [9,10,11,12] for (int i = 0; i < 201; i++) { cache.GetOrAdd(i + 200, k => k); @@ -318,6 +323,8 @@ public void Adapt() cache.PendingMaintenance(); LogLru(); + // 200 hits, no adaptation + // W [399,400] Protected [14,15,16,17,18,0,2,3,4,5,6,7,227,1] Probation [9,10,11,12] for (int i = 0; i < 200; i++) { cache.GetOrAdd(1, k => k); diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 3799da11..307a1044 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -525,13 +525,13 @@ private int EvictFromWindow() private void EvictFromMain(int candidates) { - //var victimQueue = Position.Probation; + // var victimQueue = Position.Probation; var victim = this.probationLru.First; var candidate = this.probationLru.Last; while (this.windowLru.Count + this.probationLru.Count + this.protectedLru.Count > this.Capacity) { - // TODO: is this logic reachable? + // TODO: this logic is only reachable if entries have time expiry, and are removed early. // Search the admission window for additional candidates //if (candidates == 0) //{ @@ -559,7 +559,7 @@ private void EvictFromMain(int candidates) // break; //} - //// Evict immediately if only one of the entries is present + // Evict immediately if only one of the entries is present //if (victim == null) //{ // var previous = candidate.Previous; @@ -585,13 +585,17 @@ private void EvictFromMain(int candidates) if (AdmitCandidate(candidate.Key, victim.Key)) { var evictee = victim; - victim = victim.Previous; + + // victim is initialized to first, and iterates forwards + victim = victim.Next; Evict(evictee); } else { var evictee = candidate; + + // candidate is initialized to last, and iterates backwards candidate = candidate.Previous; Evict(evictee); @@ -622,6 +626,12 @@ private void ReFitProtected() while (this.protectedLru.Count > this.capacity.Protected) { var demoted = this.protectedLru.First; + + if (demoted == null) + { + throw new Exception("demoted == null"); + } + this.protectedLru.RemoveFirst(); demoted.Position = Position.Probation; From 247e37c5ce60099b222383dbae6155c47d1e4df9 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 21 Aug 2022 15:56:51 -0700 Subject: [PATCH 6/8] rem check --- BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs | 2 -- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 6 ------ 2 files changed, 8 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs index 11ea99e3..6ac0f2b6 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs @@ -264,8 +264,6 @@ public void WriteUpdatesProtectedLruOrder() cache.TryGet(7, out var _).Should().BeTrue(); } - // TODO: there is a race condition here: this is sometimes failing with - // null ref inside the EvictFromMain method [Fact] public void Adapt() { diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 307a1044..259a91c8 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -626,12 +626,6 @@ private void ReFitProtected() while (this.protectedLru.Count > this.capacity.Protected) { var demoted = this.protectedLru.First; - - if (demoted == null) - { - throw new Exception("demoted == null"); - } - this.protectedLru.RemoveFirst(); demoted.Position = Position.Probation; From 844c6c3f9423e9daa279e7a90ba286cdf7a1ca04 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 21 Aug 2022 20:45:18 -0700 Subject: [PATCH 7/8] tests --- .../Lfu/ConcurrentLfuTests.cs | 22 ++- .../Lfu/LfuCapacityPartitionTests.cs | 141 ++++++++++++------ 2 files changed, 108 insertions(+), 55 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs index 6ac0f2b6..a2c87bd2 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs @@ -265,10 +265,9 @@ public void WriteUpdatesProtectedLruOrder() } [Fact] - public void Adapt() + public void WhenHitRateChangesWindowSizeIsAdapted() { - var scheduler = new TestScheduler(); - cache = new ConcurrentLfu(1, 20, scheduler); + cache = new ConcurrentLfu(1, 20, new NullScheduler()); // First completely fill the cache, push entries into protected for (int i = 0; i < 20; i++) @@ -289,7 +288,6 @@ public void Adapt() cache.PendingMaintenance(); LogLru(); - // The reset sample size is 200, so do 200 cache hits // W [19] Protected [12,13,14,15,16,17,18,0,1,2,3,4,5,6,7] Probation [8,9,10,11] for (int j = 0; j < 10; j++) @@ -311,7 +309,7 @@ public void Adapt() cache.PendingMaintenance(); LogLru(); - // then miss 200 more times (window adaptation) + // then miss 200 more times (window adaptation +1 window slots) // W [399,400] Protected [14,15,16,17,18,0,1,2,3,4,5,6,7,227] Probation [9,10,11,12] for (int i = 0; i < 201; i++) { @@ -321,17 +319,17 @@ public void Adapt() cache.PendingMaintenance(); LogLru(); - // 200 hits, no adaptation - // W [399,400] Protected [14,15,16,17,18,0,2,3,4,5,6,7,227,1] Probation [9,10,11,12] - for (int i = 0; i < 200; i++) - { - cache.GetOrAdd(1, k => k); - } + // make 2 requests to new keys, if window is size is now 2 both will exist: + cache.GetOrAdd(666, k => k); + cache.GetOrAdd(667, k => k); cache.PendingMaintenance(); LogLru(); - // TODO: how to verify this? + cache.TryGet(666, out var _).Should().BeTrue(); + cache.TryGet(667, out var _).Should().BeTrue(); + + this.output.WriteLine($"Scheduler ran {cache.Scheduler.RunCount} times."); } [Fact] diff --git a/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs b/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs index 91ec3f13..1c956f7c 100644 --- a/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs @@ -46,80 +46,135 @@ public void CtorSetsExpectedCapacity(int capacity, int expectedWindow, int expec partition.Probation.Should().Be(expectedProbation); } - // Objective: calculate partitions based on hit rate changes. Assume ConcurrentLru will evict things - // scenario - // 1. start out by always trying to increase window size in iteration 1 - // 2. if hit rate increases in iteration 2, increase hit window again - // 3. if hit rate decreases in teration 2, decrease window - // 4. if hit rate continues to increase, apply decay until stable [Fact] - public void TestOptimize() + public void WhenHitRateKeepsDecreasingWindowIsCappedAt80Percent() { int max = 100; var partition = new LfuCapacityPartition(max); var metrics = new TestMetrics(); - for (int i = 0; i < 10; i++) + SetHitRate(partition, metrics, max, 0.9); + + for (int i = 0; i < 20; i++) { - metrics.Hits += 1000; - metrics.Misses += 2000; + SetHitRate(partition, metrics, max, 0.1); + } - partition.OptimizePartitioning(metrics, 10 * max); + partition.Window.Should().Be(80); + partition.Protected.Should().Be(16); + } - this.output.WriteLine($"W: {partition.Window} P: {partition.Protected}"); - } + [Fact] + public void WhenHitRateIsStableWindowConverges() + { + int max = 100; + var partition = new LfuCapacityPartition(max); + var metrics = new TestMetrics(); - this.output.WriteLine("Decrease hit rate"); + // start by causing some adaptation in window so that steady state is not window = 1 + SetHitRate(partition, metrics, max, 0.9); - for (int i = 0; i < 2; i++) + for (int i = 0; i < 5; i++) { - metrics.Hits += 0001; - metrics.Misses += 1000; + SetHitRate(partition, metrics, max, 0.1); + } - partition.OptimizePartitioning(metrics, 10 * max); + this.output.WriteLine("Decrease hit rate"); + SetHitRate(partition, metrics, max, 0.0); + // window is now larger - this.output.WriteLine($"W: {partition.Window} P: {partition.Protected}"); + // go into steady state with small up and down fluctuation in hit rate + List windowSizes = new List(200); + this.output.WriteLine("Stable hit rate"); + double inc = 0.01; + for (int i = 0; i < 200; i++) + { + double c = i % 2 == 0 ? inc : -inc; + SetHitRate(partition, metrics, max, 0.9 + c); + + windowSizes.Add(partition.Window); } - this.output.WriteLine("Increase hit rate"); + // verify that hit rate has converged, last 50 samples have low variance + var last50 = windowSizes.Skip(150).Take(50).ToArray(); - for (int i = 0; i < 1; i++) - { - metrics.Hits += 1000; - metrics.Misses += 2000; + var minWindow = last50.Min(); + var maxWindow = last50.Max(); - partition.OptimizePartitioning(metrics, 10 * max); + (maxWindow - minWindow).Should().BeLessThanOrEqualTo(1); + } - this.output.WriteLine($"W: {partition.Window} P: {partition.Protected}"); + [Fact] + public void WhenHitRateFluctuatesWindowIsAdapted() + { + int max = 100; + var partition = new LfuCapacityPartition(max); + var metrics = new TestMetrics(); - } + var snapshot = new WindowSnapshot(); + + // steady state, window stays at 1 initially + SetHitRate(partition, metrics, max, 0.9); + SetHitRate(partition, metrics, max, 0.9); + snapshot.Capture(partition); + + // Decrease hit rate, verify window increases each time + this.output.WriteLine("1. Decrease hit rate"); + SetHitRate(partition, metrics, max, 0.1); + snapshot.AssertWindowIncreased(partition); + SetHitRate(partition, metrics, max, 0.1); + snapshot.AssertWindowIncreased(partition); + + // Increase hit rate, verify window continues to increase + this.output.WriteLine("2. Increase hit rate"); + SetHitRate(partition, metrics, max, 0.9); + snapshot.AssertWindowIncreased(partition); + + // Decrease hit rate, verify window decreases + this.output.WriteLine("3. Decrease hit rate"); + SetHitRate(partition, metrics, max, 0.1); + snapshot.AssertWindowDecreased(partition); + + // Increase hit rate, verify window continues to decrease + this.output.WriteLine("4. Increase hit rate"); + SetHitRate(partition, metrics, max, 0.9); + snapshot.AssertWindowDecreased(partition); + SetHitRate(partition, metrics, max, 0.9); + snapshot.AssertWindowDecreased(partition); + } - this.output.WriteLine("Decrease hit rate"); + private void SetHitRate(LfuCapacityPartition p, TestMetrics m, int max, double hitRate) + { + int total = max * 10; + m.Hits += (long)(total * hitRate); + m.Misses += total - (long)(total * hitRate); - for (int i = 0; i < 1; i++) - { - metrics.Hits += 0001; - metrics.Misses += 1000; + p.OptimizePartitioning(m, total); - partition.OptimizePartitioning(metrics, 10 * max); + this.output.WriteLine($"W: {p.Window} P: {p.Protected}"); + } - this.output.WriteLine($"W: {partition.Window} P: {partition.Protected}"); + private class WindowSnapshot + { + private int prev; + public void Capture(LfuCapacityPartition p) + { + prev = p.Window; } - this.output.WriteLine("Increase hit rate"); - - for (int i = 0; i < 5; i++) + public void AssertWindowIncreased(LfuCapacityPartition p) { - metrics.Hits += 1000; - metrics.Misses += 2000; - - partition.OptimizePartitioning(metrics, 10 * max); - - this.output.WriteLine($"W: {partition.Window} P: {partition.Protected}"); + p.Window.Should().BeGreaterThan(prev); + prev = p.Window; + } + public void AssertWindowDecreased(LfuCapacityPartition p) + { + p.Window.Should().BeLessThan(prev); + prev = p.Window; } } From 64c4efec9d24f803eb66f96d9741c49f1aa9b0bb Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 21 Aug 2022 20:46:46 -0700 Subject: [PATCH 8/8] ws --- BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs b/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs index 1c956f7c..08e1ba62 100644 --- a/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs @@ -64,7 +64,6 @@ public void WhenHitRateKeepsDecreasingWindowIsCappedAt80Percent() partition.Protected.Should().Be(16); } - [Fact] public void WhenHitRateIsStableWindowConverges() {