@@ -9,6 +9,7 @@ open System.Threading
9
9
open System.Diagnostics
10
10
open System.Diagnostics .Metrics
11
11
open Internal.Utilities .Library
12
+ open System.Runtime .CompilerServices
12
13
13
14
[<Struct; RequireQualifiedAccess; NoComparison; NoEquality>]
14
15
type internal CachingStrategy =
@@ -71,25 +72,27 @@ type internal CachedEntity<'Key, 'Value> =
71
72
override this.ToString () = $" {this.Key}"
72
73
73
74
type internal IEvictionQueue < 'Key , 'Value > =
74
- interface
75
- abstract member Acquire: 'Key * 'Value -> CachedEntity < 'Key , 'Value >
76
- abstract member Add: CachedEntity < 'Key , 'Value > * CachingStrategy -> unit
77
- abstract member Update: CachedEntity < 'Key , 'Value > -> unit
78
- abstract member GetKeysToEvict: int -> 'Key []
79
- abstract member Remove: CachedEntity < 'Key , 'Value > -> unit
80
- end
75
+ abstract member Acquire: 'Key * 'Value -> CachedEntity < 'Key , 'Value >
76
+ abstract member Add: CachedEntity < 'Key , 'Value > * CachingStrategy -> unit
77
+ abstract member Update: CachedEntity < 'Key , 'Value > -> unit
78
+ abstract member GetKeysToEvict: int -> 'Key []
79
+ abstract member Remove: CachedEntity < 'Key , 'Value > -> unit
81
80
82
- type internal EvictionQueue < 'Key , 'Value >( strategy : CachingStrategy ) =
81
+ type internal EvictionQueue < 'Key , 'Value >( strategy : CachingStrategy , maximumCapacity , overCapacity : Event < _ > ) =
83
82
84
83
let list = LinkedList< CachedEntity< 'Key, 'Value>>()
85
84
let pool = ConcurrentBag< CachedEntity< 'Key, 'Value>>()
85
+ let mutable created = 0
86
86
87
87
interface IEvictionQueue< 'Key, 'Value> with
88
88
89
89
member _.Acquire ( key , value ) =
90
90
match pool.TryTake() with
91
91
| true , entity -> entity.ReUse( key, value)
92
- | _ -> CachedEntity( key, value) .WithNode()
92
+ | _ ->
93
+ if Interlocked.Increment & created > maximumCapacity then
94
+ overCapacity.Trigger()
95
+ CachedEntity( key, value) .WithNode()
93
96
94
97
member _.Add ( entity : CachedEntity < 'Key , 'Value >, strategy ) =
95
98
lock list
@@ -137,7 +140,7 @@ type internal EvictionQueue<'Key, 'Value>(strategy: CachingStrategy) =
137
140
member this.Remove ( entity : CachedEntity < _ , _ >) =
138
141
lock list <| fun () -> list.Remove( entity.Node)
139
142
// Return to the pool for reuse.
140
- pool.Add( entity)
143
+ if pool.Count < maximumCapacity then pool.Add( entity)
141
144
142
145
member _.Count = list.Count
143
146
@@ -153,23 +156,40 @@ type internal EvictionQueue<'Key, 'Value>(strategy: CachingStrategy) =
153
156
member _.Remove ( _ ) = ()
154
157
}
155
158
159
+ type ICacheEvents =
160
+ [<CLIEvent>]
161
+ abstract member CacheHit: IEvent < unit >
162
+
163
+ [<CLIEvent>]
164
+ abstract member CacheMiss: IEvent < unit >
165
+
166
+ [<CLIEvent>]
167
+ abstract member Eviction: IEvent < unit >
168
+
169
+ [<CLIEvent>]
170
+ abstract member EvictionFail: IEvent < unit >
171
+
172
+ [<CLIEvent>]
173
+ abstract member OverCapacity: IEvent < unit >
174
+
156
175
[<Sealed; NoComparison; NoEquality>]
157
176
[<DebuggerDisplay( " {GetStats()}" ) >]
158
177
type internal Cache < 'Key , 'Value when 'Key: not null and 'Key: equality >
159
178
internal ( options: CacheOptions, capacity, cts: CancellationTokenSource) =
160
179
161
- let cacheHit = Event<_ * _>()
162
- let cacheMiss = Event<_>()
163
- let eviction = Event<_>()
164
- let evictionFail = Event<_>()
180
+ let cacheHit = Event< unit>()
181
+ let cacheMiss = Event< unit>()
182
+ let eviction = Event< unit>()
183
+ let evictionFail = Event< unit>()
184
+ let overCapacity = Event< unit>()
165
185
166
186
let store =
167
187
ConcurrentDictionary< 'Key, CachedEntity< 'Key, 'Value>>( options.LevelOfConcurrency, capacity)
168
188
169
189
let evictionQueue : IEvictionQueue < 'Key , 'Value > =
170
190
match options.EvictionMethod with
171
191
| EvictionMethod.NoEviction -> EvictionQueue.NoEviction
172
- | _ -> EvictionQueue( options.Strategy)
192
+ | _ -> EvictionQueue( options.Strategy, options.MaximumCapacity , overCapacity )
173
193
174
194
let tryEvictItems () =
175
195
let count =
@@ -182,10 +202,10 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
182
202
match store.TryRemove( key) with
183
203
| true , removed ->
184
204
evictionQueue.Remove( removed)
185
- eviction.Trigger( key )
205
+ eviction.Trigger()
186
206
| _ ->
187
207
failwith " eviction fail"
188
- evictionFail.Trigger( key )
208
+ evictionFail.Trigger()
189
209
190
210
let rec backgroundEviction () =
191
211
async {
@@ -212,12 +232,12 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
212
232
member _.TryGetValue ( key : 'Key , value : outref < 'Value >) =
213
233
match store.TryGetValue( key) with
214
234
| true , cachedEntity ->
215
- cacheHit.Trigger( key , cachedEntity.Value )
235
+ cacheHit.Trigger()
216
236
evictionQueue.Update( cachedEntity)
217
237
value <- cachedEntity.Value
218
238
true
219
239
| _ ->
220
- cacheMiss.Trigger( key )
240
+ cacheMiss.Trigger()
221
241
value <- Unchecked.defaultof< 'Value>
222
242
false
223
243
@@ -248,83 +268,116 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
248
268
249
269
evictionQueue.Add( entity, options.Strategy)
250
270
251
- [<CLIEvent>]
252
- member val CacheHit = cacheHit.Publish
271
+ interface ICacheEvents with
253
272
254
- [<CLIEvent>]
255
- member val CacheMiss = cacheMiss .Publish
273
+ [<CLIEvent>]
274
+ member val CacheHit = cacheHit .Publish
256
275
257
- [<CLIEvent>]
258
- member val Eviction = eviction .Publish
276
+ [<CLIEvent>]
277
+ member val CacheMiss = cacheMiss .Publish
259
278
260
- [<CLIEvent>]
261
- member val EvictionFail = evictionFail.Publish
279
+ [<CLIEvent>]
280
+ member val Eviction = eviction.Publish
281
+
282
+ [<CLIEvent>]
283
+ member val EvictionFail = evictionFail.Publish
284
+
285
+ [<CLIEvent>]
286
+ member val OverCapacity = overCapacity.Publish
262
287
263
288
interface IDisposable with
264
289
member this.Dispose () =
265
290
cts.Cancel()
291
+ CacheInstrumentation.RemoveInstrumentation( this)
266
292
GC.SuppressFinalize( this)
267
293
268
294
member this.Dispose () = ( this :> IDisposable) .Dispose()
269
295
270
296
override this.Finalize () : unit = this.Dispose()
271
297
272
- module internal Cache =
273
- let mutable cacheId = 0
298
+ static member Create < 'Key , 'Value when 'Key : not null and 'Key : equality > ( options : CacheOptions ) =
299
+ // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary.
300
+ let capacity =
301
+ options.MaximumCapacity
302
+ + ( options.MaximumCapacity * options.PercentageToEvict / 100 )
274
303
275
- [<Literal>]
276
- let MeterName = " FSharp.Compiler.Caches"
304
+ let cts = new CancellationTokenSource()
305
+ let cache = new Cache< 'Key, 'Value>( options, capacity, cts)
306
+ CacheInstrumentation.AddInstrumentation cache |> ignore
307
+ cache
308
+
309
+ member this.GetStats () =
310
+ CacheInstrumentation.GetStats( this)
277
311
278
- let addInstrumentation ( cache : Cache < _ , _ >) =
279
- let meter = new Meter( MeterName)
280
- let cacheId = Interlocked.Increment & cacheId
312
+ and internal CacheInstrumentation ( cache : ICacheEvents ) =
313
+ static let mutable cacheId = 0
281
314
282
- let mutable evictions = 0 L
283
- let mutable fails = 0 L
284
- let mutable hits = 0 L
285
- let mutable misses = 0 L
315
+ static let instrumentedCaches = ConcurrentDictionary< ICacheEvents, CacheInstrumentation>()
286
316
287
- let mutable allEvictions = 0 L
288
- let mutable allFails = 0 L
289
- let mutable allHits = 0 L
290
- let mutable allMisses = 0 L
317
+ static let meter = new Meter( nameof CacheInstrumentation)
318
+ let hits = meter.CreateCounter< int64>( " hits" )
319
+ let misses = meter.CreateCounter< int64>( " misses" )
320
+ let evictions = meter.CreateCounter< int64>( " evictions" )
321
+ let evictionFails = meter.CreateCounter< int64>( " eviction-fails" )
322
+ let overCapacity = meter.CreateCounter< int64>( " over-capacity" )
291
323
292
- cache.CacheHit
293
- |> Event.add ( fun _ ->
294
- Interlocked.Increment & hits |> ignore
295
- Interlocked.Increment & allHits |> ignore)
324
+ do
325
+ cache.CacheHit.Add <| fun _ -> hits.Add( 1 L)
326
+ cache.CacheMiss.Add <| fun _ -> misses.Add( 1 L)
327
+ cache.Eviction.Add <| fun _ -> evictions.Add( 1 L)
328
+ cache.EvictionFail.Add <| fun _ -> evictionFails.Add( 1 L)
329
+ cache.OverCapacity.Add <| fun _ -> overCapacity.Add( 1 L)
296
330
297
- cache.CacheMiss
298
- |> Event.add ( fun _ ->
299
- Interlocked.Increment & misses |> ignore
300
- Interlocked.Increment & allMisses |> ignore)
331
+ let current = ConcurrentDictionary< Instrument, int64 ref>()
301
332
302
- cache.Eviction
303
- |> Event.add ( fun _ ->
304
- Interlocked.Increment & evictions |> ignore
305
- Interlocked.Increment & allEvictions |> ignore )
333
+ #if DEBUG
334
+ let listener =
335
+ new MeterListener (
336
+ InstrumentPublished = fun i l -> if i.Meter = meter then l.EnableMeasurementEvents ( i ) )
306
337
307
- cache.EvictionFail
308
- |> Event.add ( fun _ ->
309
- Interlocked.Increment & fails |> ignore
310
- Interlocked.Increment & allFails |> ignore)
338
+ do
339
+ listener.SetMeasurementEventCallback< int64>( fun k v _ _ ->
340
+ Interlocked.Add( current.GetOrAdd( k, ref 0 L), v) |> ignore)
341
+ listener.Start()
342
+ #endif
311
343
312
- let hitRatio () =
313
- let misses = Interlocked.Exchange(& misses, 0 L)
314
- let hits = Interlocked.Exchange(& hits, 0 L)
315
- float hits / float ( hits + misses)
344
+ member val CacheId = $" cache-{Interlocked.Increment(&cacheId)}"
316
345
317
- meter.CreateObservableGauge( $" hit ratio {cacheId}" , hitRatio) |> ignore
346
+ member val RecentStats = " -" with get, set
347
+
348
+ member this.TryUpdateStats ( clearCounts ) =
349
+ let stats =
350
+ try
351
+ let ratio = float current[ hits]. Value / float ( current[ hits]. Value + current[ misses]. Value)
352
+ [ for i in current.Keys -> $" {i.Name}: {current[i].Value}" ]
353
+ |> String.concat " , "
354
+ |> sprintf " %s | ratio: %.2f %s " this.CacheId ratio
355
+ with _ -> " !"
356
+
357
+ if clearCounts then
358
+ for r in current.Values do Interlocked.Exchange( r, 0 L) |> ignore
359
+
360
+ if stats <> this.RecentStats then
361
+ this.RecentStats <- stats
362
+ true
363
+ else
364
+ false
365
+
366
+ static member GetStats ( cache : ICacheEvents ) =
367
+ instrumentedCaches[ cache]. TryUpdateStats( false ) |> ignore
368
+ instrumentedCaches[ cache]. RecentStats
369
+
370
+ static member GetStatsUpdateForAllCaches ( clearCounts ) =
371
+ [
372
+ for i in instrumentedCaches.Values do
373
+ if i.TryUpdateStats( clearCounts) then
374
+ i.RecentStats
375
+ ]
376
+
377
+ static member AddInstrumentation ( cache : ICacheEvents ) =
378
+ instrumentedCaches[ cache] <- CacheInstrumentation( cache)
379
+
380
+ static member RemoveInstrumentation ( cache : ICacheEvents ) =
381
+ instrumentedCaches.TryRemove( cache) |> ignore
318
382
319
- let Create < 'Key , 'Value when 'Key : not null and 'Key : equality > ( options : CacheOptions ) =
320
- // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary.
321
- let capacity =
322
- options.MaximumCapacity
323
- + ( options.MaximumCapacity * options.PercentageToEvict / 100 )
324
383
325
- let cts = new CancellationTokenSource()
326
- let cache = new Cache< 'Key, 'Value>( options, capacity, cts)
327
- #if DEBUG
328
- addInstrumentation cache
329
- #endif
330
- cache
0 commit comments