Skip to content

Commit 3033da3

Browse files
committed
Merge branch 'cache-stats' into cache-2
2 parents 14a7822 + ea5e9fe commit 3033da3

File tree

8 files changed

+152
-97
lines changed

8 files changed

+152
-97
lines changed

src/Compiler/Utilities/Caches.fs

Lines changed: 116 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -22,57 +22,103 @@ module CacheMetrics =
2222
let creations = Meter.CreateCounter<int64>("creations", "count")
2323
let disposals = Meter.CreateCounter<int64>("disposals", "count")
2424

25-
let mkTag name = KeyValuePair<_, obj>("name", name)
26-
27-
let Add (tag: KeyValuePair<_, _>) = adds.Add(1L, tag)
28-
let Update (tag: KeyValuePair<_, _>) = updates.Add(1L, tag)
29-
let Hit (tag: KeyValuePair<_, _>) = hits.Add(1L, tag)
30-
let Miss (tag: KeyValuePair<_, _>) = misses.Add(1L, tag)
31-
let Eviction (tag: KeyValuePair<_, _>) = evictions.Add(1L, tag)
32-
let EvictionFail (tag: KeyValuePair<_, _>) = evictionFails.Add(1L, tag)
33-
let Created (tag: KeyValuePair<_, _>) = creations.Add(1L, tag)
34-
let Disposed (tag: KeyValuePair<_, _>) = disposals.Add(1L, tag)
35-
36-
// Currently the Cache emits telemetry for raw cache events: hits, misses, evictions etc.
37-
// This class observes those counters and keeps a snapshot of readings. It is used in tests and can be used to print cache stats in debug mode.
38-
type CacheMetricsListener(tag) =
39-
let totals = Map [ for counter in CacheMetrics.allCounters -> counter.Name, ref 0L ]
40-
41-
let incr key v =
42-
Interlocked.Add(totals[key], v) |> ignore
43-
44-
let total key = totals[key].Value
45-
46-
let mutable ratio = Double.NaN
25+
let mutable private nextCacheId = 0
26+
27+
let mkTags (name: string) =
28+
let cacheId = Interlocked.Increment &nextCacheId
29+
[| "name", box name; "cacheId", box cacheId |]
30+
|> Array.map KeyValuePair
31+
|> TagList
32+
33+
let Add (tags: inref<TagList>) = adds.Add(1L, &tags)
34+
let Update (tags: inref<TagList>) = updates.Add(1L, &tags)
35+
let Hit (tags: inref<TagList>) = hits.Add(1L, &tags)
36+
let Miss (tags: inref<TagList>) = misses.Add(1L, &tags)
37+
let Eviction (tags: inref<TagList>) = evictions.Add(1L, &tags)
38+
let EvictionFail (tags: inref<TagList>) = evictionFails.Add(1L, &tags)
39+
let Created (tags: inref<TagList>) = creations.Add(1L, &tags)
40+
let Disposed (tags: inref<TagList>) = disposals.Add(1L, &tags)
41+
42+
type Stats() =
43+
let totals = Map [ for counter in allCounters -> counter.Name, ref 0L ]
44+
let total key = totals[key].Value
45+
46+
let mutable ratio = Double.NaN
47+
48+
let updateRatio () =
49+
ratio <-
50+
float (total hits.Name)
51+
/ float (total hits.Name + total misses.Name)
52+
53+
member _.Incr key v =
54+
assert (totals.ContainsKey key)
55+
Interlocked.Add(totals[key], v) |> ignore
56+
57+
if key = hits.Name || key = misses.Name then
58+
updateRatio ()
59+
60+
member _.GetTotals() =
61+
[ for k in totals.Keys -> k, total k ] |> Map.ofList
62+
63+
member _.Ratio = ratio
64+
65+
override _.ToString() =
66+
let parts =
67+
[ for kv in totals do
68+
yield $"{kv.Key}={kv.Value.Value}"
69+
if not (Double.IsNaN ratio) then
70+
yield $"hit-ratio={ratio:P2}" ]
71+
String.Join(", ", parts)
72+
73+
let statsByName = ConcurrentDictionary<string, Stats>()
74+
75+
let getStatsByName name = statsByName.GetOrAdd(name, fun _ -> Stats ())
76+
77+
let ListenToAll () =
78+
let listener = new MeterListener()
79+
for instrument in allCounters do
80+
listener.EnableMeasurementEvents instrument
81+
listener.SetMeasurementEventCallback(fun instrument v tags _ ->
82+
match tags[0].Value with
83+
| :? string as name ->
84+
let stats = getStatsByName name
85+
stats.Incr instrument.Name v
86+
| _ -> assert false)
87+
listener.Start()
4788

48-
let updateRatio () =
49-
ratio <-
50-
float (total CacheMetrics.hits.Name)
51-
/ float (total CacheMetrics.hits.Name + total CacheMetrics.misses.Name)
89+
let StatsToString () =
90+
let sb = Text.StringBuilder()
91+
sb.AppendLine "Cache Metrics:" |> ignore
92+
for kv in statsByName do
93+
sb.AppendLine $"Cache {kv.Key}: {kv.Value}" |> ignore
94+
sb.AppendLine() |> ignore
95+
string sb
5296

53-
let listener = new MeterListener()
97+
// Currently the Cache emits telemetry for raw cache events: hits, misses, evictions etc.
98+
// This type observes those counters and keeps a snapshot of readings. It is used in tests and can be used to print cache stats in debug mode.
99+
type CacheMetricsListener(cacheTags: TagList) =
54100

55-
do
101+
let stats = Stats()
102+
let listener = new MeterListener()
56103

57-
for instrument in CacheMetrics.allCounters do
58-
listener.EnableMeasurementEvents instrument
104+
do
105+
for instrument in allCounters do
106+
listener.EnableMeasurementEvents instrument
59107

60-
listener.SetMeasurementEventCallback(fun instrument v tags _ ->
61-
if tags[0] = tag then
62-
incr instrument.Name v
108+
listener.SetMeasurementEventCallback(fun instrument v tags _ ->
109+
let tagsMatch = tags[0] = cacheTags[0] && tags[1] = cacheTags[1]
110+
if tagsMatch then stats.Incr instrument.Name v)
63111

64-
if instrument = CacheMetrics.hits || instrument = CacheMetrics.misses then
65-
updateRatio ())
112+
listener.Start()
66113

67-
listener.Start()
114+
interface IDisposable with
115+
member _.Dispose() = listener.Dispose()
68116

69-
interface IDisposable with
70-
member _.Dispose() = listener.Dispose()
117+
member _.GetTotals() = stats.GetTotals()
71118

72-
member _.GetTotals() =
73-
[ for k in totals.Keys -> k, total k ] |> Map.ofList
119+
member _.Ratio = stats.Ratio
74120

75-
member _.GetStats() = [ "hit-ratio", ratio ] |> Map.ofList
121+
override _.ToString() = stats.ToString()
76122

77123
[<RequireQualifiedAccess>]
78124
type EvictionMode =
@@ -163,7 +209,7 @@ type EvictionQueueMessage<'Entity, 'Target> =
163209
| Update of 'Entity
164210

165211
[<Sealed; NoComparison; NoEquality>]
166-
[<DebuggerDisplay("{GetStats()}")>]
212+
[<DebuggerDisplay("{DebugDisplay()}")>]
167213
type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Key>, ?name) =
168214

169215
do
@@ -190,7 +236,7 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke
190236
let evicted = Event<_>()
191237
let evictionFailed = Event<_>()
192238

193-
let tag = CacheMetrics.mkTag name
239+
let tags = CacheMetrics.mkTags name
194240

195241
// Track disposal state (0 = not disposed, 1 = disposed)
196242
let mutable disposed = 0
@@ -223,10 +269,10 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke
223269

224270
match store.TryRemove(first.Value.Key) with
225271
| true, _ ->
226-
CacheMetrics.Eviction tag
272+
CacheMetrics.Eviction &tags
227273
evicted.Trigger()
228274
| _ ->
229-
CacheMetrics.EvictionFail tag
275+
CacheMetrics.EvictionFail &tags
230276
evictionFailed.Trigger()
231277
deadKeysCount <- deadKeysCount + 1
232278

@@ -244,11 +290,14 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke
244290
let startEvictionProcessor ct =
245291
MailboxProcessor.Start(
246292
(fun mb ->
247-
async {
248-
while true do
293+
let rec processNext () =
294+
async {
249295
let! message = mb.Receive()
250296
processEvictionMessage message
251-
}),
297+
return! processNext ()
298+
}
299+
300+
processNext ()),
252301
ct
253302
)
254303

@@ -271,20 +320,24 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke
271320

272321
post, dispose
273322

274-
do CacheMetrics.Created tag
323+
#if DEBUG
324+
let debugListener = new CacheMetrics.CacheMetricsListener(tags)
325+
#endif
326+
327+
do CacheMetrics.Created &tags
275328

276329
member val Evicted = evicted.Publish
277330
member val EvictionFailed = evictionFailed.Publish
278331

279332
member _.TryGetValue(key: 'Key, value: outref<'Value>) =
280333
match store.TryGetValue(key) with
281334
| true, entity ->
282-
CacheMetrics.Hit tag
335+
CacheMetrics.Hit &tags
283336
post (EvictionQueueMessage.Update entity)
284337
value <- entity.Value
285338
true
286339
| _ ->
287-
CacheMetrics.Miss tag
340+
CacheMetrics.Miss &tags
288341
value <- Unchecked.defaultof<'Value>
289342
false
290343

@@ -294,7 +347,7 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke
294347
let added = store.TryAdd(key, entity)
295348

296349
if added then
297-
CacheMetrics.Add tag
350+
CacheMetrics.Add &tags
298351
post (EvictionQueueMessage.Add(entity, store))
299352

300353
added
@@ -311,11 +364,11 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke
311364

312365
if wasMiss then
313366
post (EvictionQueueMessage.Add(result, store))
314-
CacheMetrics.Add tag
315-
CacheMetrics.Miss tag
367+
CacheMetrics.Add &tags
368+
CacheMetrics.Miss &tags
316369
else
317370
post (EvictionQueueMessage.Update result)
318-
CacheMetrics.Hit tag
371+
CacheMetrics.Hit &tags
319372

320373
result.Value
321374

@@ -330,18 +383,18 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke
330383

331384
// Returned value tells us if the entity was added or updated.
332385
if Object.ReferenceEquals(addValue, result) then
333-
CacheMetrics.Add tag
386+
CacheMetrics.Add &tags
334387
post (EvictionQueueMessage.Add(addValue, store))
335388
else
336-
CacheMetrics.Update tag
389+
CacheMetrics.Update &tags
337390
post (EvictionQueueMessage.Update result)
338391

339-
member _.CreateMetricsListener() = new CacheMetricsListener(tag)
392+
member _.CreateMetricsListener() = new CacheMetrics.CacheMetricsListener(tags)
340393

341394
member _.Dispose() =
342395
if Interlocked.Exchange(&disposed, 1) = 0 then
343396
disposeEvictionProcessor ()
344-
CacheMetrics.Disposed tag
397+
CacheMetrics.Disposed &tags
345398

346399
interface IDisposable with
347400
member this.Dispose() =
@@ -350,3 +403,7 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke
350403

351404
// Finalizer to ensure eviction loop is cancelled if Dispose wasn't called.
352405
override this.Finalize() = this.Dispose()
406+
407+
#if DEBUG
408+
member _.DebugDisplay() = debugListener.ToString()
409+
#endif

src/Compiler/Utilities/Caches.fsi

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ module internal CacheMetrics =
88
/// Global telemetry Meter for all caches. Exposed for testing purposes.
99
/// Set FSHARP_OTEL_EXPORT environment variable to enable OpenTelemetry export to external collectors in tests.
1010
val Meter: Meter
11+
val ListenToAll: unit -> unit
12+
val StatsToString: unit -> string
1113

12-
/// A local listener that can be created for a specific Cache instance to get its metrics. For testing purposes only.
13-
[<Class>]
14-
type internal CacheMetricsListener =
15-
member GetStats: unit -> Map<string, float>
16-
member GetTotals: unit -> Map<string, int64>
17-
interface IDisposable
14+
/// A local listener that can be created for a specific Cache instance to get its metrics. For testing purposes only.
15+
[<Class>]
16+
type internal CacheMetricsListener =
17+
member Ratio: float
18+
member GetTotals: unit -> Map<string, int64>
19+
interface IDisposable
1820

1921
[<RequireQualifiedAccess; NoComparison>]
2022
type internal EvictionMode =
@@ -64,4 +66,4 @@ type internal Cache<'Key, 'Value when 'Key: not null> =
6466
/// For testing only.
6567
member EvictionFailed: IEvent<unit>
6668
/// For testing only. Creates a local telemetry listener for this cache instance.
67-
member CreateMetricsListener: unit -> CacheMetricsListener
69+
member CreateMetricsListener: unit -> CacheMetrics.CacheMetricsListener

tests/Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<NoOptimizationData>false</NoOptimizationData>
1717
<NoInterfaceData>false</NoInterfaceData>
1818
<CompressMetadata>true</CompressMetadata>
19+
<ServerGarbageCollection>true</ServerGarbageCollection>
1920
</PropertyGroup>
2021

2122
</Project>

tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,9 @@ let ``Metrics can be retrieved`` () =
133133
cache.TryAdd("key3", 3) |> shouldBeTrue
134134
evictionCompleted.Task.Wait shouldNeverTimeout |> shouldBeTrue
135135

136-
let stats = metricsListener.GetStats()
137136
let totals = metricsListener.GetTotals()
138137

139-
stats.["hit-ratio"] |> shouldEqual 1.0
138+
metricsListener.Ratio |> shouldEqual 1.0
140139
totals.["evictions"] |> shouldEqual 1L
141140
totals.["adds"] |> shouldEqual 3L
142141

@@ -156,11 +155,10 @@ let ``GetOrAdd basic usage`` () =
156155
v3 |> shouldEqual 4
157156
factoryCalls |> shouldEqual 2
158157
// Metrics assertions
159-
let stats = metricsListener.GetStats()
160158
let totals = metricsListener.GetTotals()
161159
totals.["hits"] |> shouldEqual 1L
162160
totals.["misses"] |> shouldEqual 2L
163-
stats.["hit-ratio"] |> shouldEqual (1.0/3.0)
161+
metricsListener.Ratio |> shouldEqual (1.0/3.0)
164162
totals.["adds"] |> shouldEqual 2L
165163

166164
[<Fact>]
@@ -179,11 +177,10 @@ let ``AddOrUpdate basic usage`` () =
179177
cache.TryGetValue("y", &value) |> shouldBeTrue
180178
value |> shouldEqual 99
181179
// Metrics assertions
182-
let stats = metricsListener.GetStats()
183180
let totals = metricsListener.GetTotals()
184181
totals.["hits"] |> shouldEqual 3L // 3 cache hits
185182
totals.["misses"] |> shouldEqual 0L // 0 cache misses
186-
stats.["hit-ratio"] |> shouldEqual 1.0
183+
metricsListener.Ratio |> shouldEqual 1.0
187184
totals.["adds"] |> shouldEqual 2L // "x" and "y" added
188185
totals.["updates"] |> shouldEqual 1L // "x" updated
189186

@@ -220,11 +217,10 @@ let ``GetOrAdd with reference identity`` () =
220217
v1'' |> shouldEqual v1'
221218
v2'' |> shouldEqual v2'
222219
// Metrics assertions
223-
let stats = metricsListener.GetStats()
224220
let totals = metricsListener.GetTotals()
225221
totals.["hits"] |> shouldEqual 4L
226222
totals.["misses"] |> shouldEqual 3L
227-
stats.["hit-ratio"] |> shouldEqual (4.0 / 7.0)
223+
metricsListener.Ratio |> shouldEqual (4.0 / 7.0)
228224
totals.["adds"] |> shouldEqual 2L
229225

230226
[<Fact>]
@@ -250,10 +246,9 @@ let ``AddOrUpdate with reference identity`` () =
250246
cache.TryGetValue(t1, &value1Updated) |> shouldBeTrue
251247
value1Updated |> shouldEqual 9
252248
// Metrics assertions
253-
let stats = metricsListener.GetStats()
254249
let totals = metricsListener.GetTotals()
255250
totals.["hits"] |> shouldEqual 3L // 3 cache hits
256251
totals.["misses"] |> shouldEqual 0L // 0 cache misses
257-
stats.["hit-ratio"] |> shouldEqual 1.0
252+
metricsListener.Ratio |> shouldEqual 1.0
258253
totals.["adds"] |> shouldEqual 2L // t1 and t2 added
259254
totals.["updates"] |> shouldEqual 1L // t1 updated once

0 commit comments

Comments
 (0)