diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index ba843ca702c..1024e0eb30e 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -1,5 +1,6 @@ ### Fixed +* Stop leaking a `System.Diagnostics.Metrics.MeterListener` per `Cache` in DEBUG builds. Each cache created a `CacheMetricsListener` (which starts a `MeterListener` registered in the process-global metrics registry) and never disposed it, so listeners accumulated for the lifetime of the process. Because every cache hit/miss/add published to all registered listeners, the per-operation cost grew linearly with the number of leaked listeners, so repeated checks (and Debug FCS test runs) slowed down over time. The per-cache `DebugDisplay` totals are now tracked directly instead of via a `MeterListener`. ([PR #19995](https://github.com/dotnet/fsharp/pull/19995)) * Semantic classification no longer marks recursive object self-references (`as this`, `let rec` self-refs) as mutable. ([Issue #5229](https://github.com/dotnet/fsharp/issues/5229)) * Fix `MethodAccessException` under `--realsig+` when a closure (inner `let rec`, `task`/`async` state machine, or quotation splice) inside a member defined in an intrinsic type augmentation (`type C with member ...`) accesses a `private` member of `C`. The synthesized closure is now nested inside the declaring type instead of beside it in the module class. ([Issue #19933](https://github.com/dotnet/fsharp/issues/19933), [PR #19955](https://github.com/dotnet/fsharp/pull/19955)) * Preserve source range for type errors on empty-bodied computation expressions (e.g. `foo {}`) in pipelines, function arguments, and type-annotated contexts, instead of reporting `unknown(1,1)`. ([Issue #19550](https://github.com/dotnet/fsharp/issues/19550), [PR #19849](https://github.com/dotnet/fsharp/pull/19849)) diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index fb024844e09..17d0c8e2ac1 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -32,12 +32,6 @@ module CacheMetrics = tags.Add("cacheId", box cacheId) tags - let Add (tags: inref) = adds.Add(1L, &tags) - let Update (tags: inref) = updates.Add(1L, &tags) - let Hit (tags: inref) = hits.Add(1L, &tags) - let Miss (tags: inref) = misses.Add(1L, &tags) - let Eviction (tags: inref) = evictions.Add(1L, &tags) - let EvictionFail (tags: inref) = evictionFails.Add(1L, &tags) let Created (tags: inref) = creations.Add(1L, &tags) let Disposed (tags: inref) = disposals.Add(1L, &tags) @@ -279,6 +273,23 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke let tags = CacheMetrics.mkTags name + // Per-cache metric totals used by DebugDisplay. These are incremented directly (see recordMetric) + // rather than via a per-cache CacheMetrics.CacheMetricsListener: a listener starts a + // System.Diagnostics.Metrics.MeterListener which registers in the process-global metrics registry, + // so one per Cache would never be disposed (leak) and would make every measurement O(number of + // caches) as listeners accumulate. +#if DEBUG + let debugStats = CacheMetrics.Stats() +#endif + + // Publish one measurement to the global Meter counter (for --times / StatsToString) and, in DEBUG, + // record it in this cache's own totals for DebugDisplay. + let recordMetric (counter: Counter) = + counter.Add(1L, &tags) +#if DEBUG + debugStats.Incr counter.Name 1L +#endif + // Track disposal state (0 = not disposed, 1 = disposed) let mutable disposed = 0 @@ -310,10 +321,10 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke match store.TryRemove(first.Value.Key) with | true, _ -> - CacheMetrics.Eviction &tags + recordMetric CacheMetrics.evictions evicted.Trigger() | _ -> - CacheMetrics.EvictionFail &tags + recordMetric CacheMetrics.evictionFails evictionFailed.Trigger() deadKeysCount <- deadKeysCount + 1 @@ -361,10 +372,6 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke post, dispose -#if DEBUG - let debugListener = new CacheMetrics.CacheMetricsListener(tags) -#endif - do CacheMetrics.Created &tags member val Evicted = evicted.Publish @@ -373,12 +380,12 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke member _.TryGetValue(key: 'Key, value: outref<'Value>) = match store.TryGetValue(key) with | true, entity -> - CacheMetrics.Hit &tags + recordMetric CacheMetrics.hits post (EvictionQueueMessage.Update entity) value <- entity.Value true | _ -> - CacheMetrics.Miss &tags + recordMetric CacheMetrics.misses value <- Unchecked.defaultof<'Value> false @@ -388,7 +395,7 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke let added = store.TryAdd(key, entity) if added then - CacheMetrics.Add &tags + recordMetric CacheMetrics.adds post (EvictionQueueMessage.Add(entity, store)) added @@ -405,11 +412,11 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke if wasMiss then post (EvictionQueueMessage.Add(result, store)) - CacheMetrics.Add &tags - CacheMetrics.Miss &tags + recordMetric CacheMetrics.adds + recordMetric CacheMetrics.misses else post (EvictionQueueMessage.Update result) - CacheMetrics.Hit &tags + recordMetric CacheMetrics.hits result.Value @@ -424,10 +431,10 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke // Returned value tells us if the entity was added or updated. if Object.ReferenceEquals(addValue, result) then - CacheMetrics.Add &tags + recordMetric CacheMetrics.adds post (EvictionQueueMessage.Add(addValue, store)) else - CacheMetrics.Update &tags + recordMetric CacheMetrics.updates post (EvictionQueueMessage.Update result) member _.CreateMetricsListener() = @@ -447,5 +454,5 @@ type Cache<'Key, 'Value when 'Key: not null> internal (options: CacheOptions<'Ke override this.Finalize() = this.Dispose() #if DEBUG - member _.DebugDisplay() = debugListener.ToString() + member _.DebugDisplay() = debugStats.ToString() #endif