Skip to content

Commit 20f8412

Browse files
authored
hill climbing (#191)
* notes * notes * poc * adapt test * fix gap * rem check * tests * ws
1 parent 974d7a1 commit 20f8412

File tree

4 files changed

+325
-14
lines changed

4 files changed

+325
-14
lines changed

BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public void WhenNewItemsAreAddedTheyArePromotedBasedOnFrequency()
8080
LogLru();
8181

8282
for (int k = 0; k < 2; k++)
83-
{
83+
{
8484
for (int j = 0; j < 6; j++)
8585
{
8686
for (int i = 0; i < 15; i++)
@@ -264,6 +264,74 @@ public void WriteUpdatesProtectedLruOrder()
264264
cache.TryGet(7, out var _).Should().BeTrue();
265265
}
266266

267+
[Fact]
268+
public void WhenHitRateChangesWindowSizeIsAdapted()
269+
{
270+
cache = new ConcurrentLfu<int, int>(1, 20, new NullScheduler());
271+
272+
// First completely fill the cache, push entries into protected
273+
for (int i = 0; i < 20; i++)
274+
{
275+
cache.GetOrAdd(i, k => k);
276+
}
277+
278+
// W [19] Protected [] Probation [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]
279+
cache.PendingMaintenance();
280+
LogLru();
281+
282+
for (int i = 0; i < 15; i++)
283+
{
284+
cache.GetOrAdd(i, k => k);
285+
}
286+
287+
// W [19] Protected [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14] Probation [15,16,17,18]
288+
cache.PendingMaintenance();
289+
LogLru();
290+
291+
// The reset sample size is 200, so do 200 cache hits
292+
// W [19] Protected [12,13,14,15,16,17,18,0,1,2,3,4,5,6,7] Probation [8,9,10,11]
293+
for (int j = 0; j < 10; j++)
294+
for (int i = 0; i < 20; i++)
295+
{
296+
cache.GetOrAdd(i, k => k);
297+
}
298+
299+
cache.PendingMaintenance();
300+
LogLru();
301+
302+
// then miss 200 times
303+
// W [300] Protected [12,13,14,15,16,17,18,0,1,2,3,4,5,6,7] Probation [9,10,11,227]
304+
for (int i = 0; i < 201; i++)
305+
{
306+
cache.GetOrAdd(i + 100, k => k);
307+
}
308+
309+
cache.PendingMaintenance();
310+
LogLru();
311+
312+
// then miss 200 more times (window adaptation +1 window slots)
313+
// W [399,400] Protected [14,15,16,17,18,0,1,2,3,4,5,6,7,227] Probation [9,10,11,12]
314+
for (int i = 0; i < 201; i++)
315+
{
316+
cache.GetOrAdd(i + 200, k => k);
317+
}
318+
319+
cache.PendingMaintenance();
320+
LogLru();
321+
322+
// make 2 requests to new keys, if window is size is now 2 both will exist:
323+
cache.GetOrAdd(666, k => k);
324+
cache.GetOrAdd(667, k => k);
325+
326+
cache.PendingMaintenance();
327+
LogLru();
328+
329+
cache.TryGet(666, out var _).Should().BeTrue();
330+
cache.TryGet(667, out var _).Should().BeTrue();
331+
332+
this.output.WriteLine($"Scheduler ran {cache.Scheduler.RunCount} times.");
333+
}
334+
267335
[Fact]
268336
public void ReadSchedulesMaintenanceWhenBufferIsFull()
269337
{

BitFaster.Caching.UnitTests/Lfu/LfuCapacityPartitionTests.cs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@
66
using BitFaster.Caching.Lfu;
77
using FluentAssertions;
88
using Xunit;
9+
using Xunit.Abstractions;
910

1011
namespace BitFaster.Caching.UnitTests.Lfu
1112
{
1213
public class LfuCapacityPartitionTests
1314
{
15+
private readonly ITestOutputHelper output;
16+
17+
public LfuCapacityPartitionTests(ITestOutputHelper output)
18+
{
19+
this.output = output;
20+
}
21+
1422
[Fact]
1523
public void WhenCapacityIsLessThan3CtorThrows()
1624
{
@@ -37,5 +45,151 @@ public void CtorSetsExpectedCapacity(int capacity, int expectedWindow, int expec
3745
partition.Protected.Should().Be(expectedProtected);
3846
partition.Probation.Should().Be(expectedProbation);
3947
}
48+
49+
[Fact]
50+
public void WhenHitRateKeepsDecreasingWindowIsCappedAt80Percent()
51+
{
52+
int max = 100;
53+
var partition = new LfuCapacityPartition(max);
54+
var metrics = new TestMetrics();
55+
56+
SetHitRate(partition, metrics, max, 0.9);
57+
58+
for (int i = 0; i < 20; i++)
59+
{
60+
SetHitRate(partition, metrics, max, 0.1);
61+
}
62+
63+
partition.Window.Should().Be(80);
64+
partition.Protected.Should().Be(16);
65+
}
66+
67+
[Fact]
68+
public void WhenHitRateIsStableWindowConverges()
69+
{
70+
int max = 100;
71+
var partition = new LfuCapacityPartition(max);
72+
var metrics = new TestMetrics();
73+
74+
// start by causing some adaptation in window so that steady state is not window = 1
75+
SetHitRate(partition, metrics, max, 0.9);
76+
77+
for (int i = 0; i < 5; i++)
78+
{
79+
SetHitRate(partition, metrics, max, 0.1);
80+
}
81+
82+
this.output.WriteLine("Decrease hit rate");
83+
SetHitRate(partition, metrics, max, 0.0);
84+
// window is now larger
85+
86+
// go into steady state with small up and down fluctuation in hit rate
87+
List<int> windowSizes = new List<int>(200);
88+
this.output.WriteLine("Stable hit rate");
89+
90+
double inc = 0.01;
91+
for (int i = 0; i < 200; i++)
92+
{
93+
double c = i % 2 == 0 ? inc : -inc;
94+
SetHitRate(partition, metrics, max, 0.9 + c);
95+
96+
windowSizes.Add(partition.Window);
97+
}
98+
99+
// verify that hit rate has converged, last 50 samples have low variance
100+
var last50 = windowSizes.Skip(150).Take(50).ToArray();
101+
102+
var minWindow = last50.Min();
103+
var maxWindow = last50.Max();
104+
105+
(maxWindow - minWindow).Should().BeLessThanOrEqualTo(1);
106+
}
107+
108+
[Fact]
109+
public void WhenHitRateFluctuatesWindowIsAdapted()
110+
{
111+
int max = 100;
112+
var partition = new LfuCapacityPartition(max);
113+
var metrics = new TestMetrics();
114+
115+
var snapshot = new WindowSnapshot();
116+
117+
// steady state, window stays at 1 initially
118+
SetHitRate(partition, metrics, max, 0.9);
119+
SetHitRate(partition, metrics, max, 0.9);
120+
snapshot.Capture(partition);
121+
122+
// Decrease hit rate, verify window increases each time
123+
this.output.WriteLine("1. Decrease hit rate");
124+
SetHitRate(partition, metrics, max, 0.1);
125+
snapshot.AssertWindowIncreased(partition);
126+
SetHitRate(partition, metrics, max, 0.1);
127+
snapshot.AssertWindowIncreased(partition);
128+
129+
// Increase hit rate, verify window continues to increase
130+
this.output.WriteLine("2. Increase hit rate");
131+
SetHitRate(partition, metrics, max, 0.9);
132+
snapshot.AssertWindowIncreased(partition);
133+
134+
// Decrease hit rate, verify window decreases
135+
this.output.WriteLine("3. Decrease hit rate");
136+
SetHitRate(partition, metrics, max, 0.1);
137+
snapshot.AssertWindowDecreased(partition);
138+
139+
// Increase hit rate, verify window continues to decrease
140+
this.output.WriteLine("4. Increase hit rate");
141+
SetHitRate(partition, metrics, max, 0.9);
142+
snapshot.AssertWindowDecreased(partition);
143+
SetHitRate(partition, metrics, max, 0.9);
144+
snapshot.AssertWindowDecreased(partition);
145+
}
146+
147+
private void SetHitRate(LfuCapacityPartition p, TestMetrics m, int max, double hitRate)
148+
{
149+
int total = max * 10;
150+
m.Hits += (long)(total * hitRate);
151+
m.Misses += total - (long)(total * hitRate);
152+
153+
p.OptimizePartitioning(m, total);
154+
155+
this.output.WriteLine($"W: {p.Window} P: {p.Protected}");
156+
}
157+
158+
private class WindowSnapshot
159+
{
160+
private int prev;
161+
162+
public void Capture(LfuCapacityPartition p)
163+
{
164+
prev = p.Window;
165+
}
166+
167+
public void AssertWindowIncreased(LfuCapacityPartition p)
168+
{
169+
p.Window.Should().BeGreaterThan(prev);
170+
prev = p.Window;
171+
}
172+
173+
public void AssertWindowDecreased(LfuCapacityPartition p)
174+
{
175+
p.Window.Should().BeLessThan(prev);
176+
prev = p.Window;
177+
}
178+
}
179+
180+
private class TestMetrics : ICacheMetrics
181+
{
182+
public double HitRatio => (double)Hits / (double)Total;
183+
184+
public long Total => Hits + Misses;
185+
186+
public long Hits { get; set; }
187+
188+
public long Misses { get; set; }
189+
190+
public long Evicted { get; set; }
191+
192+
public long Updated { get; set; }
193+
}
40194
}
41195
}

BitFaster.Caching/Lfu/ConcurrentLfu.cs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using BitFaster.Caching.Buffers;
2323
using BitFaster.Caching.Lru;
2424
using BitFaster.Caching.Scheduler;
25+
using static BitFaster.Caching.Lfu.LfuCapacityPartition;
2526

2627
namespace BitFaster.Caching.Lfu
2728
{
@@ -75,6 +76,8 @@ public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler)
7576
this.dictionary = new ConcurrentDictionary<K, LfuNode<K, V>>(concurrencyLevel, capacity, comparer);
7677

7778
this.readBuffer = new StripedBuffer<LfuNode<K, V>>(concurrencyLevel, BufferSize);
79+
80+
// TODO: how big should this be in total? We shouldn't allow more than some capacity % of writes in the buffer
7881
this.writeBuffer = new StripedBuffer<LfuNode<K, V>>(concurrencyLevel, BufferSize);
7982

8083
this.cmSketch = new CmSketch<K>(1, comparer);
@@ -396,8 +399,9 @@ private bool Maintenance()
396399
ArrayPool<LfuNode<K, V>>.Shared.Return(localDrainBuffer);
397400
#endif
398401

399-
// TODO: hill climb
400402
EvictEntries();
403+
this.capacity.OptimizePartitioning(this.metrics, this.cmSketch.ResetSampleSize);
404+
ReFitProtected();
401405

402406
// Reset to idle if either
403407
// 1. We drained both input buffers (all work done)
@@ -521,13 +525,13 @@ private int EvictFromWindow()
521525

522526
private void EvictFromMain(int candidates)
523527
{
524-
//var victimQueue = Position.Probation;
528+
// var victimQueue = Position.Probation;
525529
var victim = this.probationLru.First;
526530
var candidate = this.probationLru.Last;
527531

528532
while (this.windowLru.Count + this.probationLru.Count + this.protectedLru.Count > this.Capacity)
529533
{
530-
// TODO: is this logic reachable?
534+
// TODO: this logic is only reachable if entries have time expiry, and are removed early.
531535
// Search the admission window for additional candidates
532536
//if (candidates == 0)
533537
//{
@@ -555,7 +559,7 @@ private void EvictFromMain(int candidates)
555559
// break;
556560
//}
557561

558-
//// Evict immediately if only one of the entries is present
562+
// Evict immediately if only one of the entries is present
559563
//if (victim == null)
560564
//{
561565
// var previous = candidate.Previous;
@@ -581,13 +585,17 @@ private void EvictFromMain(int candidates)
581585
if (AdmitCandidate(candidate.Key, victim.Key))
582586
{
583587
var evictee = victim;
584-
victim = victim.Previous;
588+
589+
// victim is initialized to first, and iterates forwards
590+
victim = victim.Next;
585591

586592
Evict(evictee);
587593
}
588594
else
589595
{
590596
var evictee = candidate;
597+
598+
// candidate is initialized to last, and iterates backwards
591599
candidate = candidate.Previous;
592600

593601
Evict(evictee);
@@ -611,6 +619,20 @@ private void Evict(LfuNode<K, V> evictee)
611619
this.metrics.evictedCount++;
612620
}
613621

622+
private void ReFitProtected()
623+
{
624+
// If hill climbing decreased protected, there may be too many items
625+
// - demote overflow to probation.
626+
while (this.protectedLru.Count > this.capacity.Protected)
627+
{
628+
var demoted = this.protectedLru.First;
629+
this.protectedLru.RemoveFirst();
630+
631+
demoted.Position = Position.Probation;
632+
this.probationLru.AddLast(demoted);
633+
}
634+
}
635+
614636
[DebuggerDisplay("{Format()}")]
615637
private class DrainStatus
616638
{

0 commit comments

Comments
 (0)