1
1
using System . Collections . Concurrent ;
2
+ using System . Diagnostics . Metrics ;
2
3
3
4
namespace HotChocolate . Utilities ;
4
5
@@ -14,6 +15,7 @@ public sealed class Cache<TValue>
14
15
private readonly int _capacity ;
15
16
private readonly CacheEntry ? [ ] _ring ;
16
17
private readonly ConcurrentDictionary < string , CacheEntry > _map ;
18
+ private readonly CacheDiagnostics _diagnostics ;
17
19
18
20
// The clock hand is incremented atomically and is used to
19
21
// determine which cache entry to try to set a new entry into.
@@ -26,18 +28,25 @@ public sealed class Cache<TValue>
26
28
/// <param name="capacity">
27
29
/// The maximum number of items that can be stored in this cache.
28
30
/// </param>
31
+ /// <param name="diagnostics">
32
+ /// The diagnostics for the cache.
33
+ /// </param>
29
34
/// <exception cref="ArgumentOutOfRangeException">
30
35
/// Thrown when the capacity is less than 10.
31
36
/// </exception>
32
- public Cache ( int capacity = 256 )
37
+ public Cache ( int capacity = 256 , CacheDiagnostics ? diagnostics = null )
33
38
{
34
39
ArgumentOutOfRangeException . ThrowIfLessThan ( capacity , 10 ) ;
40
+
35
41
_capacity = capacity ;
36
42
_ring = new CacheEntry [ capacity ] ;
37
43
_map = new ConcurrentDictionary < string , CacheEntry > (
38
44
concurrencyLevel : Environment . ProcessorCount ,
39
45
capacity : _capacity ,
40
46
comparer : StringComparer . Ordinal ) ;
47
+ _diagnostics = diagnostics ?? NoOpCacheDiagnostics . Instance ;
48
+ _diagnostics . RegisterCapacityGauge ( ( ) => _capacity ) ;
49
+ _diagnostics . RegisterSizeGauge ( ( ) => _map . Count ) ;
41
50
}
42
51
43
52
/// <summary>
@@ -67,13 +76,17 @@ public bool TryGet(string key, out TValue? value)
67
76
{
68
77
if ( _map . TryGetValue ( key , out var entry ) )
69
78
{
70
- // we mark our entry as used by setting Accessed to 1
79
+ // We mark our entry as used by setting Accessed to 1
71
80
// this means the entry will be safe from the next eviction.
81
+ // Note: Volatile.Write is faster than Interlocked.Exchange, and we accept the
82
+ // tiny risk that an in‑flight eviction may still remove this entry.
72
83
Volatile . Write ( ref entry . Accessed , 1 ) ;
84
+ _diagnostics . Hit ( ) ;
73
85
value = entry . Value ;
74
86
return true ;
75
87
}
76
88
89
+ _diagnostics . Miss ( ) ;
77
90
value = default ;
78
91
return false ;
79
92
}
@@ -116,6 +129,22 @@ public TValue GetOrCreate<TState>(string key, Func<string, TState, TValue> creat
116
129
ArgumentNullException . ThrowIfNull ( key ) ;
117
130
ArgumentNullException . ThrowIfNull ( create ) ;
118
131
132
+ // We first check if the entry is already in the map.
133
+ // This is a fast lookup and will be used most of the time.
134
+ if ( _map . TryGetValue ( key , out var entry ) )
135
+ {
136
+ // We mark our entry as used by setting Accessed to 1
137
+ // this means the entry will be safe from the next eviction.
138
+ // Note: Volatile.Write is faster than Interlocked.Exchange, and we accept the
139
+ // tiny risk that an in‑flight eviction may still remove this entry.
140
+ Volatile . Write ( ref entry . Accessed , 1 ) ;
141
+ _diagnostics . Hit ( ) ;
142
+ return entry . Value ;
143
+ }
144
+
145
+ // If we have miss we do a GetOrAdd on the map to get at the end
146
+ // the winner in case of contention.
147
+ //
119
148
// The GetOrAdd of the ConcurrentDictionary is not atomic.
120
149
// It is possible that two threads will try to create the same entry
121
150
// at the same time.
@@ -130,17 +159,22 @@ public TValue GetOrCreate<TState>(string key, Func<string, TState, TValue> creat
130
159
// the overhead of a lock on the dictionary itself.
131
160
// The ConcurrentDictionary is vert efficient and does not
132
161
// lock the whole dictionary when adding an entry.
133
- var entry = _map . GetOrAdd (
162
+ var args = new CacheEntryCreateArgs < TState > ( state , create , this ) ;
163
+
164
+ entry = _map . GetOrAdd (
134
165
key ,
135
166
static ( k , arg ) =>
136
167
{
137
- var value = arg . create ( k , arg . state ) ;
138
- return arg . cache . InsertNew ( k , value ) ;
168
+ arg . Diagnostics . Miss ( ) ;
169
+ var value = arg . Create ( k , arg . State ) ;
170
+ return arg . Cache . InsertNew ( k , value ) ;
139
171
} ,
140
- ( state , create , cache : this ) ) ;
172
+ args ) ;
141
173
142
- // in the case we did not add a new entry but instead retrieved it
174
+ // In the case we did not add a new entry but instead retrieved it
143
175
// from the ConcurrentDictionary we need to mark it as recently accessed
176
+ // Note: Volatile.Write is faster than Interlocked.Exchange, and we accept the
177
+ // tiny risk that an in‑flight eviction may still remove this entry.
144
178
Volatile . Write ( ref entry . Accessed , 1 ) ;
145
179
return entry . Value ;
146
180
}
@@ -157,6 +191,17 @@ private CacheEntry InsertNew(string key, TValue value)
157
191
var idx = ( int ) ( handle % ( uint ) _capacity ) ;
158
192
var entry = _ring [ idx ] ;
159
193
194
+ if ( ++ spins > maxSpins && entry is not null )
195
+ {
196
+ var prev = Interlocked . CompareExchange ( ref _ring [ idx ] , newEntry , entry ) ;
197
+ if ( ReferenceEquals ( prev , entry ) )
198
+ {
199
+ _map . TryRemove ( prev . Key , out _ ) ;
200
+ _diagnostics . Evict ( ) ;
201
+ return newEntry ;
202
+ }
203
+ }
204
+
160
205
if ( entry is null )
161
206
{
162
207
// if the current cache slot is empty, we will try to insert
@@ -165,57 +210,35 @@ private CacheEntry InsertNew(string key, TValue value)
165
210
{
166
211
return newEntry ;
167
212
}
168
-
169
- continue ;
170
213
}
171
-
172
- if ( Interlocked . CompareExchange ( ref entry . Accessed , 0 , 1 ) == 0 )
214
+ else if ( Interlocked . CompareExchange ( ref entry . Accessed , 0 , 1 ) == 0 )
173
215
{
174
216
// If we found a slot that was not recently retrieved, we will try to
175
217
// replace it with our new entry. This will only succeed if no other thread
176
218
// was able to replace the entry in the meantime. This operation is atomic.
177
219
var prev = Interlocked . CompareExchange ( ref _ring [ idx ] , newEntry , entry ) ;
178
220
179
221
// If we were successful in replacing the entry, we will
180
- // then prev is the old entry and we need to remove it from the map.
222
+ // then prev is the old entry, and we need to remove it from the map.
181
223
// It might be that the old entry was retrieved in the meantime, and we
182
224
// accept this small window in which the map might have a dangling reference.
183
225
if ( ReferenceEquals ( prev , entry ) )
184
226
{
185
227
_map . TryRemove ( prev . Key , out _ ) ;
228
+ _diagnostics . Evict ( ) ;
186
229
return newEntry ;
187
230
}
188
231
}
189
-
190
- if ( ++ spins > maxSpins )
191
- {
192
- entry = _ring [ idx ] ! ; // re‑read reference
193
-
194
- var oldKey = entry . Key ;
195
-
196
- // atomic swap
197
- Interlocked . Exchange ( ref _ring [ idx ] , newEntry ) ;
198
-
199
- _map . TryRemove ( oldKey , out _ ) ;
200
- return newEntry ;
201
- }
202
232
}
203
233
}
204
234
205
235
/// <summary>
206
236
/// Clears all entries from the cache.
237
+ /// The clear might leave the cache in a dirty state.
238
+ /// This is acceptable as we are not using a clear in production.
239
+ /// It's more a helper for testing.
207
240
/// </summary>
208
- public void Clear ( )
209
- {
210
- _map . Clear ( ) ;
211
-
212
- for ( var i = 0 ; i < _ring . Length ; i ++ )
213
- {
214
- _ring [ i ] = null ;
215
- }
216
-
217
- Interlocked . Exchange ( ref _hand , 0 ) ;
218
- }
241
+ public void Clear ( ) => _map . Clear ( ) ;
219
242
220
243
/// <summary>
221
244
/// Returns all keys in the cache. This method is for testing only.
@@ -234,12 +257,46 @@ internal IEnumerable<string> GetKeys()
234
257
235
258
private sealed class CacheEntry ( string key , TValue value )
236
259
{
260
+ /// <summary>
261
+ /// The key of the entry.
262
+ /// </summary>
237
263
public readonly string Key = key ;
238
264
265
+ /// <summary>
266
+ /// The value of the entry.
267
+ /// </summary>
239
268
public readonly TValue Value = value ;
240
269
241
- // 0 = not accessed recently
242
- // 1 = accessed recently
270
+ /// <summary>
271
+ /// 0 = not accessed recently
272
+ /// 1 = accessed recently
273
+ /// </summary>
243
274
public int Accessed = 1 ;
244
275
}
276
+
277
+ private readonly struct CacheEntryCreateArgs < TState > (
278
+ TState state ,
279
+ Func < string , TState , TValue > create ,
280
+ Cache < TValue > cache )
281
+ {
282
+ /// <summary>
283
+ /// The state that is needed to create the value to cache.
284
+ /// </summary>
285
+ public readonly TState State = state ;
286
+
287
+ /// <summary>
288
+ /// The factory to create the value to cache.
289
+ /// </summary>
290
+ public readonly Func < string , TState , TValue > Create = create ;
291
+
292
+ /// <summary>
293
+ /// The cache instance.
294
+ /// </summary>
295
+ public readonly Cache < TValue > Cache = cache ;
296
+
297
+ /// <summary>
298
+ /// The diagnostics for the cache.
299
+ /// </summary>
300
+ public readonly CacheDiagnostics Diagnostics = cache . _diagnostics ;
301
+ }
245
302
}
0 commit comments