Skip to content

Commit 56eab5a

Browse files
committed
metrics
1 parent 245460f commit 56eab5a

File tree

3 files changed

+131
-107
lines changed

3 files changed

+131
-107
lines changed

src/Compiler/Checking/import.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ let createTypeSubsumptionCache (g: TcGlobals) =
105105
MaximumCapacity = 32_000 } // 8192 }
106106
Cache.Create<TTypeCacheKey, bool>(options)
107107

108-
let typeSubsumptionCaches = ConditionalWeakTable<TcGlobals, Cache<_,_>>()
108+
let typeSubsumptionCaches = ConditionalWeakTable<TcGlobals, Cache<TTypeCacheKey, bool>>()
109109

110110
//-------------------------------------------------------------------------
111111
// Import an IL types as F# types.

src/Compiler/Utilities/Caches.fs

Lines changed: 127 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ open System.Threading
99
open System.Diagnostics
1010
open System.Diagnostics.Metrics
1111
open Internal.Utilities.Library
12+
open System.Runtime.CompilerServices
1213

1314
[<Struct; RequireQualifiedAccess; NoComparison; NoEquality>]
1415
type internal CachingStrategy =
@@ -71,25 +72,27 @@ type internal CachedEntity<'Key, 'Value> =
7172
override this.ToString() = $"{this.Key}"
7273

7374
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
8180

82-
type internal EvictionQueue<'Key, 'Value>(strategy: CachingStrategy) =
81+
type internal EvictionQueue<'Key, 'Value>(strategy: CachingStrategy, maximumCapacity, overCapacity: Event<_>) =
8382

8483
let list = LinkedList<CachedEntity<'Key, 'Value>>()
8584
let pool = ConcurrentBag<CachedEntity<'Key, 'Value>>()
85+
let mutable created = 0
8686

8787
interface IEvictionQueue<'Key, 'Value> with
8888

8989
member _.Acquire(key, value) =
9090
match pool.TryTake() with
9191
| 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()
9396

9497
member _.Add(entity: CachedEntity<'Key, 'Value>, strategy) =
9598
lock list
@@ -137,7 +140,7 @@ type internal EvictionQueue<'Key, 'Value>(strategy: CachingStrategy) =
137140
member this.Remove(entity: CachedEntity<_, _>) =
138141
lock list <| fun () -> list.Remove(entity.Node)
139142
// Return to the pool for reuse.
140-
pool.Add(entity)
143+
if pool.Count < maximumCapacity then pool.Add(entity)
141144

142145
member _.Count = list.Count
143146

@@ -153,23 +156,40 @@ type internal EvictionQueue<'Key, 'Value>(strategy: CachingStrategy) =
153156
member _.Remove(_) = ()
154157
}
155158

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+
156175
[<Sealed; NoComparison; NoEquality>]
157176
[<DebuggerDisplay("{GetStats()}")>]
158177
type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
159178
internal (options: CacheOptions, capacity, cts: CancellationTokenSource) =
160179

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>()
165185

166186
let store =
167187
ConcurrentDictionary<'Key, CachedEntity<'Key, 'Value>>(options.LevelOfConcurrency, capacity)
168188

169189
let evictionQueue: IEvictionQueue<'Key, 'Value> =
170190
match options.EvictionMethod with
171191
| EvictionMethod.NoEviction -> EvictionQueue.NoEviction
172-
| _ -> EvictionQueue(options.Strategy)
192+
| _ -> EvictionQueue(options.Strategy, options.MaximumCapacity, overCapacity)
173193

174194
let tryEvictItems () =
175195
let count =
@@ -182,10 +202,10 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
182202
match store.TryRemove(key) with
183203
| true, removed ->
184204
evictionQueue.Remove(removed)
185-
eviction.Trigger(key)
205+
eviction.Trigger()
186206
| _ ->
187207
failwith "eviction fail"
188-
evictionFail.Trigger(key)
208+
evictionFail.Trigger()
189209

190210
let rec backgroundEviction () =
191211
async {
@@ -212,12 +232,12 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
212232
member _.TryGetValue(key: 'Key, value: outref<'Value>) =
213233
match store.TryGetValue(key) with
214234
| true, cachedEntity ->
215-
cacheHit.Trigger(key, cachedEntity.Value)
235+
cacheHit.Trigger()
216236
evictionQueue.Update(cachedEntity)
217237
value <- cachedEntity.Value
218238
true
219239
| _ ->
220-
cacheMiss.Trigger(key)
240+
cacheMiss.Trigger()
221241
value <- Unchecked.defaultof<'Value>
222242
false
223243

@@ -248,83 +268,116 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
248268

249269
evictionQueue.Add(entity, options.Strategy)
250270

251-
[<CLIEvent>]
252-
member val CacheHit = cacheHit.Publish
271+
interface ICacheEvents with
253272

254-
[<CLIEvent>]
255-
member val CacheMiss = cacheMiss.Publish
273+
[<CLIEvent>]
274+
member val CacheHit = cacheHit.Publish
256275

257-
[<CLIEvent>]
258-
member val Eviction = eviction.Publish
276+
[<CLIEvent>]
277+
member val CacheMiss = cacheMiss.Publish
259278

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
262287

263288
interface IDisposable with
264289
member this.Dispose() =
265290
cts.Cancel()
291+
CacheInstrumentation.RemoveInstrumentation(this)
266292
GC.SuppressFinalize(this)
267293

268294
member this.Dispose() = (this :> IDisposable).Dispose()
269295

270296
override this.Finalize() : unit = this.Dispose()
271297

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)
274303

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)
277311

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
281314

282-
let mutable evictions = 0L
283-
let mutable fails = 0L
284-
let mutable hits = 0L
285-
let mutable misses = 0L
315+
static let instrumentedCaches = ConcurrentDictionary<ICacheEvents, CacheInstrumentation>()
286316

287-
let mutable allEvictions = 0L
288-
let mutable allFails = 0L
289-
let mutable allHits = 0L
290-
let mutable allMisses = 0L
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")
291323

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(1L)
326+
cache.CacheMiss.Add <| fun _ -> misses.Add(1L)
327+
cache.Eviction.Add <| fun _ -> evictions.Add(1L)
328+
cache.EvictionFail.Add <| fun _ -> evictionFails.Add(1L)
329+
cache.OverCapacity.Add <| fun _ -> overCapacity.Add(1L)
296330

297-
cache.CacheMiss
298-
|> Event.add (fun _ ->
299-
Interlocked.Increment &misses |> ignore
300-
Interlocked.Increment &allMisses |> ignore)
331+
let current = ConcurrentDictionary<Instrument, int64 ref>()
301332

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))
306337

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 0L), v) |> ignore)
341+
listener.Start()
342+
#endif
311343

312-
let hitRatio () =
313-
let misses = Interlocked.Exchange(&misses, 0L)
314-
let hits = Interlocked.Exchange(&hits, 0L)
315-
float hits / float (hits + misses)
344+
member val CacheId = $"cache-{Interlocked.Increment(&cacheId)}"
316345

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, 0L) |> 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
318382

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)
324383

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

vsintegration/src/FSharp.Editor/Common/Logging.fs

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -151,41 +151,12 @@ module FSharpServiceTelemetry =
151151

152152
#if DEBUG
153153
let logCacheMetricsToOutput () =
154-
let listener =
155-
new MeterListener(
156-
InstrumentPublished =
157-
fun instrument l ->
158-
if instrument.Meter.Name = "FSharp.Compiler.Caches" then
159-
l.EnableMeasurementEvents(instrument)
160-
)
161-
162-
let measurements = Collections.Generic.Dictionary<_, _>()
163-
let changed = ResizeArray()
164-
165-
let callBack =
166-
MeasurementCallback(fun i v _ _ ->
167-
let v = if Double.IsNaN v then "-" else $"%.1f{v * 100.}%%"
168-
169-
if measurements.ContainsKey(i.Name) && measurements[i.Name] = v then
170-
()
171-
else
172-
measurements[i.Name] <- v
173-
changed.Add i.Name)
174-
175-
listener.SetMeasurementEventCallback callBack
176-
listener.Start()
177154

178155
let timer = new System.Timers.Timer(1000.0, AutoReset = true)
179156

180157
timer.Elapsed.Add(fun _ ->
181-
changed.Clear()
182-
listener.RecordObservableInstruments()
183-
184-
let msg =
185-
seq { for k in changed -> $"{k}: {measurements[k]}" } |> String.concat ", "
186-
187-
if msg <> "" then
188-
logMsg msg)
158+
FSharp.Compiler.CacheInstrumentation.GetStatsUpdateForAllCaches(clearCounts = true)
159+
|> Seq.iter logMsg)
189160

190161
timer.Start()
191162

@@ -199,7 +170,7 @@ module FSharpServiceTelemetry =
199170
OpenTelemetry.Sdk
200171
.CreateMeterProviderBuilder()
201172
.ConfigureResource(fun r -> r.AddService("F#") |> ignore)
202-
.AddMeter(FSharp.Compiler.Cache.MeterName)
173+
.AddMeter(nameof FSharp.Compiler.CacheInstrumentation)
203174
.AddOtlpExporter(fun _e m ->
204175
m.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds <- 1000
205176
m.TemporalityPreference <- MetricReaderTemporalityPreference.Cumulative)

0 commit comments

Comments
 (0)