Skip to content

Commit 9cec56d

Browse files
authored
Caches: remove EntityPool, improve metrics, add eviction stress test (#18786)
1 parent 4f05bdf commit 9cec56d

File tree

10 files changed

+232
-266
lines changed

10 files changed

+232
-266
lines changed

src/Compiler/Checking/TypeRelations.fs

Lines changed: 36 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -101,20 +101,6 @@ let TypesFeasiblyEquiv ndeep g amap m ty1 ty2 =
101101
let TypesFeasiblyEquivStripMeasures g amap m ty1 ty2 =
102102
TypesFeasiblyEquivalent true 0 g amap m ty1 ty2
103103

104-
let inline TryGetCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key =
105-
if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
106-
match amap.TypeSubsumptionCache.TryGetValue(key) with
107-
| true, subsumes ->
108-
ValueSome subsumes
109-
| false, _ ->
110-
ValueNone
111-
else
112-
ValueNone
113-
114-
let inline UpdateCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key subsumes : unit =
115-
if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
116-
amap.TypeSubsumptionCache.TryAdd(key, subsumes) |> ignore
117-
118104
/// The feasible coercion relation. Part of the language spec.
119105
let rec TypeFeasiblySubsumesType ndeep (g: TcGlobals) (amap: ImportMap) m (ty1: TType) (canCoerce: CanCoerce) (ty2: TType) =
120106

@@ -124,41 +110,42 @@ let rec TypeFeasiblySubsumesType ndeep (g: TcGlobals) (amap: ImportMap) m (ty1:
124110
let ty1 = stripTyEqns g ty1
125111
let ty2 = stripTyEqns g ty2
126112

127-
// Check if language feature supported
128-
let key = TTypeCacheKey.FromStrippedTypes (ty1, ty2, canCoerce)
129-
130-
match TryGetCachedTypeSubsumption g amap key with
131-
| ValueSome subsumes ->
132-
subsumes
133-
| ValueNone ->
134-
let subsumes =
135-
match ty1, ty2 with
136-
| TType_measure _, TType_measure _
137-
| TType_var _, _ | _, TType_var _ ->
138-
true
139-
140-
| TType_app (tc1, l1, _), TType_app (tc2, l2, _) when tyconRefEq g tc1 tc2 ->
141-
List.lengthsEqAndForall2 (TypesFeasiblyEquiv ndeep g amap m) l1 l2
142-
143-
| TType_tuple _, TType_tuple _
144-
| TType_anon _, TType_anon _
145-
| TType_fun _, TType_fun _ ->
146-
TypesFeasiblyEquiv ndeep g amap m ty1 ty2
147-
148-
| _ ->
149-
// F# reference types are subtypes of type 'obj'
150-
if isObjTyAnyNullness g ty1 && (canCoerce = CanCoerce || isRefTy g ty2) then
151-
true
152-
elif isAppTy g ty2 && (canCoerce = CanCoerce || isRefTy g ty2) && TypeFeasiblySubsumesTypeWithSupertypeCheck g amap m ndeep ty1 ty2 then
153-
true
154-
else
155-
let interfaces = GetImmediateInterfacesOfType SkipUnrefInterfaces.Yes g amap m ty2
156-
// See if any interface in type hierarchy of ty2 is a supertype of ty1
157-
List.exists (TypeFeasiblySubsumesType (ndeep + 1) g amap m ty1 NoCoerce) interfaces
158-
159-
UpdateCachedTypeSubsumption g amap key subsumes
160-
161-
subsumes
113+
let checkSubsumes ty1 ty2 =
114+
match ty1, ty2 with
115+
| TType_measure _, TType_measure _
116+
| TType_var _, _ | _, TType_var _ ->
117+
true
118+
119+
| TType_app (tc1, l1, _), TType_app (tc2, l2, _) when tyconRefEq g tc1 tc2 ->
120+
List.lengthsEqAndForall2 (TypesFeasiblyEquiv ndeep g amap m) l1 l2
121+
122+
| TType_tuple _, TType_tuple _
123+
| TType_anon _, TType_anon _
124+
| TType_fun _, TType_fun _ ->
125+
TypesFeasiblyEquiv ndeep g amap m ty1 ty2
126+
127+
| _ ->
128+
// F# reference types are subtypes of type 'obj'
129+
if isObjTyAnyNullness g ty1 && (canCoerce = CanCoerce || isRefTy g ty2) then
130+
true
131+
elif isAppTy g ty2 && (canCoerce = CanCoerce || isRefTy g ty2) && TypeFeasiblySubsumesTypeWithSupertypeCheck g amap m ndeep ty1 ty2 then
132+
true
133+
else
134+
let interfaces = GetImmediateInterfacesOfType SkipUnrefInterfaces.Yes g amap m ty2
135+
// See if any interface in type hierarchy of ty2 is a supertype of ty1
136+
List.exists (TypeFeasiblySubsumesType (ndeep + 1) g amap m ty1 NoCoerce) interfaces
137+
138+
if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
139+
let key = TTypeCacheKey.FromStrippedTypes (ty1, ty2, canCoerce)
140+
141+
match amap.TypeSubsumptionCache.TryGetValue(key) with
142+
| true, subsumes -> subsumes
143+
| false, _ ->
144+
let subsumes = checkSubsumes ty1 ty2
145+
amap.TypeSubsumptionCache.TryAdd(key, subsumes) |> ignore
146+
subsumes
147+
else
148+
checkSubsumes ty1 ty2
162149

163150
and TypeFeasiblySubsumesTypeWithSupertypeCheck g amap m ndeep ty1 ty2 =
164151
match GetSuperTypeOfType g amap m ty2 with

src/Compiler/Utilities/Caches.fs

Lines changed: 71 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ open System.Collections.Concurrent
77
open System.Threading
88
open System.Diagnostics
99
open System.Diagnostics.Metrics
10+
open System.Collections.Immutable
1011

1112
[<Struct; RequireQualifiedAccess; NoComparison; NoEquality>]
1213
type 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 0L)
7472

75-
listener.SetMeasurementEventCallback<int64>(fun k v _ _ -> Interlocked.Add(readings.GetOrAdd(k.Name, ref 0L), 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, 0L) |> 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 1L
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

157126
module 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 1L
185+
metrics.Evictions.Add 1L
229186
evicted.Trigger()
230-
| _ -> evictionFails.Add 1L
187+
| _ ->
188+
// This should not be possible to happen, but if it does, we want to know.
189+
metrics.EvictionFails.Add 1L
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 1L
213+
metrics.Hits.Add 1L
255214
evictionProcessor.Post(EvictionQueueMessage.Update entity)
256215
value <- entity.Value
257216
true
258217
| _ ->
259-
misses.Add 1L
218+
metrics.Misses.Add 1L
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

Comments
 (0)