@@ -7,6 +7,7 @@ open System.Collections.Concurrent
77open System.Threading
88open System.Diagnostics
99open System.Diagnostics .Metrics
10+ open System.Collections .Immutable
1011
1112[<Struct; RequireQualifiedAccess; NoComparison; NoEquality>]
1213type CacheOptions =
@@ -47,112 +48,80 @@ type CachedEntity<'Key, 'Value> =
4748 entity.node <- LinkedListNode( entity)
4849 entity
4950
50- member this.ReUse ( key , value ) =
51- this.key <- key
52- this.value <- value
53- this
54-
5551 override this.ToString () = $" {this.Key}"
5652
5753// Currently the Cache itself exposes Metrics.Counters that count raw cache events: hits, misses, evictions etc.
5854// This class observes those counters and keeps a snapshot of readings. For now this is used only to print cache stats in debug mode.
5955// 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 ) =
6157 static let meter = new Meter( " FSharp.Compiler.Cache" )
62-
6358 static let observedCaches = ConcurrentDictionary< string, CacheMetrics>()
6459
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 ]
6666
67- let listener = new MeterListener()
67+ let totals =
68+ let builder = ImmutableDictionary.CreateBuilder< Instrument, int64 ref>()
6869
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)
7472
75- listener.SetMeasurementEventCallback< int64>( fun k v _ _ -> Interlocked.Add( readings.GetOrAdd( k.Name, ref 0 L), v) |> ignore)
76- listener.Start()
73+ builder.ToImmutable()
7774
78- member this.Dispose () = listener.Dispose()
75+ let incr key v =
76+ Interlocked.Add( totals[ key], v) |> ignore
7977
80- member val CacheId = cacheId
78+ let total key = totals [ key ]. Value
8179
82- static member val Meter = meter
80+ let mutable ratio = Double.NaN
8381
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)
11584
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()
12086
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
12890
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
13293
133- observedCaches[ cacheId] <- new CacheMetrics( cacheId)
94+ if instrument = hits || instrument = misses then
95+ updateRatio ())
13496
135- static member RemoveInstrumentation ( cacheId ) =
136- observedCaches[ cacheId]. Dispose()
137- observedCaches.TryRemove( cacheId) |> ignore
97+ listener.Start()
13898
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
143104
144- let created = CacheMetrics.Meter.CreateCounter< int64>( " created" , " count" , cacheId)
105+ member this.ObserveMetrics () =
106+ observedCaches[ cacheId] <- this
107+ startListening ()
145108
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
152115
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()
156125
157126module Cache =
158127 // During testing a lot of compilations are started in app domains and subprocesses.
@@ -176,25 +145,13 @@ type EvictionQueueMessage<'Key, 'Value> =
176145
177146[<Sealed; NoComparison; NoEquality>]
178147[<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 ) =
182149
183- let observeMetrics = defaultArg observeMetrics false
150+ let metrics = new CacheMetrics ( name )
184151
185152 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()
198155
199156 let store =
200157 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
205162 let capacity = totalCapacity - headroom
206163
207164 let evicted = Event<_>()
165+ let evictionFailed = Event<_>()
208166
209167 let cts = new CancellationTokenSource()
210168
@@ -222,12 +180,14 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
222180 let first = nonNull evictionQueue.First
223181
224182 match store.TryRemove( first.Value.Key) with
225- | true , removed ->
183+ | true , _ ->
226184 evictionQueue.Remove( first)
227- pool.Reclaim( removed)
228- evictions.Add 1 L
185+ metrics.Evictions.Add 1 L
229186 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()
231191
232192 // Store updates are not synchronized. It is possible the entity is no longer in the queue.
233193 | 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
245205 )
246206
247207 member val Evicted = evicted.Publish
248-
249- member val Name = instanceId
208+ member val EvictionFailed = evictionFailed.Publish
250209
251210 member _.TryGetValue ( key : 'Key , value : outref < 'Value >) =
252211 match store.TryGetValue( key) with
253212 | true , entity ->
254- hits .Add 1 L
213+ metrics.Hits .Add 1 L
255214 evictionProcessor.Post( EvictionQueueMessage.Update entity)
256215 value <- entity.Value
257216 true
258217 | _ ->
259- misses .Add 1 L
218+ metrics.Misses .Add 1 L
260219 value <- Unchecked.defaultof< 'Value>
261220 false
262221
263222 member _.TryAdd ( key : 'Key , value : 'Value ) =
264- let entity = pool.Acquire ( key, value)
223+ let entity = CachedEntity.Create ( key, value)
265224
266225 let added = store.TryAdd( key, entity)
267226
268227 if added then
269228 evictionProcessor.Post( EvictionQueueMessage.Add entity)
270- else
271- pool.Reclaim( entity)
272229
273230 added
274231
@@ -278,14 +235,10 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
278235 cts.Dispose()
279236 evictionProcessor.Dispose()
280237 store.Clear()
281-
282- if observeMetrics then
283- CacheMetrics.RemoveInstrumentation instanceId
238+ metrics.Dispose()
284239
285240 member this.Dispose () = ( this :> IDisposable) .Dispose()
286241
287- member this.GetStats () = CacheMetrics.GetStats( this.Name)
288-
289242 static member Create < 'Key , 'Value >( options : CacheOptions , ? name , ? observeMetrics ) =
290243 if options.TotalCapacity < 0 then
291244 invalidArg " Capacity" " Capacity must be positive"
@@ -298,7 +251,9 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
298251 let headroom =
299252 int ( float options.TotalCapacity * float options.HeadroomPercentage / 100.0 )
300253
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)
303258
304259 cache
0 commit comments