@@ -7,6 +7,7 @@ open System.Collections.Concurrent
7
7
open System.Threading
8
8
open System.Diagnostics
9
9
open System.Diagnostics .Metrics
10
+ open System.Collections .Immutable
10
11
11
12
[<Struct; RequireQualifiedAccess; NoComparison; NoEquality>]
12
13
type CacheOptions =
@@ -47,112 +48,80 @@ type CachedEntity<'Key, 'Value> =
47
48
entity.node <- LinkedListNode( entity)
48
49
entity
49
50
50
- member this.ReUse ( key , value ) =
51
- this.key <- key
52
- this.value <- value
53
- this
54
-
55
51
override this.ToString () = $" {this.Key}"
56
52
57
53
// Currently the Cache itself exposes Metrics.Counters that count raw cache events: hits, misses, evictions etc.
58
54
// This class observes those counters and keeps a snapshot of readings. For now this is used only to print cache stats in debug mode.
59
55
// TODO: We could add some System.Diagnostics.Metrics.Gauge instruments to this class, to get computed stats also exposed as metrics.
60
- type CacheMetrics ( cacheId ) =
56
+ type CacheMetrics ( cacheId : string ) =
61
57
static let meter = new Meter( " FSharp.Compiler.Cache" )
62
-
63
58
static let observedCaches = ConcurrentDictionary< string, CacheMetrics>()
64
59
65
- let readings = ConcurrentDictionary< string, int64 ref>()
60
+ let created = meter.CreateCounter< int64>( " created" , " count" , cacheId)
61
+ let hits = meter.CreateCounter< int64>( " hits" , " count" , cacheId)
62
+ let misses = meter.CreateCounter< int64>( " misses" , " count" , cacheId)
63
+ let evictions = meter.CreateCounter< int64>( " evictions" , " count" , cacheId)
64
+ let evictionFails = meter.CreateCounter< int64>( " eviction-fails" , " count" , cacheId)
65
+ let allCouinters = [ created; hits; misses; evictions; evictionFails ]
66
66
67
- let listener = new MeterListener()
67
+ let totals =
68
+ let builder = ImmutableDictionary.CreateBuilder< Instrument, int64 ref>()
68
69
69
- do
70
- listener.InstrumentPublished <-
71
- fun i l ->
72
- if i.Meter = meter && i.Description = cacheId then
73
- l.EnableMeasurementEvents( i)
70
+ for counter in allCouinters do
71
+ builder.Add( counter, ref 0 L)
74
72
75
- listener.SetMeasurementEventCallback< int64>( fun k v _ _ -> Interlocked.Add( readings.GetOrAdd( k.Name, ref 0 L), v) |> ignore)
76
- listener.Start()
73
+ builder.ToImmutable()
77
74
78
- member this.Dispose () = listener.Dispose()
75
+ let incr key v =
76
+ Interlocked.Add( totals[ key], v) |> ignore
79
77
80
- member val CacheId = cacheId
78
+ let total key = totals [ key ]. Value
81
79
82
- static member val Meter = meter
80
+ let mutable ratio = Double.NaN
83
81
84
- member val RecentStats = " -" with get, set
85
-
86
- member this.TryUpdateStats ( clearCounts ) =
87
- let ratio =
88
- try
89
- float readings[ " hits" ]. Value
90
- / float ( readings[ " hits" ]. Value + readings[ " misses" ]. Value)
91
- * 100.0
92
- with _ ->
93
- Double.NaN
94
-
95
- let stats =
96
- [
97
- for name in readings.Keys do
98
- let v = readings[ name]. Value
99
-
100
- if v > 0 then
101
- $" {name}: {v}"
102
- ]
103
- |> String.concat " , "
104
- |> sprintf " %s | hit ratio: %s %s " this.CacheId ( if Double.IsNaN( ratio) then " -" else $" %.1f {ratio}%%" )
105
-
106
- if clearCounts then
107
- for r in readings.Values do
108
- Interlocked.Exchange( r, 0 L) |> ignore
109
-
110
- if stats <> this.RecentStats then
111
- this.RecentStats <- stats
112
- true
113
- else
114
- false
82
+ let updateRatio () =
83
+ ratio <- float ( total hits) / float ( total hits + total misses)
115
84
116
- // TODO: Should return a Map, not a string
117
- static member GetStats ( cacheId ) =
118
- observedCaches[ cacheId]. TryUpdateStats( false ) |> ignore
119
- observedCaches[ cacheId]. RecentStats
85
+ let listener = new MeterListener()
120
86
121
- static member GetStatsUpdateForAllCaches ( clearCounts ) =
122
- [
123
- for i in observedCaches.Values do
124
- if i.TryUpdateStats( clearCounts) then
125
- i.RecentStats
126
- ]
127
- |> String.concat " \n "
87
+ let startListening () =
88
+ for i in allCouinters do
89
+ listener.EnableMeasurementEvents i
128
90
129
- static member AddInstrumentation ( cacheId ) =
130
- if observedCaches.ContainsKey cacheId then
131
- invalidArg " cacheId" $" cache with name {cacheId} already exists"
91
+ listener.SetMeasurementEventCallback( fun instrument v _ _ ->
92
+ incr instrument v
132
93
133
- observedCaches[ cacheId] <- new CacheMetrics( cacheId)
94
+ if instrument = hits || instrument = misses then
95
+ updateRatio ())
134
96
135
- static member RemoveInstrumentation ( cacheId ) =
136
- observedCaches[ cacheId]. Dispose()
137
- observedCaches.TryRemove( cacheId) |> ignore
97
+ listener.Start()
138
98
139
- // Creates and after reclaiming holds entities for reuse.
140
- // More than totalCapacity can be created, but it will hold for reuse at most totalCapacity.
141
- type EntityPool < 'Key , 'Value >( totalCapacity , cacheId ) =
142
- let pool = ConcurrentBag< CachedEntity< 'Key, 'Value>>()
99
+ member val Created = created
100
+ member val Hits = hits
101
+ member val Misses = misses
102
+ member val Evictions = evictions
103
+ member val EvictionFails = evictionFails
143
104
144
- let created = CacheMetrics.Meter.CreateCounter< int64>( " created" , " count" , cacheId)
105
+ member this.ObserveMetrics () =
106
+ observedCaches[ cacheId] <- this
107
+ startListening ()
145
108
146
- member _.Acquire ( key , value ) =
147
- match pool.TryTake () with
148
- | true , entity -> entity.ReUse ( key , value )
149
- | _ ->
150
- created.Add 1 L
151
- CachedEntity.Create ( key , value )
109
+ member this.Dispose ( ) =
110
+ observedCaches.TryRemove cacheId |> ignore
111
+ listener.Dispose ( )
112
+
113
+ member _.GetInstanceTotals () =
114
+ [ for k in totals.Keys -> k.Name , total k ] |> Map.ofList
152
115
153
- member _.Reclaim ( entity : CachedEntity < 'Key , 'Value >) =
154
- if pool.Count < totalCapacity then
155
- pool.Add( entity)
116
+ member _.GetInstanceStats () = [ " hit-ratio" , ratio ] |> Map.ofList
117
+
118
+ static member val Meter = meter
119
+
120
+ static member GetTotals ( cacheId ) =
121
+ observedCaches[ cacheId]. GetInstanceTotals()
122
+
123
+ static member GetStats ( cacheId ) =
124
+ observedCaches[ cacheId]. GetInstanceStats()
156
125
157
126
module Cache =
158
127
// During testing a lot of compilations are started in app domains and subprocesses.
@@ -176,25 +145,13 @@ type EvictionQueueMessage<'Key, 'Value> =
176
145
177
146
[<Sealed; NoComparison; NoEquality>]
178
147
[<DebuggerDisplay( " {GetStats()}" ) >]
179
- type Cache < 'Key , 'Value when 'Key: not null and 'Key: equality > internal ( totalCapacity , headroom , ? name , ? observeMetrics ) =
180
-
181
- let instanceId = defaultArg name ( Guid.NewGuid() .ToString())
148
+ type Cache < 'Key , 'Value when 'Key: not null and 'Key: equality > internal ( totalCapacity , headroom , name , listen ) =
182
149
183
- let observeMetrics = defaultArg observeMetrics false
150
+ let metrics = new CacheMetrics ( name )
184
151
185
152
do
186
- if observeMetrics then
187
- CacheMetrics.AddInstrumentation instanceId
188
-
189
- let meter = CacheMetrics.Meter
190
- let hits = meter.CreateCounter< int64>( " hits" , " count" , instanceId)
191
- let misses = meter.CreateCounter< int64>( " misses" , " count" , instanceId)
192
- let evictions = meter.CreateCounter< int64>( " evictions" , " count" , instanceId)
193
-
194
- let evictionFails =
195
- meter.CreateCounter< int64>( " eviction-fails" , " count" , instanceId)
196
-
197
- let pool = EntityPool< 'Key, 'Value>( totalCapacity, instanceId)
153
+ if listen then
154
+ metrics.ObserveMetrics()
198
155
199
156
let store =
200
157
ConcurrentDictionary< 'Key, CachedEntity< 'Key, 'Value>>( Environment.ProcessorCount, totalCapacity)
@@ -205,6 +162,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
205
162
let capacity = totalCapacity - headroom
206
163
207
164
let evicted = Event<_>()
165
+ let evictionFailed = Event<_>()
208
166
209
167
let cts = new CancellationTokenSource()
210
168
@@ -222,12 +180,14 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
222
180
let first = nonNull evictionQueue.First
223
181
224
182
match store.TryRemove( first.Value.Key) with
225
- | true , removed ->
183
+ | true , _ ->
226
184
evictionQueue.Remove( first)
227
- pool.Reclaim( removed)
228
- evictions.Add 1 L
185
+ metrics.Evictions.Add 1 L
229
186
evicted.Trigger()
230
- | _ -> evictionFails.Add 1 L
187
+ | _ ->
188
+ // This should not be possible to happen, but if it does, we want to know.
189
+ metrics.EvictionFails.Add 1 L
190
+ evictionFailed.Trigger()
231
191
232
192
// Store updates are not synchronized. It is possible the entity is no longer in the queue.
233
193
| EvictionQueueMessage.Update entity when isNull entity.Node.List -> ()
@@ -245,30 +205,27 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
245
205
)
246
206
247
207
member val Evicted = evicted.Publish
248
-
249
- member val Name = instanceId
208
+ member val EvictionFailed = evictionFailed.Publish
250
209
251
210
member _.TryGetValue ( key : 'Key , value : outref < 'Value >) =
252
211
match store.TryGetValue( key) with
253
212
| true , entity ->
254
- hits .Add 1 L
213
+ metrics.Hits .Add 1 L
255
214
evictionProcessor.Post( EvictionQueueMessage.Update entity)
256
215
value <- entity.Value
257
216
true
258
217
| _ ->
259
- misses .Add 1 L
218
+ metrics.Misses .Add 1 L
260
219
value <- Unchecked.defaultof< 'Value>
261
220
false
262
221
263
222
member _.TryAdd ( key : 'Key , value : 'Value ) =
264
- let entity = pool.Acquire ( key, value)
223
+ let entity = CachedEntity.Create ( key, value)
265
224
266
225
let added = store.TryAdd( key, entity)
267
226
268
227
if added then
269
228
evictionProcessor.Post( EvictionQueueMessage.Add entity)
270
- else
271
- pool.Reclaim( entity)
272
229
273
230
added
274
231
@@ -278,14 +235,10 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
278
235
cts.Dispose()
279
236
evictionProcessor.Dispose()
280
237
store.Clear()
281
-
282
- if observeMetrics then
283
- CacheMetrics.RemoveInstrumentation instanceId
238
+ metrics.Dispose()
284
239
285
240
member this.Dispose () = ( this :> IDisposable) .Dispose()
286
241
287
- member this.GetStats () = CacheMetrics.GetStats( this.Name)
288
-
289
242
static member Create < 'Key , 'Value >( options : CacheOptions , ? name , ? observeMetrics ) =
290
243
if options.TotalCapacity < 0 then
291
244
invalidArg " Capacity" " Capacity must be positive"
@@ -298,7 +251,9 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
298
251
let headroom =
299
252
int ( float options.TotalCapacity * float options.HeadroomPercentage / 100.0 )
300
253
301
- let cache =
302
- new Cache<_, _>( totalCapacity, headroom, ?name = name, ?observeMetrics = observeMetrics)
254
+ let name = defaultArg name ( Guid.NewGuid() .ToString())
255
+ let observeMetrics = defaultArg observeMetrics false
256
+
257
+ let cache = new Cache<_, _>( totalCapacity, headroom, name, observeMetrics)
303
258
304
259
cache
0 commit comments