Skip to content

Commit ce9cc3b

Browse files
authored
Extract ConcurrentLfu (#522)
1 parent a298f4a commit ce9cc3b

File tree

2 files changed

+248
-237
lines changed

2 files changed

+248
-237
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Threading.Tasks;
7+
using BitFaster.Caching.Buffers;
8+
using BitFaster.Caching.Lru;
9+
using BitFaster.Caching.Scheduler;
10+
11+
namespace BitFaster.Caching.Lfu
12+
{
13+
/// <summary>
14+
/// An approximate LFU based on the W-TinyLfu eviction policy. W-TinyLfu tracks items using a window LRU list, and
15+
/// a main space LRU divided into protected and probation segments. Reads and writes to the cache are stored in buffers
16+
/// and later applied to the policy LRU lists in batches under a lock. Each read and write is tracked using a compact
17+
/// popularity sketch to probalistically estimate item frequency. Items proceed through the LRU lists as follows:
18+
/// <list type="number">
19+
/// <item><description>New items are added to the window LRU. When acessed window items move to the window MRU position.</description></item>
20+
/// <item><description>When the window is full, candidate items are moved to the probation segment in LRU order.</description></item>
21+
/// <item><description>When the main space is full, the access frequency of each window candidate is compared
22+
/// to probation victims in LRU order. The item with the lowest frequency is evicted until the cache size is within bounds.</description></item>
23+
/// <item><description>When a probation item is accessed, it is moved to the protected segment. If the protected segment is full,
24+
/// the LRU protected item is demoted to probation.</description></item>
25+
/// <item><description>When a protected item is accessed, it is moved to the protected MRU position.</description></item>
26+
/// </list>
27+
/// The size of the admission window and main space are adapted over time to iteratively improve hit rate using a
28+
/// hill climbing algorithm. A larger window favors workloads with high recency bias, whereas a larger main space
29+
/// favors workloads with frequency bias.
30+
/// </summary>
31+
/// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes).
32+
/// https://github.com/ben-manes/caffeine
33+
[DebuggerTypeProxy(typeof(ConcurrentLfu<,>.LfuDebugView<>))]
34+
[DebuggerDisplay("Count = {Count}/{Capacity}")]
35+
public sealed class ConcurrentLfu<K, V> : ICache<K, V>, IAsyncCache<K, V>, IBoundedPolicy
36+
{
37+
// Note: for performance reasons this is a mutable struct, it cannot be readonly.
38+
private ConcurrentLfuCore<K, V, AccessOrderNode<K, V>, AccessOrderPolicy<K, V>> core;
39+
40+
/// <summary>
41+
/// The default buffer size.
42+
/// </summary>
43+
public const int DefaultBufferSize = 128;
44+
45+
/// <summary>
46+
/// Initializes a new instance of the ConcurrentLfu class with the specified capacity.
47+
/// </summary>
48+
/// <param name="capacity">The capacity.</param>
49+
public ConcurrentLfu(int capacity)
50+
{
51+
this.core = new(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer<K>.Default, () => this.DrainBuffers());
52+
}
53+
54+
/// <summary>
55+
/// Initializes a new instance of the ConcurrentLfu class with the specified concurrencyLevel, capacity, scheduler, equality comparer and buffer size.
56+
/// </summary>
57+
/// <param name="concurrencyLevel">The concurrency level.</param>
58+
/// <param name="capacity">The capacity.</param>
59+
/// <param name="scheduler">The scheduler.</param>
60+
/// <param name="comparer">The equality comparer.</param>
61+
public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer<K> comparer)
62+
{
63+
this.core = new(concurrencyLevel, capacity, scheduler, comparer, () => this.DrainBuffers());
64+
}
65+
66+
internal ConcurrentLfuCore<K, V, AccessOrderNode<K, V>, AccessOrderPolicy<K, V>> Core => core;
67+
68+
// structs cannot declare self referencing lambda functions, therefore pass this in from the ctor
69+
private void DrainBuffers()
70+
{
71+
this.core.DrainBuffers();
72+
}
73+
74+
///<inheritdoc/>
75+
public int Count => core.Count;
76+
77+
///<inheritdoc/>
78+
public Optional<ICacheMetrics> Metrics => core.Metrics;
79+
80+
///<inheritdoc/>
81+
public Optional<ICacheEvents<K, V>> Events => core.Events;
82+
83+
///<inheritdoc/>
84+
public CachePolicy Policy => core.Policy;
85+
86+
///<inheritdoc/>
87+
public ICollection<K> Keys => core.Keys;
88+
89+
///<inheritdoc/>
90+
public int Capacity => core.Capacity;
91+
92+
///<inheritdoc/>
93+
public IScheduler Scheduler => core.Scheduler;
94+
95+
/// <summary>
96+
/// Synchronously perform all pending policy maintenance. Drain the read and write buffers then
97+
/// use the eviction policy to preserve bounded size and remove expired items.
98+
/// </summary>
99+
/// <remarks>
100+
/// Note: maintenance is automatically performed asynchronously immediately following a read or write.
101+
/// It is not necessary to call this method, <see cref="DoMaintenance"/> is provided purely to enable tests to reach a consistent state.
102+
/// </remarks>
103+
public void DoMaintenance()
104+
{
105+
core.DoMaintenance();
106+
}
107+
108+
///<inheritdoc/>
109+
public void AddOrUpdate(K key, V value)
110+
{
111+
core.AddOrUpdate(key, value);
112+
}
113+
114+
///<inheritdoc/>
115+
public void Clear()
116+
{
117+
core.Clear();
118+
}
119+
120+
///<inheritdoc/>
121+
public V GetOrAdd(K key, Func<K, V> valueFactory)
122+
{
123+
return core.GetOrAdd(key, valueFactory);
124+
}
125+
126+
///<inheritdoc/>
127+
public V GetOrAdd<TArg>(K key, Func<K, TArg, V> valueFactory, TArg factoryArgument)
128+
{
129+
return core.GetOrAdd(key, valueFactory, factoryArgument);
130+
}
131+
132+
///<inheritdoc/>
133+
public ValueTask<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
134+
{
135+
return core.GetOrAddAsync(key, valueFactory);
136+
}
137+
138+
///<inheritdoc/>
139+
public ValueTask<V> GetOrAddAsync<TArg>(K key, Func<K, TArg, Task<V>> valueFactory, TArg factoryArgument)
140+
{
141+
return core.GetOrAddAsync(key, valueFactory, factoryArgument);
142+
}
143+
144+
///<inheritdoc/>
145+
public void Trim(int itemCount)
146+
{
147+
core.Trim(itemCount);
148+
}
149+
150+
///<inheritdoc/>
151+
public bool TryGet(K key, out V value)
152+
{
153+
return core.TryGet(key, out value);
154+
}
155+
156+
///<inheritdoc/>
157+
public bool TryRemove(K key)
158+
{
159+
return core.TryRemove(key);
160+
}
161+
162+
/// <summary>
163+
/// Attempts to remove the specified key value pair.
164+
/// </summary>
165+
/// <param name="item">The item to remove.</param>
166+
/// <returns>true if the item was removed successfully; otherwise, false.</returns>
167+
public bool TryRemove(KeyValuePair<K, V> item)
168+
{
169+
return core.TryRemove(item);
170+
}
171+
172+
/// <summary>
173+
/// Attempts to remove and return the value that has the specified key.
174+
/// </summary>
175+
/// <param name="key">The key of the element to remove.</param>
176+
/// <param name="value">When this method returns, contains the object removed, or the default value of the value type if key does not exist.</param>
177+
/// <returns>true if the object was removed successfully; otherwise, false.</returns>
178+
public bool TryRemove(K key, out V value)
179+
{
180+
return core.TryRemove(key, out value);
181+
}
182+
183+
///<inheritdoc/>
184+
public bool TryUpdate(K key, V value)
185+
{
186+
return core.TryUpdate(key, value);
187+
}
188+
189+
///<inheritdoc/>
190+
public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
191+
{
192+
return core.GetEnumerator();
193+
}
194+
195+
///<inheritdoc/>
196+
IEnumerator IEnumerable.GetEnumerator()
197+
{
198+
return core.GetEnumerator();
199+
}
200+
201+
#if DEBUG
202+
/// <summary>
203+
/// Format the LFU as a string by converting all the keys to strings.
204+
/// </summary>
205+
/// <returns>The LFU formatted as a string.</returns>
206+
public string FormatLfuString()
207+
{
208+
return core.FormatLfuString();
209+
}
210+
#endif
211+
212+
[ExcludeFromCodeCoverage]
213+
internal class LfuDebugView<N>
214+
where N : LfuNode<K, V>
215+
{
216+
private readonly ConcurrentLfu<K, V> lfu;
217+
218+
public LfuDebugView(ConcurrentLfu<K, V> lfu)
219+
{
220+
this.lfu = lfu;
221+
}
222+
223+
public string Maintenance => lfu.core.drainStatus.Format();
224+
225+
public ICacheMetrics Metrics => lfu.Metrics.Value;
226+
227+
public StripedMpscBuffer<N> ReadBuffer => this.lfu.core.readBuffer as StripedMpscBuffer<N>;
228+
229+
public MpscBoundedBuffer<N> WriteBuffer => this.lfu.core.writeBuffer as MpscBoundedBuffer<N>;
230+
231+
public KeyValuePair<K, V>[] Items
232+
{
233+
get
234+
{
235+
var items = new KeyValuePair<K, V>[lfu.Count];
236+
237+
int index = 0;
238+
foreach (var kvp in lfu)
239+
{
240+
items[index++] = kvp;
241+
}
242+
return items;
243+
}
244+
}
245+
}
246+
}
247+
248+
}

0 commit comments

Comments
 (0)