From 2e8000fe7784b8a78209c4cdd0f277a9beee482e Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Thu, 10 Apr 2025 15:38:13 +0200
Subject: [PATCH 01/44] add caches
---
src/Compiler/FSharp.Compiler.Service.fsproj | 1 +
src/Compiler/Utilities/Caches.fs | 206 ++++++++++++++++++++
2 files changed, 207 insertions(+)
create mode 100644 src/Compiler/Utilities/Caches.fs
diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj
index 74e59954e8f..af43f511094 100644
--- a/src/Compiler/FSharp.Compiler.Service.fsproj
+++ b/src/Compiler/FSharp.Compiler.Service.fsproj
@@ -146,6 +146,7 @@
+
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
new file mode 100644
index 00000000000..469f56a3b98
--- /dev/null
+++ b/src/Compiler/Utilities/Caches.fs
@@ -0,0 +1,206 @@
+namespace FSharp.Compiler
+
+open System
+open System.Collections.Concurrent
+open System.Threading
+open System.Threading.Tasks
+open System.Diagnostics
+
+[]
+// Default Seq.* function have one issue - when doing `Seq.sortBy`, it will call a `ToArray` on the collection,
+// which is *not* calling `ConcurrentDictionary.ToArray`, but uses a custom one instead (treating it as `ICollection`)
+// this leads to and exception when trying to evict without locking (The index is equal to or greater than the length of the array,
+// or the number of elements in the dictionary is greater than the available space from index to the end of the destination array.)
+// this is casuedby insertions happened between reading the `Count` and doing the `CopyTo`.
+// This solution introduces a custom `ConcurrentDictionary.sortBy` which will be calling a proper `CopyTo`, the one on the ConcurrentDictionary itself.
+module ConcurrentDictionary =
+
+ open System.Collections
+ open System.Collections.Generic
+
+ let inline mkSeq f =
+ { new IEnumerable<'U> with
+ member _.GetEnumerator() = f ()
+
+ interface IEnumerable with
+ member _.GetEnumerator() = (f () :> IEnumerator)
+ }
+
+ let inline mkDelayedSeq (f: unit -> IEnumerable<'T>) = mkSeq (fun () -> f().GetEnumerator())
+
+ let inline sortBy ([] projection) (source: ConcurrentDictionary<_, _>) =
+ mkDelayedSeq (fun () ->
+ let array = source.ToArray()
+ Array.sortInPlaceBy projection array
+ array :> seq<_>)
+
+[]
+type CachingStrategy =
+ | LRU
+ | LFU
+
+[]
+type EvictionMethod =
+ | Blocking
+ | Background
+
+[]
+type CacheOptions =
+ {
+ MaximumCapacity: int
+ PercentageToEvict: int
+ Strategy: CachingStrategy
+ EvictionMethod: EvictionMethod
+ LevelOfConcurrency: int
+ }
+
+ static member Default =
+ {
+ MaximumCapacity = 100
+ PercentageToEvict = 5
+ Strategy = CachingStrategy.LRU
+ LevelOfConcurrency = Environment.ProcessorCount
+ EvictionMethod = EvictionMethod.Blocking
+ }
+
+[]
+type CachedEntity<'Value> =
+ val Value: 'Value
+ val mutable LastAccessed: int64
+ val mutable AccessCount: int64
+
+ new(value: 'Value) =
+ {
+ Value = value
+ LastAccessed = DateTimeOffset.Now.Ticks
+ AccessCount = 0L
+ }
+
+[]
+[]
+type Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts) =
+
+ let cacheHit = Event<_ * _>()
+ let cacheMiss = Event<_>()
+ let eviction = Event<_>()
+
+ []
+ member val CacheHit = cacheHit.Publish
+
+ []
+ member val CacheMiss = cacheMiss.Publish
+
+ []
+ member val Eviction = eviction.Publish
+
+ // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary.
+ member val Store = ConcurrentDictionary<_, CachedEntity<'Value>>(options.LevelOfConcurrency, capacity)
+
+ static member Create(options: CacheOptions) =
+ let capacity =
+ options.MaximumCapacity
+ + (options.MaximumCapacity * options.PercentageToEvict / 100)
+
+ let cts = new CancellationTokenSource()
+ let cache = new Cache<'Key, 'Value>(options, capacity, cts)
+
+ if options.EvictionMethod = EvictionMethod.Background then
+ Task.Run(cache.TryEvictTask, cts.Token) |> ignore
+
+ cache
+
+ member this.GetStats() =
+ {|
+ Capacity = options.MaximumCapacity
+ PercentageToEvict = options.PercentageToEvict
+ Strategy = options.Strategy
+ LevelOfConcurrency = options.LevelOfConcurrency
+ Count = this.Store.Count
+ MostRecentlyAccesssed = this.Store.Values |> Seq.maxBy _.LastAccessed |> _.LastAccessed
+ LeastRecentlyAccesssed = this.Store.Values |> Seq.minBy _.LastAccessed |> _.LastAccessed
+ MostFrequentlyAccessed = this.Store.Values |> Seq.maxBy _.AccessCount |> _.AccessCount
+ LeastFrequentlyAccessed = this.Store.Values |> Seq.minBy _.AccessCount |> _.AccessCount
+ |}
+
+ member private this.CalculateEvictionCount() =
+ if this.Store.Count >= options.MaximumCapacity then
+ (this.Store.Count - options.MaximumCapacity)
+ + (options.MaximumCapacity * options.PercentageToEvict / 100)
+ else
+ 0
+
+ // TODO: All of these are proofs of concept, a very naive implementation of eviction strategies, it will always walk the dictionary to find the items to evict, this is not efficient.
+ member private this.TryGetPickToEvict() =
+ this.Store
+ |> match options.Strategy with
+ | CachingStrategy.LRU -> ConcurrentDictionary.sortBy _.Value.LastAccessed
+ | CachingStrategy.LFU -> ConcurrentDictionary.sortBy _.Value.AccessCount
+ |> Seq.take (this.CalculateEvictionCount())
+ |> Seq.map (fun x -> x.Key)
+
+ // TODO: Explore an eviction shortcut, some sort of list of keys to evict first, based on the strategy.
+ member private this.TryEvictItems() =
+ if this.CalculateEvictionCount() > 0 then
+ for key in this.TryGetPickToEvict() do
+ match this.Store.TryRemove(key) with
+ | true, _ -> eviction.Trigger(key)
+ | _ -> () // TODO: We probably want to count eviction misses as well?
+
+ // TODO: Shall this be a safer task, wrapping everything in try .. with, so it's not crashing silently?
+ member private this.TryEvictTask() =
+ backgroundTask {
+ while not cts.Token.IsCancellationRequested do
+ let evictionCount = this.CalculateEvictionCount()
+
+ if evictionCount > 0 then
+ this.TryEvictItems()
+
+ let utilization = (this.Store.Count / options.MaximumCapacity)
+ // So, based on utilization this will scale the delay between 0 and 1 seconds.
+ // Worst case scenario would be when 1 second delay happens,
+ // if the cache will grow rapidly (or in bursts), it will go beyond the maximum capacity.
+ // In this case underlying dictionary will resize, AND we will have to evict items, which will likely be slow.
+ // In this case, cache stats should be used to adjust MaximumCapacity and PercentageToEvict.
+ let delay = 1000 - (1000 * utilization)
+
+ if delay > 0 then
+ do! Task.Delay(delay)
+ }
+
+ member this.TryEvict() =
+ if this.CalculateEvictionCount() > 0 then
+ match options.EvictionMethod with
+ | EvictionMethod.Blocking -> this.TryEvictItems()
+ | EvictionMethod.Background -> ()
+
+ member this.TryGet(key, value: outref<'Value>) =
+ match this.Store.TryGetValue(key) with
+ | true, cachedEntity ->
+ // this is fine to be non-atomic, I guess, we are okay with race if the time is within the time of multiple concurrent calls.
+ cachedEntity.LastAccessed <- DateTimeOffset.Now.Ticks
+ let _ = Interlocked.Increment(&cachedEntity.AccessCount)
+ cacheHit.Trigger(key, cachedEntity.Value)
+ value <- cachedEntity.Value
+ true
+ | _ ->
+ cacheMiss.Trigger(key)
+ value <- Unchecked.defaultof<'Value>
+ false
+
+ member this.TryAdd(key, value: 'Value, ?update: bool) =
+ let update = defaultArg update false
+
+ this.TryEvict()
+
+ let value = CachedEntity<'Value>(value)
+
+ if update then
+ let _ = this.Store.AddOrUpdate(key, value, (fun _ _ -> value))
+ true
+ else
+ this.Store.TryAdd(key, value)
+
+ interface IDisposable with
+ member _.Dispose() = cts.Cancel()
+
+ member this.Dispose() = (this :> IDisposable).Dispose()
From 17f2d9325c2792e2c0ba0d199d7ec6552dd893fc Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Thu, 10 Apr 2025 15:38:26 +0200
Subject: [PATCH 02/44] plug it in
---
src/Compiler/Checking/TypeRelations.fs | 8 ++++----
src/Compiler/Checking/import.fs | 3 ++-
src/Compiler/Checking/import.fsi | 2 +-
3 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/src/Compiler/Checking/TypeRelations.fs b/src/Compiler/Checking/TypeRelations.fs
index 2cb5dd4057a..daa5a656415 100644
--- a/src/Compiler/Checking/TypeRelations.fs
+++ b/src/Compiler/Checking/TypeRelations.fs
@@ -102,8 +102,8 @@ let TypesFeasiblyEquivStripMeasures g amap m ty1 ty2 =
TypesFeasiblyEquivalent true 0 g amap m ty1 ty2
let inline TryGetCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key =
- if g.compilationMode = CompilationMode.OneOff && g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
- match amap.TypeSubsumptionCache.TryGetValue(key) with
+ if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
+ match amap.TypeSubsumptionCache.TryGet(key) with
| true, subsumes ->
ValueSome subsumes
| false, _ ->
@@ -112,8 +112,8 @@ let inline TryGetCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key =
ValueNone
let inline UpdateCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key subsumes : unit =
- if g.compilationMode = CompilationMode.OneOff && g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
- amap.TypeSubsumptionCache[key] <- subsumes
+ if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
+ amap.TypeSubsumptionCache.TryAdd(key, subsumes) |> ignore
/// The feasible coercion relation. Part of the language spec.
let rec TypeFeasiblySubsumesType ndeep (g: TcGlobals) (amap: ImportMap) m (ty1: TType) (canCoerce: CanCoerce) (ty2: TType) =
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index c87d6cdad03..836087f2534 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -106,7 +106,8 @@ type [] TTypeCacheKey =
type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) =
let typeRefToTyconRefCache = ConcurrentDictionary()
- let typeSubsumptionCache = ConcurrentDictionary(System.Environment.ProcessorCount, 1024)
+ let typeSubsumptionCache =
+ Cache.Create({ CacheOptions.Default with MaximumCapacity = 1024 })
member _.g = g
diff --git a/src/Compiler/Checking/import.fsi b/src/Compiler/Checking/import.fsi
index c387558fcba..043692ac41c 100644
--- a/src/Compiler/Checking/import.fsi
+++ b/src/Compiler/Checking/import.fsi
@@ -73,7 +73,7 @@ type ImportMap =
member g: TcGlobals
/// Type subsumption cache
- member TypeSubsumptionCache: ConcurrentDictionary
+ member TypeSubsumptionCache: Cache
module Nullness =
From 17d2cbbee54e6d732f04f76c2a488c7639f8586a Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Thu, 10 Apr 2025 18:04:17 +0200
Subject: [PATCH 03/44] internal
---
src/Compiler/Utilities/Caches.fs | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 469f56a3b98..6365aa660f5 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -13,7 +13,7 @@ open System.Diagnostics
// or the number of elements in the dictionary is greater than the available space from index to the end of the destination array.)
// this is casuedby insertions happened between reading the `Count` and doing the `CopyTo`.
// This solution introduces a custom `ConcurrentDictionary.sortBy` which will be calling a proper `CopyTo`, the one on the ConcurrentDictionary itself.
-module ConcurrentDictionary =
+module internal ConcurrentDictionary =
open System.Collections
open System.Collections.Generic
@@ -35,17 +35,17 @@ module ConcurrentDictionary =
array :> seq<_>)
[]
-type CachingStrategy =
+type internal CachingStrategy =
| LRU
| LFU
[]
-type EvictionMethod =
+type internal EvictionMethod =
| Blocking
| Background
[]
-type CacheOptions =
+type internal CacheOptions =
{
MaximumCapacity: int
PercentageToEvict: int
@@ -64,7 +64,7 @@ type CacheOptions =
}
[]
-type CachedEntity<'Value> =
+type internal CachedEntity<'Value> =
val Value: 'Value
val mutable LastAccessed: int64
val mutable AccessCount: int64
@@ -78,7 +78,7 @@ type CachedEntity<'Value> =
[]
[]
-type Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts) =
+type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts) =
let cacheHit = Event<_ * _>()
let cacheMiss = Event<_>()
From ced43c564dd61ac84afed1a2106de460b82846cd Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Thu, 10 Apr 2025 18:49:58 +0200
Subject: [PATCH 04/44] ok
---
src/Compiler/Utilities/Caches.fs | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 6365aa660f5..5e9ea27eb10 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -109,18 +109,18 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
cache
- member this.GetStats() =
- {|
- Capacity = options.MaximumCapacity
- PercentageToEvict = options.PercentageToEvict
- Strategy = options.Strategy
- LevelOfConcurrency = options.LevelOfConcurrency
- Count = this.Store.Count
- MostRecentlyAccesssed = this.Store.Values |> Seq.maxBy _.LastAccessed |> _.LastAccessed
- LeastRecentlyAccesssed = this.Store.Values |> Seq.minBy _.LastAccessed |> _.LastAccessed
- MostFrequentlyAccessed = this.Store.Values |> Seq.maxBy _.AccessCount |> _.AccessCount
- LeastFrequentlyAccessed = this.Store.Values |> Seq.minBy _.AccessCount |> _.AccessCount
- |}
+ //member this.GetStats() =
+ // {|
+ // Capacity = options.MaximumCapacity
+ // PercentageToEvict = options.PercentageToEvict
+ // Strategy = options.Strategy
+ // LevelOfConcurrency = options.LevelOfConcurrency
+ // Count = this.Store.Count
+ // MostRecentlyAccesssed = this.Store.Values |> Seq.maxBy _.LastAccessed |> _.LastAccessed
+ // LeastRecentlyAccesssed = this.Store.Values |> Seq.minBy _.LastAccessed |> _.LastAccessed
+ // MostFrequentlyAccessed = this.Store.Values |> Seq.maxBy _.AccessCount |> _.AccessCount
+ // LeastFrequentlyAccessed = this.Store.Values |> Seq.minBy _.AccessCount |> _.AccessCount
+ // |}
member private this.CalculateEvictionCount() =
if this.Store.Count >= options.MaximumCapacity then
From 08d37300fcdcd02a998eb19b2bd84112cb05f56b Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Sat, 12 Apr 2025 11:42:09 +0200
Subject: [PATCH 05/44] trace count in incremental use
---
src/Compiler/Checking/TypeRelations.fs | 12 ++---
src/Compiler/Checking/import.fs | 7 ++-
src/Compiler/Utilities/Caches.fs | 44 +++++++++++++------
.../src/FSharp.Editor/Common/Logging.fs | 31 ++++++++-----
.../LanguageService/LanguageService.fs | 8 ++--
5 files changed, 63 insertions(+), 39 deletions(-)
diff --git a/src/Compiler/Checking/TypeRelations.fs b/src/Compiler/Checking/TypeRelations.fs
index daa5a656415..ffece895ccd 100644
--- a/src/Compiler/Checking/TypeRelations.fs
+++ b/src/Compiler/Checking/TypeRelations.fs
@@ -101,18 +101,18 @@ let TypesFeasiblyEquiv ndeep g amap m ty1 ty2 =
let TypesFeasiblyEquivStripMeasures g amap m ty1 ty2 =
TypesFeasiblyEquivalent true 0 g amap m ty1 ty2
-let inline TryGetCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key =
- if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
+let inline TryGetCachedTypeSubsumption (_g: TcGlobals) (amap: ImportMap) key =
+ //if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
match amap.TypeSubsumptionCache.TryGet(key) with
| true, subsumes ->
ValueSome subsumes
| false, _ ->
ValueNone
- else
- ValueNone
+ //else
+ // ValueNone
-let inline UpdateCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key subsumes : unit =
- if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
+let inline UpdateCachedTypeSubsumption (_g: TcGlobals) (amap: ImportMap) key subsumes : unit =
+ //if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
amap.TypeSubsumptionCache.TryAdd(key, subsumes) |> ignore
/// The feasible coercion relation. Part of the language spec.
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index 836087f2534..e86191af86f 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -90,6 +90,8 @@ type [] TTypeCacheKey =
combined
+let typeSubsumptionCache = lazy Cache.Create({ CacheOptions.Default with EvictionMethod = EvictionMethod.Background })
+
//-------------------------------------------------------------------------
// Import an IL types as F# types.
//-------------------------------------------------------------------------
@@ -106,16 +108,13 @@ type [] TTypeCacheKey =
type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) =
let typeRefToTyconRefCache = ConcurrentDictionary()
- let typeSubsumptionCache =
- Cache.Create({ CacheOptions.Default with MaximumCapacity = 1024 })
-
member _.g = g
member _.assemblyLoader = assemblyLoader
member _.ILTypeRefToTyconRefCache = typeRefToTyconRefCache
- member _.TypeSubsumptionCache = typeSubsumptionCache
+ member _.TypeSubsumptionCache = typeSubsumptionCache.Value
let CanImportILScopeRef (env: ImportMap) m scoref =
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 5e9ea27eb10..c0306b4b400 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -6,6 +6,8 @@ open System.Threading
open System.Threading.Tasks
open System.Diagnostics
+open FSharp.Compiler.Diagnostics
+
[]
// Default Seq.* function have one issue - when doing `Seq.sortBy`, it will call a `ToArray` on the collection,
// which is *not* calling `ConcurrentDictionary.ToArray`, but uses a custom one instead (treating it as `ICollection`)
@@ -56,7 +58,7 @@ type internal CacheOptions =
static member Default =
{
- MaximumCapacity = 100
+ MaximumCapacity = 500_000
PercentageToEvict = 5
Strategy = CachingStrategy.LRU
LevelOfConcurrency = Environment.ProcessorCount
@@ -84,6 +86,8 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
let cacheMiss = Event<_>()
let eviction = Event<_>()
+ let mutable maxCount = 0
+
[]
member val CacheHit = cacheHit.Publish
@@ -104,23 +108,25 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
let cts = new CancellationTokenSource()
let cache = new Cache<'Key, 'Value>(options, capacity, cts)
+ Task.Run(cache.TraceSize, cts.Token) |> ignore
+
if options.EvictionMethod = EvictionMethod.Background then
Task.Run(cache.TryEvictTask, cts.Token) |> ignore
cache
- //member this.GetStats() =
- // {|
- // Capacity = options.MaximumCapacity
- // PercentageToEvict = options.PercentageToEvict
- // Strategy = options.Strategy
- // LevelOfConcurrency = options.LevelOfConcurrency
- // Count = this.Store.Count
- // MostRecentlyAccesssed = this.Store.Values |> Seq.maxBy _.LastAccessed |> _.LastAccessed
- // LeastRecentlyAccesssed = this.Store.Values |> Seq.minBy _.LastAccessed |> _.LastAccessed
- // MostFrequentlyAccessed = this.Store.Values |> Seq.maxBy _.AccessCount |> _.AccessCount
- // LeastFrequentlyAccessed = this.Store.Values |> Seq.minBy _.AccessCount |> _.AccessCount
- // |}
+ member this.GetStats(): obj =
+ {|
+ Capacity = options.MaximumCapacity
+ PercentageToEvict = options.PercentageToEvict
+ Strategy = options.Strategy
+ LevelOfConcurrency = options.LevelOfConcurrency
+ Count = this.Store.Count
+ MostRecentlyAccesssed = this.Store.Values |> Seq.maxBy _.LastAccessed |> _.LastAccessed
+ LeastRecentlyAccesssed = this.Store.Values |> Seq.minBy _.LastAccessed |> _.LastAccessed
+ MostFrequentlyAccessed = this.Store.Values |> Seq.maxBy _.AccessCount |> _.AccessCount
+ LeastFrequentlyAccessed = this.Store.Values |> Seq.minBy _.AccessCount |> _.AccessCount
+ |}
member private this.CalculateEvictionCount() =
if this.Store.Count >= options.MaximumCapacity then
@@ -146,11 +152,21 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
| true, _ -> eviction.Trigger(key)
| _ -> () // TODO: We probably want to count eviction misses as well?
+ member private this.TraceSize() =
+ backgroundTask {
+ while not cts.Token.IsCancellationRequested do
+ if this.Store.Count > maxCount then
+ maxCount <- this.Store.Count
+ use _ = Activity.start "CacheSize" (seq { "size", string maxCount })
+ ()
+ do! Task.Delay(1000)
+ }
+
// TODO: Shall this be a safer task, wrapping everything in try .. with, so it's not crashing silently?
member private this.TryEvictTask() =
backgroundTask {
while not cts.Token.IsCancellationRequested do
- let evictionCount = this.CalculateEvictionCount()
+ let evictionCount = 0 // this.CalculateEvictionCount()
if evictionCount > 0 then
this.TryEvictItems()
diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs
index b0f56df3234..845c71cc73c 100644
--- a/vsintegration/src/FSharp.Editor/Common/Logging.fs
+++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs
@@ -135,16 +135,16 @@ module Activity =
String.replicate (loop activity 0) " "
let collectTags (activity: Activity) =
- [ for tag in activity.Tags -> $"{tag.Key}: %A{tag.Value}" ]
+ [ for tag in activity.Tags -> $"{tag.Key}: {tag.Value}" ]
|> String.concat ", "
let listener =
new ActivityListener(
- ShouldListenTo = (fun source -> source.Name = FSharp.Compiler.Diagnostics.ActivityNames.FscSourceName),
+ ShouldListenTo = (fun source -> source.Name = ActivityNames.FscSourceName),
Sample =
(fun context ->
if context.Name.Contains(filter) then
- ActivitySamplingResult.AllDataAndRecorded
+ ActivitySamplingResult.AllData
else
ActivitySamplingResult.None),
ActivityStarted = (fun a -> logMsg $"{indent a}{a.OperationName} {collectTags a}")
@@ -152,13 +152,24 @@ module Activity =
ActivitySource.AddActivityListener(listener)
- let export () =
- OpenTelemetry.Sdk
- .CreateTracerProviderBuilder()
- .AddSource(ActivityNames.FscSourceName)
- .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName = "F#", serviceVersion = "1.0.0"))
- .AddOtlpExporter()
- .Build()
+ let exportTraces() =
+ let provider =
+ // Configure OpenTelemetry export. Traces can be viewed in Jaeger or other compatible tools.
+ OpenTelemetry.Sdk.CreateTracerProviderBuilder()
+ .AddSource(ActivityNames.FscSourceName)
+ .ConfigureResource(fun r -> r.AddService("F#") |> ignore)
+ .AddOtlpExporter(fun o ->
+ // Empirical values to ensure no traces are lost and no significant delay at the end of test run.
+ o.TimeoutMilliseconds <- 200
+ o.BatchExportProcessorOptions.MaxQueueSize <- 16384
+ o.BatchExportProcessorOptions.ScheduledDelayMilliseconds <- 100
+ )
+ .Build()
+ let a = Activity.startNoTags "FSharpPackage"
+ fun () ->
+ a.Dispose()
+ provider.ForceFlush(5000) |> ignore
+ provider.Dispose()
let listenToAll () = listen ""
#endif
diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
index 5cc9cec2943..30f62e2a4df 100644
--- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
+++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
@@ -340,11 +340,9 @@ type internal FSharpPackage() as this =
let mutable solutionEventsOpt = None
-#if DEBUG
- let _traceProvider = Logging.Activity.export ()
- let _logger = Logging.Activity.listenToAll ()
- // Logging.Activity.listen "IncrementalBuild"
-#endif
+ #if DEBUG
+ do Logging.Activity.listen "CacheSize"
+ #endif
// FSI-LINKAGE-POINT: unsited init
do FSharp.Interactive.Hooks.fsiConsoleWindowPackageCtorUnsited (this :> Package)
From ffd764df9ee8428b68dc14e589cd411f30dc4c5e Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Sat, 12 Apr 2025 12:23:54 +0200
Subject: [PATCH 06/44] show count in release config too
---
vsintegration/src/FSharp.Editor/Common/Logging.fs | 9 ++++-----
.../src/FSharp.Editor/LanguageService/LanguageService.fs | 2 --
2 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs
index 845c71cc73c..65e9f2d12b3 100644
--- a/vsintegration/src/FSharp.Editor/Common/Logging.fs
+++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs
@@ -118,11 +118,8 @@ module Logging =
let logExceptionWithContext (ex: Exception, context) =
logErrorf "Context: %s\nException Message: %s\nStack Trace: %s" context ex.Message ex.StackTrace
-#if DEBUG
-module Activity =
- open OpenTelemetry.Resources
- open OpenTelemetry.Trace
+module Activity =
let listen filter =
let indent (activity: Activity) =
@@ -151,7 +148,9 @@ module Activity =
)
ActivitySource.AddActivityListener(listener)
-
+#if DEBUG
+ open OpenTelemetry.Resources
+ open OpenTelemetry.Trace
let exportTraces() =
let provider =
// Configure OpenTelemetry export. Traces can be viewed in Jaeger or other compatible tools.
diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
index 30f62e2a4df..fb4508214d9 100644
--- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
+++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
@@ -340,9 +340,7 @@ type internal FSharpPackage() as this =
let mutable solutionEventsOpt = None
- #if DEBUG
do Logging.Activity.listen "CacheSize"
- #endif
// FSI-LINKAGE-POINT: unsited init
do FSharp.Interactive.Hooks.fsiConsoleWindowPackageCtorUnsited (this :> Package)
From 5f2c535e617b98c48d902a812416bb1a7428a592 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Sat, 12 Apr 2025 19:18:04 +0200
Subject: [PATCH 07/44] tune cache
---
src/Compiler/Checking/import.fs | 6 ++---
src/Compiler/Checking/import.fsi | 2 +-
src/Compiler/Driver/CompilerImports.fs | 8 ++++++-
src/Compiler/Utilities/Caches.fs | 24 +++++++------------
.../LanguageService/LanguageService.fs | 2 +-
5 files changed, 20 insertions(+), 22 deletions(-)
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index e86191af86f..f20164e97f2 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -90,8 +90,6 @@ type [] TTypeCacheKey =
combined
-let typeSubsumptionCache = lazy Cache.Create({ CacheOptions.Default with EvictionMethod = EvictionMethod.Background })
-
//-------------------------------------------------------------------------
// Import an IL types as F# types.
//-------------------------------------------------------------------------
@@ -105,7 +103,7 @@ let typeSubsumptionCache = lazy Cache.Create({ CacheOptions
/// using tcImports.GetImportMap() if needed, and it is not harmful if multiple instances are used. The object
/// serves as an interface through to the tables stored in the primary TcImports structures defined in CompileOps.fs.
[]
-type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) =
+type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader, typeSubsumptionCache: Cache) =
let typeRefToTyconRefCache = ConcurrentDictionary()
member _.g = g
@@ -114,7 +112,7 @@ type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) =
member _.ILTypeRefToTyconRefCache = typeRefToTyconRefCache
- member _.TypeSubsumptionCache = typeSubsumptionCache.Value
+ member _.TypeSubsumptionCache = typeSubsumptionCache
let CanImportILScopeRef (env: ImportMap) m scoref =
diff --git a/src/Compiler/Checking/import.fsi b/src/Compiler/Checking/import.fsi
index 043692ac41c..3a7bd6e8437 100644
--- a/src/Compiler/Checking/import.fsi
+++ b/src/Compiler/Checking/import.fsi
@@ -64,7 +64,7 @@ type TTypeCacheKey =
/// serves as an interface through to the tables stored in the primary TcImports structures defined in CompileOps.fs.
[]
type ImportMap =
- new: g: TcGlobals * assemblyLoader: AssemblyLoader -> ImportMap
+ new: g: TcGlobals * assemblyLoader: AssemblyLoader * typeSubsumptionCache: Cache -> ImportMap
/// The AssemblyLoader for the import context
member assemblyLoader: AssemblyLoader
diff --git a/src/Compiler/Driver/CompilerImports.fs b/src/Compiler/Driver/CompilerImports.fs
index 4ab1ca3d7e4..afb067ae6c9 100644
--- a/src/Compiler/Driver/CompilerImports.fs
+++ b/src/Compiler/Driver/CompilerImports.fs
@@ -1305,6 +1305,12 @@ and [] TcImports
| None -> false
| None -> false
+ let typeSubsumptionCache = lazy Cache.Create({
+ CacheOptions.Default with
+ EvictionMethod = EvictionMethod.Background
+ PercentageToEvict = 20
+ MaximumCapacity = 200_000 })
+
member internal _.Base =
CheckDisposed()
importsBase
@@ -1704,7 +1710,7 @@ and [] TcImports
member _.RecordGeneratedTypeRoot root = tcImports.RecordGeneratedTypeRoot root
}
#endif
- ImportMap(tcImports.GetTcGlobals(), loaderInterface)
+ ImportMap(tcImports.GetTcGlobals(), loaderInterface, typeSubsumptionCache.Value)
// Note the tcGlobals are only available once mscorlib and fslib have been established. For TcImports,
// they are logically only needed when converting AbsIL data structures into F# data structures, and
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index c0306b4b400..5203b15b954 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -58,7 +58,7 @@ type internal CacheOptions =
static member Default =
{
- MaximumCapacity = 500_000
+ MaximumCapacity = 10_000
PercentageToEvict = 5
Strategy = CachingStrategy.LRU
LevelOfConcurrency = Environment.ProcessorCount
@@ -86,7 +86,7 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
let cacheMiss = Event<_>()
let eviction = Event<_>()
- let mutable maxCount = 0
+ let mutable currentCapacity = capacity
[]
member val CacheHit = cacheHit.Publish
@@ -105,11 +105,11 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
options.MaximumCapacity
+ (options.MaximumCapacity * options.PercentageToEvict / 100)
+ use _ = Activity.start "Cache.Created" (seq { "capacity", string capacity })
+
let cts = new CancellationTokenSource()
let cache = new Cache<'Key, 'Value>(options, capacity, cts)
- Task.Run(cache.TraceSize, cts.Token) |> ignore
-
if options.EvictionMethod = EvictionMethod.Background then
Task.Run(cache.TryEvictTask, cts.Token) |> ignore
@@ -152,23 +152,17 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
| true, _ -> eviction.Trigger(key)
| _ -> () // TODO: We probably want to count eviction misses as well?
- member private this.TraceSize() =
- backgroundTask {
- while not cts.Token.IsCancellationRequested do
- if this.Store.Count > maxCount then
- maxCount <- this.Store.Count
- use _ = Activity.start "CacheSize" (seq { "size", string maxCount })
- ()
- do! Task.Delay(1000)
- }
-
// TODO: Shall this be a safer task, wrapping everything in try .. with, so it's not crashing silently?
member private this.TryEvictTask() =
backgroundTask {
while not cts.Token.IsCancellationRequested do
- let evictionCount = 0 // this.CalculateEvictionCount()
+ let evictionCount = this.CalculateEvictionCount()
if evictionCount > 0 then
+ let exceeded = this.Store.Count > currentCapacity
+ if exceeded then
+ currentCapacity <- this.Store.Count
+ use _ = Activity.start "Cache.Eviction" (seq { yield "Store.Count", string this.Store.Count; if exceeded then yield "RESIZE", "!" })
this.TryEvictItems()
let utilization = (this.Store.Count / options.MaximumCapacity)
diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
index fb4508214d9..f8a3d3e0e32 100644
--- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
+++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
@@ -340,7 +340,7 @@ type internal FSharpPackage() as this =
let mutable solutionEventsOpt = None
- do Logging.Activity.listen "CacheSize"
+ do Logging.Activity.listen "Cache"
// FSI-LINKAGE-POINT: unsited init
do FSharp.Interactive.Hooks.fsiConsoleWindowPackageCtorUnsited (this :> Package)
From a7d4605f30239f9190c67a2898f71fc7585a4a84 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Sat, 12 Apr 2025 19:41:49 +0200
Subject: [PATCH 08/44] fantomas
---
src/Compiler/Driver/CompilerImports.fs | 15 ++++++++++-----
src/Compiler/Utilities/Caches.fs | 15 +++++++++++++--
vsintegration/src/FSharp.Editor/Common/Logging.fs | 15 ++++++++-------
3 files changed, 31 insertions(+), 14 deletions(-)
diff --git a/src/Compiler/Driver/CompilerImports.fs b/src/Compiler/Driver/CompilerImports.fs
index afb067ae6c9..12418ad36cb 100644
--- a/src/Compiler/Driver/CompilerImports.fs
+++ b/src/Compiler/Driver/CompilerImports.fs
@@ -1305,11 +1305,16 @@ and [] TcImports
| None -> false
| None -> false
- let typeSubsumptionCache = lazy Cache.Create({
- CacheOptions.Default with
- EvictionMethod = EvictionMethod.Background
- PercentageToEvict = 20
- MaximumCapacity = 200_000 })
+ let typeSubsumptionCache =
+ lazy
+ Cache
+ .Create(
+ { CacheOptions.Default with
+ EvictionMethod = EvictionMethod.Background
+ PercentageToEvict = 20
+ MaximumCapacity = 200_000
+ }
+ )
member internal _.Base =
CheckDisposed()
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 5203b15b954..c2f62538e16 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -115,7 +115,7 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
cache
- member this.GetStats(): obj =
+ member this.GetStats() : obj =
{|
Capacity = options.MaximumCapacity
PercentageToEvict = options.PercentageToEvict
@@ -160,9 +160,20 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
if evictionCount > 0 then
let exceeded = this.Store.Count > currentCapacity
+
if exceeded then
currentCapacity <- this.Store.Count
- use _ = Activity.start "Cache.Eviction" (seq { yield "Store.Count", string this.Store.Count; if exceeded then yield "RESIZE", "!" })
+
+ use _ =
+ Activity.start
+ "Cache.Eviction"
+ (seq {
+ yield "Store.Count", string this.Store.Count
+
+ if exceeded then
+ yield "RESIZE", "!"
+ })
+
this.TryEvictItems()
let utilization = (this.Store.Count / options.MaximumCapacity)
diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs
index 65e9f2d12b3..e978d1e19d9 100644
--- a/vsintegration/src/FSharp.Editor/Common/Logging.fs
+++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs
@@ -118,7 +118,6 @@ module Logging =
let logExceptionWithContext (ex: Exception, context) =
logErrorf "Context: %s\nException Message: %s\nStack Trace: %s" context ex.Message ex.StackTrace
-
module Activity =
let listen filter =
@@ -132,8 +131,7 @@ module Activity =
String.replicate (loop activity 0) " "
let collectTags (activity: Activity) =
- [ for tag in activity.Tags -> $"{tag.Key}: {tag.Value}" ]
- |> String.concat ", "
+ [ for tag in activity.Tags -> $"{tag.Key}: {tag.Value}" ] |> String.concat ", "
let listener =
new ActivityListener(
@@ -151,20 +149,23 @@ module Activity =
#if DEBUG
open OpenTelemetry.Resources
open OpenTelemetry.Trace
- let exportTraces() =
+
+ let exportTraces () =
let provider =
// Configure OpenTelemetry export. Traces can be viewed in Jaeger or other compatible tools.
- OpenTelemetry.Sdk.CreateTracerProviderBuilder()
+ OpenTelemetry.Sdk
+ .CreateTracerProviderBuilder()
.AddSource(ActivityNames.FscSourceName)
.ConfigureResource(fun r -> r.AddService("F#") |> ignore)
.AddOtlpExporter(fun o ->
// Empirical values to ensure no traces are lost and no significant delay at the end of test run.
o.TimeoutMilliseconds <- 200
o.BatchExportProcessorOptions.MaxQueueSize <- 16384
- o.BatchExportProcessorOptions.ScheduledDelayMilliseconds <- 100
- )
+ o.BatchExportProcessorOptions.ScheduledDelayMilliseconds <- 100)
.Build()
+
let a = Activity.startNoTags "FSharpPackage"
+
fun () ->
a.Dispose()
provider.ForceFlush(5000) |> ignore
From 7d9746a3b57a7c2066bab8496e22e5cb236164f9 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Sat, 12 Apr 2025 20:09:11 +0200
Subject: [PATCH 09/44] ilver
---
tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl | 1 +
.../ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl | 1 +
.../ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl | 1 +
3 files changed, 3 insertions(+)
diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl
index 69842b9e059..219a0ec11db 100644
--- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl
+++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl
@@ -5,6 +5,7 @@
[IL]: Error [UnmanagedPointer]: : FSharp.Compiler.IO.RawByteMemory::.ctor(uint8*, int32, object)][offset 0x00000009] Unmanaged pointers are not a verifiable type.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::get_Item(int32)][offset 0x0000001E][found Native Int] Expected ByRef on the stack.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::set_Item(int32, uint8)][offset 0x00000025][found Native Int] Expected ByRef on the stack.
+[IL]: Error [StackUnexpected]: : FSharp.Compiler.Cache`2::TryGetPickToEvict()][offset 0x0000005A][found ref 'object'][expected ref '[S.P.CoreLib]System.Collections.Generic.IEnumerable`1>>'] Unexpected type on the stack.
[IL]: Error [ReturnPtrToStack]: : Internal.Utilities.Text.Lexing.LexBuffer`1::get_LexemeView()][offset 0x00000019] Return type is ByRef, TypedReference, ArgHandle, or ArgIterator.
[IL]: Error [StackUnexpected]: : Internal.Utilities.Text.Lexing.UnicodeTables::scanUntilSentinel([FSharp.Compiler.Service]Internal.Utilities.Text.Lexing.LexBuffer`1, int32)][offset 0x00000079][found Short] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Xml.XmlDoc::processLines([FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1)][offset 0x00000031][found Char] Unexpected type on the stack.
diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl
index 6e41547cd11..209cabb338b 100644
--- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl
+++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl
@@ -5,6 +5,7 @@
[IL]: Error [UnmanagedPointer]: : FSharp.Compiler.IO.RawByteMemory::.ctor(uint8*, int32, object)][offset 0x00000009] Unmanaged pointers are not a verifiable type.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::get_Item(int32)][offset 0x0000001E][found Native Int] Expected ByRef on the stack.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::set_Item(int32, uint8)][offset 0x00000025][found Native Int] Expected ByRef on the stack.
+[IL]: Error [StackUnexpected]: : FSharp.Compiler.Cache`2::TryGetPickToEvict()][offset 0x0000005A][found ref 'object'][expected ref '[S.P.CoreLib]System.Collections.Generic.IEnumerable`1>>'] Unexpected type on the stack.
[IL]: Error [ReturnPtrToStack]: : Internal.Utilities.Text.Lexing.LexBuffer`1::get_LexemeView()][offset 0x00000019] Return type is ByRef, TypedReference, ArgHandle, or ArgIterator.
[IL]: Error [StackUnexpected]: : Internal.Utilities.Text.Lexing.UnicodeTables::scanUntilSentinel([FSharp.Compiler.Service]Internal.Utilities.Text.Lexing.LexBuffer`1, int32)][offset 0x00000079][found Short] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Xml.XmlDoc::processLines([FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1)][offset 0x00000031][found Char] Unexpected type on the stack.
diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl
index 431d4e5512a..8d926f8c113 100644
--- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl
+++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl
@@ -5,6 +5,7 @@
[IL]: Error [UnmanagedPointer]: : FSharp.Compiler.IO.RawByteMemory::.ctor(uint8*, int32, object)][offset 0x00000009] Unmanaged pointers are not a verifiable type.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::get_Item(int32)][offset 0x0000001A][found Native Int] Expected ByRef on the stack.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::set_Item(int32, uint8)][offset 0x0000001B][found Native Int] Expected ByRef on the stack.
+[IL]: Error [StackUnexpected]: : FSharp.Compiler.Cache`2::TryGetPickToEvict()][offset 0x00000034][found ref 'object'][expected ref '[S.P.CoreLib]System.Collections.Generic.IEnumerable`1>>'] Unexpected type on the stack.
[IL]: Error [ReturnPtrToStack]: : Internal.Utilities.Text.Lexing.LexBuffer`1::get_LexemeView()][offset 0x00000017] Return type is ByRef, TypedReference, ArgHandle, or ArgIterator.
[IL]: Error [StackUnexpected]: : Internal.Utilities.Text.Lexing.UnicodeTables::scanUntilSentinel([FSharp.Compiler.Service]Internal.Utilities.Text.Lexing.LexBuffer`1, int32)][offset 0x0000008D][found Short] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Xml.XmlDoc::processLines([FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1)][offset 0x0000002C][found Char] Unexpected type on the stack.
From 238a92aa359d104b37e64027abcf8327c9cea823 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Sun, 13 Apr 2025 10:14:21 +0200
Subject: [PATCH 10/44] fix sa again
---
src/Compiler/Utilities/Caches.fs | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index c2f62538e16..5b436fb1bee 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -115,18 +115,18 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
cache
- member this.GetStats() : obj =
- {|
- Capacity = options.MaximumCapacity
- PercentageToEvict = options.PercentageToEvict
- Strategy = options.Strategy
- LevelOfConcurrency = options.LevelOfConcurrency
- Count = this.Store.Count
- MostRecentlyAccesssed = this.Store.Values |> Seq.maxBy _.LastAccessed |> _.LastAccessed
- LeastRecentlyAccesssed = this.Store.Values |> Seq.minBy _.LastAccessed |> _.LastAccessed
- MostFrequentlyAccessed = this.Store.Values |> Seq.maxBy _.AccessCount |> _.AccessCount
- LeastFrequentlyAccessed = this.Store.Values |> Seq.minBy _.AccessCount |> _.AccessCount
- |}
+ //member this.GetStats() =
+ // {|
+ // Capacity = options.MaximumCapacity
+ // PercentageToEvict = options.PercentageToEvict
+ // Strategy = options.Strategy
+ // LevelOfConcurrency = options.LevelOfConcurrency
+ // Count = this.Store.Count
+ // MostRecentlyAccesssed = this.Store.Values |> Seq.maxBy _.LastAccessed |> _.LastAccessed
+ // LeastRecentlyAccesssed = this.Store.Values |> Seq.minBy _.LastAccessed |> _.LastAccessed
+ // MostFrequentlyAccessed = this.Store.Values |> Seq.maxBy _.AccessCount |> _.AccessCount
+ // LeastFrequentlyAccessed = this.Store.Values |> Seq.minBy _.AccessCount |> _.AccessCount
+ // |}
member private this.CalculateEvictionCount() =
if this.Store.Count >= options.MaximumCapacity then
From 1d21c78818d2074e6a02cb6adc211a4308e51352 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Sun, 13 Apr 2025 10:15:56 +0200
Subject: [PATCH 11/44] just CWT for now
decide where to attach the cache later
---
src/Compiler/Checking/import.fs | 20 ++++++++++++++++++--
src/Compiler/Checking/import.fsi | 2 +-
src/Compiler/Driver/CompilerImports.fs | 15 ++-------------
3 files changed, 21 insertions(+), 16 deletions(-)
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index f20164e97f2..775564e3817 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -6,17 +6,20 @@ module internal FSharp.Compiler.Import
open System.Collections.Concurrent
open System.Collections.Generic
open System.Collections.Immutable
-open FSharp.Compiler.Text.Range
+open System.Runtime.CompilerServices
+
open Internal.Utilities.Library
open Internal.Utilities.Library.Extras
open Internal.Utilities.TypeHashing
open Internal.Utilities.TypeHashing.HashTypes
+
open FSharp.Compiler
open FSharp.Compiler.AbstractIL.IL
open FSharp.Compiler.CompilerGlobalState
open FSharp.Compiler.DiagnosticsLogger
open FSharp.Compiler.SyntaxTreeOps
open FSharp.Compiler.Text
+open FSharp.Compiler.Text.Range
open FSharp.Compiler.Xml
open FSharp.Compiler.TypedTree
open FSharp.Compiler.TypedTreeBasics
@@ -90,6 +93,8 @@ type [] TTypeCacheKey =
combined
+let typeSubsumptionCaches = ConditionalWeakTable<_, Cache>()
+
//-------------------------------------------------------------------------
// Import an IL types as F# types.
//-------------------------------------------------------------------------
@@ -103,9 +108,20 @@ type [] TTypeCacheKey =
/// using tcImports.GetImportMap() if needed, and it is not harmful if multiple instances are used. The object
/// serves as an interface through to the tables stored in the primary TcImports structures defined in CompileOps.fs.
[]
-type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader, typeSubsumptionCache: Cache) =
+type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) =
let typeRefToTyconRefCache = ConcurrentDictionary()
+ let typeSubsumptionCache =
+ typeSubsumptionCaches.GetValue(g, fun _ ->
+ Cache.Create(
+ { CacheOptions.Default with
+ EvictionMethod = EvictionMethod.Background
+ PercentageToEvict = 20
+ MaximumCapacity = 200_000
+ }
+ )
+ )
+
member _.g = g
member _.assemblyLoader = assemblyLoader
diff --git a/src/Compiler/Checking/import.fsi b/src/Compiler/Checking/import.fsi
index 3a7bd6e8437..043692ac41c 100644
--- a/src/Compiler/Checking/import.fsi
+++ b/src/Compiler/Checking/import.fsi
@@ -64,7 +64,7 @@ type TTypeCacheKey =
/// serves as an interface through to the tables stored in the primary TcImports structures defined in CompileOps.fs.
[]
type ImportMap =
- new: g: TcGlobals * assemblyLoader: AssemblyLoader * typeSubsumptionCache: Cache -> ImportMap
+ new: g: TcGlobals * assemblyLoader: AssemblyLoader -> ImportMap
/// The AssemblyLoader for the import context
member assemblyLoader: AssemblyLoader
diff --git a/src/Compiler/Driver/CompilerImports.fs b/src/Compiler/Driver/CompilerImports.fs
index 12418ad36cb..41cffb15506 100644
--- a/src/Compiler/Driver/CompilerImports.fs
+++ b/src/Compiler/Driver/CompilerImports.fs
@@ -1304,18 +1304,7 @@ and [] TcImports
true
| None -> false
| None -> false
-
- let typeSubsumptionCache =
- lazy
- Cache
- .Create(
- { CacheOptions.Default with
- EvictionMethod = EvictionMethod.Background
- PercentageToEvict = 20
- MaximumCapacity = 200_000
- }
- )
-
+
member internal _.Base =
CheckDisposed()
importsBase
@@ -1715,7 +1704,7 @@ and [] TcImports
member _.RecordGeneratedTypeRoot root = tcImports.RecordGeneratedTypeRoot root
}
#endif
- ImportMap(tcImports.GetTcGlobals(), loaderInterface, typeSubsumptionCache.Value)
+ ImportMap(tcImports.GetTcGlobals(), loaderInterface)
// Note the tcGlobals are only available once mscorlib and fslib have been established. For TcImports,
// they are logically only needed when converting AbsIL data structures into F# data structures, and
From 03dc52b2851560a16c78db0dd1c0be49b31052de Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Sun, 13 Apr 2025 11:03:30 +0200
Subject: [PATCH 12/44] add some ids to see whats what
---
src/Compiler/Utilities/Caches.fs | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 5b436fb1bee..2abf1b5a357 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -80,12 +80,14 @@ type internal CachedEntity<'Value> =
[]
[]
-type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts) =
+type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts, name: string) =
let cacheHit = Event<_ * _>()
let cacheMiss = Event<_>()
let eviction = Event<_>()
+ static let mutable cacheId = 0
+
let mutable currentCapacity = capacity
[]
@@ -105,10 +107,12 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
options.MaximumCapacity
+ (options.MaximumCapacity * options.PercentageToEvict / 100)
- use _ = Activity.start "Cache.Created" (seq { "capacity", string capacity })
+ let name = $"Cache{Interlocked.Increment &cacheId}"
+
+ use _ = Activity.start "Cache.Created" (seq {"name", name; "capacity", string capacity })
let cts = new CancellationTokenSource()
- let cache = new Cache<'Key, 'Value>(options, capacity, cts)
+ let cache = new Cache<'Key, 'Value>(options, capacity, cts, name)
if options.EvictionMethod = EvictionMethod.Background then
Task.Run(cache.TryEvictTask, cts.Token) |> ignore
@@ -168,6 +172,7 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
Activity.start
"Cache.Eviction"
(seq {
+ yield "name", name
yield "Store.Count", string this.Store.Count
if exceeded then
From d524663bfc885f78a599d167be2f8a6457b9819b Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Sun, 13 Apr 2025 12:51:18 +0200
Subject: [PATCH 13/44] for some reason it didnt work
---
src/Compiler/Checking/import.fs | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index 775564e3817..28f9bc1b1d4 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -93,7 +93,18 @@ type [] TTypeCacheKey =
combined
-let typeSubsumptionCaches = ConditionalWeakTable<_, Cache>()
+//let typeSubsumptionCaches = ConditionalWeakTable<_, Cache>()
+
+let typeSubsumptionCache =
+ //typeSubsumptionCaches.GetValue(g, fun _ ->
+ Cache.Create(
+ { CacheOptions.Default with
+ EvictionMethod = EvictionMethod.Background
+ PercentageToEvict = 15
+ MaximumCapacity = 500_000
+ }
+ )
+ //)
//-------------------------------------------------------------------------
// Import an IL types as F# types.
@@ -111,17 +122,6 @@ let typeSubsumptionCaches = ConditionalWeakTable<_, Cache>(
type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) =
let typeRefToTyconRefCache = ConcurrentDictionary()
- let typeSubsumptionCache =
- typeSubsumptionCaches.GetValue(g, fun _ ->
- Cache.Create(
- { CacheOptions.Default with
- EvictionMethod = EvictionMethod.Background
- PercentageToEvict = 20
- MaximumCapacity = 200_000
- }
- )
- )
-
member _.g = g
member _.assemblyLoader = assemblyLoader
From e130e011141e0a8d52a78757d78466ce4b56a723 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Mon, 14 Apr 2025 12:10:10 +0200
Subject: [PATCH 14/44] metrics
---
src/Compiler/Utilities/Caches.fs | 38 +++++-----------
.../src/FSharp.Editor/Common/Logging.fs | 44 ++++++++++++++-----
.../LanguageService/LanguageService.fs | 12 ++++-
3 files changed, 53 insertions(+), 41 deletions(-)
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 2abf1b5a357..9a35507a6a4 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -5,8 +5,7 @@ open System.Collections.Concurrent
open System.Threading
open System.Threading.Tasks
open System.Diagnostics
-
-open FSharp.Compiler.Diagnostics
+open System.Diagnostics.Metrics
[]
// Default Seq.* function have one issue - when doing `Seq.sortBy`, it will call a `ToArray` on the collection,
@@ -78,18 +77,19 @@ type internal CachedEntity<'Value> =
AccessCount = 0L
}
+module internal CacheMetrics =
+ let mutable cacheId = 0
+ let meter = new Meter("FSharp.Compiler.Caches")
+ // let _count = meter.CreateObservableCounter("Count", (fun _ -> cache.Store.Count))
+
[]
[]
-type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts, name: string) =
+type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts) =
let cacheHit = Event<_ * _>()
let cacheMiss = Event<_>()
let eviction = Event<_>()
- static let mutable cacheId = 0
-
- let mutable currentCapacity = capacity
-
[]
member val CacheHit = cacheHit.Publish
@@ -107,12 +107,10 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts,
options.MaximumCapacity
+ (options.MaximumCapacity * options.PercentageToEvict / 100)
- let name = $"Cache{Interlocked.Increment &cacheId}"
-
- use _ = Activity.start "Cache.Created" (seq {"name", name; "capacity", string capacity })
-
let cts = new CancellationTokenSource()
- let cache = new Cache<'Key, 'Value>(options, capacity, cts, name)
+ let cache = new Cache<'Key, 'Value>(options, capacity, cts)
+
+ CacheMetrics.meter.CreateObservableGauge($"count-{Interlocked.Increment &CacheMetrics.cacheId}", (fun () -> cache.Store.Count)) |> ignore
if options.EvictionMethod = EvictionMethod.Background then
Task.Run(cache.TryEvictTask, cts.Token) |> ignore
@@ -163,22 +161,6 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts,
let evictionCount = this.CalculateEvictionCount()
if evictionCount > 0 then
- let exceeded = this.Store.Count > currentCapacity
-
- if exceeded then
- currentCapacity <- this.Store.Count
-
- use _ =
- Activity.start
- "Cache.Eviction"
- (seq {
- yield "name", name
- yield "Store.Count", string this.Store.Count
-
- if exceeded then
- yield "RESIZE", "!"
- })
-
this.TryEvictItems()
let utilization = (this.Store.Count / options.MaximumCapacity)
diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs
index e978d1e19d9..6bc530652fe 100644
--- a/vsintegration/src/FSharp.Editor/Common/Logging.fs
+++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs
@@ -30,6 +30,7 @@ module Config =
let fsharpOutputGuid = Guid fsharpOutputGuidString
open Config
+open System.Diagnostics.Metrics
[]
type Logger [] ([)>] serviceProvider: IServiceProvider) =
@@ -118,7 +119,7 @@ module Logging =
let logExceptionWithContext (ex: Exception, context) =
logErrorf "Context: %s\nException Message: %s\nStack Trace: %s" context ex.Message ex.StackTrace
-module Activity =
+module FSharpServiceTelemetry =
let listen filter =
let indent (activity: Activity) =
@@ -146,30 +147,51 @@ module Activity =
)
ActivitySource.AddActivityListener(listener)
+
+ let logCacheMetricsToOutput () =
+ let listener = new MeterListener(
+ InstrumentPublished = fun instrument l ->
+ if instrument.Meter.Name = "FSharp.Compiler.Caches" then
+ l.EnableMeasurementEvents(instrument)
+ )
+ let callBack = MeasurementCallback(fun instr v _ _ -> logMsg $"{instr.Name}: {v}")
+ listener.SetMeasurementEventCallback callBack
+ listener.Start()
+
+ backgroundTask {
+ while true do
+ do! System.Threading.Tasks.Task.Delay(1000)
+ listener.RecordObservableInstruments()
+ } |> ignore
+
#if DEBUG
open OpenTelemetry.Resources
open OpenTelemetry.Trace
+ open OpenTelemetry.Metrics
- let exportTraces () =
- let provider =
+ let export () =
+ let meterProvider =
+ // Configure OpenTelemetry metrics. Metrics can be viewed in Prometheus or other compatible tools.
+ OpenTelemetry.Sdk
+ .CreateMeterProviderBuilder()
+ .AddOtlpExporter()
+ .Build()
+ let tracerProvider =
// Configure OpenTelemetry export. Traces can be viewed in Jaeger or other compatible tools.
OpenTelemetry.Sdk
.CreateTracerProviderBuilder()
.AddSource(ActivityNames.FscSourceName)
.ConfigureResource(fun r -> r.AddService("F#") |> ignore)
- .AddOtlpExporter(fun o ->
- // Empirical values to ensure no traces are lost and no significant delay at the end of test run.
- o.TimeoutMilliseconds <- 200
- o.BatchExportProcessorOptions.MaxQueueSize <- 16384
- o.BatchExportProcessorOptions.ScheduledDelayMilliseconds <- 100)
+ .AddOtlpExporter()
.Build()
let a = Activity.startNoTags "FSharpPackage"
fun () ->
a.Dispose()
- provider.ForceFlush(5000) |> ignore
- provider.Dispose()
+ tracerProvider.ForceFlush(5000) |> ignore
+ tracerProvider.Dispose()
+ meterProvider.Dispose()
let listenToAll () = listen ""
-#endif
+#endif
\ No newline at end of file
diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
index f8a3d3e0e32..738cf2f6135 100644
--- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
+++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
@@ -340,11 +340,19 @@ type internal FSharpPackage() as this =
let mutable solutionEventsOpt = None
- do Logging.Activity.listen "Cache"
-
// FSI-LINKAGE-POINT: unsited init
do FSharp.Interactive.Hooks.fsiConsoleWindowPackageCtorUnsited (this :> Package)
+ do Logging.FSharpServiceTelemetry.logCacheMetricsToOutput()
+
+ #if DEBUG
+ let flushTelemetry = Logging.FSharpServiceTelemetry.export()
+
+ override this.Dispose (disposing: bool) =
+ base.Dispose(disposing: bool)
+ if disposing then flushTelemetry()
+ #endif
+
override this.InitializeAsync(cancellationToken: CancellationToken, progress: IProgress) : Tasks.Task =
// `base.` methods can't be called in the `async` builder, so we have to cache it
let baseInitializeAsync = base.InitializeAsync(cancellationToken, progress)
From 47b4165af177ccb61ae5232143c5cd04f22f7f28 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Mon, 14 Apr 2025 12:33:56 +0200
Subject: [PATCH 15/44] metrics
---
vsintegration/src/FSharp.Editor/Common/Logging.fs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs
index 6bc530652fe..74085e7288e 100644
--- a/vsintegration/src/FSharp.Editor/Common/Logging.fs
+++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs
@@ -154,10 +154,15 @@ module FSharpServiceTelemetry =
if instrument.Meter.Name = "FSharp.Compiler.Caches" then
l.EnableMeasurementEvents(instrument)
)
- let callBack = MeasurementCallback(fun instr v _ _ -> logMsg $"{instr.Name}: {v}")
+
+ let msg = Event()
+
+ let callBack = MeasurementCallback(fun instr v _ _ -> msg.Trigger $"{instr.Name}: {v}")
listener.SetMeasurementEventCallback callBack
listener.Start()
+ msg.Publish |> Event.pairwise |> Event.filter (fun (x, y) -> x <> y) |> Event.map snd |> Event.add logMsg
+
backgroundTask {
while true do
do! System.Threading.Tasks.Task.Delay(1000)
From 90eaa02173bdc668174d4e03540c4c72524ca5bc Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Mon, 14 Apr 2025 13:30:19 +0200
Subject: [PATCH 16/44] one cache instance per TcGlobals
---
src/Compiler/Checking/import.fs | 27 ++++++++++---------
src/Compiler/Utilities/Caches.fs | 3 +--
.../src/FSharp.Editor/Common/Logging.fs | 21 ++++++++++-----
3 files changed, 29 insertions(+), 22 deletions(-)
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index 28f9bc1b1d4..24d2c313150 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -93,18 +93,7 @@ type [] TTypeCacheKey =
combined
-//let typeSubsumptionCaches = ConditionalWeakTable<_, Cache>()
-
-let typeSubsumptionCache =
- //typeSubsumptionCaches.GetValue(g, fun _ ->
- Cache.Create(
- { CacheOptions.Default with
- EvictionMethod = EvictionMethod.Background
- PercentageToEvict = 15
- MaximumCapacity = 500_000
- }
- )
- //)
+let typeSubsumptionCaches = ConditionalWeakTable<_, Cache>()
//-------------------------------------------------------------------------
// Import an IL types as F# types.
@@ -122,13 +111,25 @@ let typeSubsumptionCache =
type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) =
let typeRefToTyconRefCache = ConcurrentDictionary()
+ let typeSubsumptionCache =
+ lazy
+ typeSubsumptionCaches.GetValue(g, fun g ->
+ Cache.Create(
+ { CacheOptions.Default with
+ // EvictionMethod = EvictionMethod.Background
+ PercentageToEvict = 15
+ MaximumCapacity = if g.compilationMode = CompilationMode.OneOff then System.Int32.MaxValue else 500_000
+ }
+ )
+ )
+
member _.g = g
member _.assemblyLoader = assemblyLoader
member _.ILTypeRefToTyconRefCache = typeRefToTyconRefCache
- member _.TypeSubsumptionCache = typeSubsumptionCache
+ member _.TypeSubsumptionCache = typeSubsumptionCache.Value
let CanImportILScopeRef (env: ImportMap) m scoref =
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 9a35507a6a4..7e5024ca8ca 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -80,7 +80,6 @@ type internal CachedEntity<'Value> =
module internal CacheMetrics =
let mutable cacheId = 0
let meter = new Meter("FSharp.Compiler.Caches")
- // let _count = meter.CreateObservableCounter("Count", (fun _ -> cache.Store.Count))
[]
[]
@@ -110,7 +109,7 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
let cts = new CancellationTokenSource()
let cache = new Cache<'Key, 'Value>(options, capacity, cts)
- CacheMetrics.meter.CreateObservableGauge($"count-{Interlocked.Increment &CacheMetrics.cacheId}", (fun () -> cache.Store.Count)) |> ignore
+ CacheMetrics.meter.CreateObservableGauge($"count{Interlocked.Increment &CacheMetrics.cacheId}", (fun () -> cache.Store.Count)) |> ignore
if options.EvictionMethod = EvictionMethod.Background then
Task.Run(cache.TryEvictTask, cts.Token) |> ignore
diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs
index 74085e7288e..6c5f75eb71b 100644
--- a/vsintegration/src/FSharp.Editor/Common/Logging.fs
+++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs
@@ -149,26 +149,33 @@ module FSharpServiceTelemetry =
ActivitySource.AddActivityListener(listener)
let logCacheMetricsToOutput () =
+ let cacheCounts = Collections.Generic.Dictionary()
let listener = new MeterListener(
InstrumentPublished = fun instrument l ->
if instrument.Meter.Name = "FSharp.Compiler.Caches" then
- l.EnableMeasurementEvents(instrument)
+ cacheCounts[instrument.Name] <- 0
+ l.EnableMeasurementEvents(instrument)
)
- let msg = Event()
-
- let callBack = MeasurementCallback(fun instr v _ _ -> msg.Trigger $"{instr.Name}: {v}")
- listener.SetMeasurementEventCallback callBack
+ let callBack = MeasurementCallback(fun instr v _ _ -> cacheCounts[instr.Name] <- v)
+ listener.SetMeasurementEventCallback callBack
listener.Start()
-
- msg.Publish |> Event.pairwise |> Event.filter (fun (x, y) -> x <> y) |> Event.map snd |> Event.add logMsg
+
+ let msg = Event()
backgroundTask {
while true do
do! System.Threading.Tasks.Task.Delay(1000)
listener.RecordObservableInstruments()
+ if cacheCounts.Count > 0 then
+ let details =
+ [ for kvp in cacheCounts -> $"{kvp.Key}: {kvp.Value}"]
+ |> String.concat ", "
+ msg.Trigger $"total: {cacheCounts.Values |> Seq.sum} | {details}"
} |> ignore
+ msg.Publish |> Event.pairwise |> Event.filter (fun (x, y) -> x <> y) |> Event.map snd |> Event.add logMsg
+
#if DEBUG
open OpenTelemetry.Resources
open OpenTelemetry.Trace
From 165ea2482287e71f8e472888876a56fd52765fb0 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Mon, 14 Apr 2025 15:37:54 +0200
Subject: [PATCH 17/44] fix no eviction when OneOff
---
src/Compiler/Checking/import.fs | 22 ++++++++++++----------
src/Compiler/Utilities/Caches.fs | 4 +++-
2 files changed, 15 insertions(+), 11 deletions(-)
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index 24d2c313150..33bd2ca1d0b 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -112,16 +112,18 @@ type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) =
let typeRefToTyconRefCache = ConcurrentDictionary()
let typeSubsumptionCache =
- lazy
- typeSubsumptionCaches.GetValue(g, fun g ->
- Cache.Create(
- { CacheOptions.Default with
- // EvictionMethod = EvictionMethod.Background
- PercentageToEvict = 15
- MaximumCapacity = if g.compilationMode = CompilationMode.OneOff then System.Int32.MaxValue else 500_000
- }
- )
- )
+ lazy
+ let options =
+ if g.compilationMode = CompilationMode.OneOff then
+ { CacheOptions.Default with
+ PercentageToEvict = 0
+ EvictionMethod = EvictionMethod.KeepAll }
+ else
+ { CacheOptions.Default with
+ EvictionMethod = EvictionMethod.Blocking // EvictionMethod.Background
+ PercentageToEvict = 15
+ MaximumCapacity = 500_000 }
+ typeSubsumptionCaches.GetValue(g, fun _ -> Cache.Create(options))
member _.g = g
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 7e5024ca8ca..1038b77ef0a 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -44,6 +44,7 @@ type internal CachingStrategy =
type internal EvictionMethod =
| Blocking
| Background
+ | KeepAll
[]
type internal CacheOptions =
@@ -178,7 +179,8 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
if this.CalculateEvictionCount() > 0 then
match options.EvictionMethod with
| EvictionMethod.Blocking -> this.TryEvictItems()
- | EvictionMethod.Background -> ()
+ | EvictionMethod.Background
+ | EvictionMethod.KeepAll -> ()
member this.TryGet(key, value: outref<'Value>) =
match this.Store.TryGetValue(key) with
From 83208acb2eeaec1dc734cc3c1a1adafc7ced926c Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Mon, 14 Apr 2025 19:46:49 +0200
Subject: [PATCH 18/44] restore LanguageFeature
---
src/Compiler/Checking/TypeRelations.fs | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/Compiler/Checking/TypeRelations.fs b/src/Compiler/Checking/TypeRelations.fs
index ffece895ccd..daa5a656415 100644
--- a/src/Compiler/Checking/TypeRelations.fs
+++ b/src/Compiler/Checking/TypeRelations.fs
@@ -101,18 +101,18 @@ let TypesFeasiblyEquiv ndeep g amap m ty1 ty2 =
let TypesFeasiblyEquivStripMeasures g amap m ty1 ty2 =
TypesFeasiblyEquivalent true 0 g amap m ty1 ty2
-let inline TryGetCachedTypeSubsumption (_g: TcGlobals) (amap: ImportMap) key =
- //if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
+let inline TryGetCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key =
+ if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
match amap.TypeSubsumptionCache.TryGet(key) with
| true, subsumes ->
ValueSome subsumes
| false, _ ->
ValueNone
- //else
- // ValueNone
+ else
+ ValueNone
-let inline UpdateCachedTypeSubsumption (_g: TcGlobals) (amap: ImportMap) key subsumes : unit =
- //if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
+let inline UpdateCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key subsumes : unit =
+ if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
amap.TypeSubsumptionCache.TryAdd(key, subsumes) |> ignore
/// The feasible coercion relation. Part of the language spec.
From 4659934d7c170e6f573e2542c439b3a32413f3f9 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Mon, 14 Apr 2025 19:47:51 +0200
Subject: [PATCH 19/44] singleton, but compilationMode aware
---
src/Compiler/Checking/import.fs | 38 +++++++++++++++++++--------------
1 file changed, 22 insertions(+), 16 deletions(-)
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index 33bd2ca1d0b..5a4e454654f 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -93,7 +93,27 @@ type [] TTypeCacheKey =
combined
-let typeSubsumptionCaches = ConditionalWeakTable<_, Cache>()
+let getOrCreateTypeSubsumptionCache =
+ let mutable lockObj = obj()
+ let mutable cache = None
+
+ fun compilationMode ->
+ lock lockObj <| fun () ->
+ match cache with
+ | Some c -> c
+ | _ ->
+ let options =
+ if compilationMode = CompilationMode.OneOff then
+ { CacheOptions.Default with
+ PercentageToEvict = 0
+ EvictionMethod = EvictionMethod.KeepAll }
+ else
+ { CacheOptions.Default with
+ EvictionMethod = EvictionMethod.Background
+ PercentageToEvict = 15
+ MaximumCapacity = 100_000 }
+ cache <- Some (Cache.Create(options))
+ cache.Value
//-------------------------------------------------------------------------
// Import an IL types as F# types.
@@ -111,27 +131,13 @@ let typeSubsumptionCaches = ConditionalWeakTable<_, Cache>(
type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) =
let typeRefToTyconRefCache = ConcurrentDictionary()
- let typeSubsumptionCache =
- lazy
- let options =
- if g.compilationMode = CompilationMode.OneOff then
- { CacheOptions.Default with
- PercentageToEvict = 0
- EvictionMethod = EvictionMethod.KeepAll }
- else
- { CacheOptions.Default with
- EvictionMethod = EvictionMethod.Blocking // EvictionMethod.Background
- PercentageToEvict = 15
- MaximumCapacity = 500_000 }
- typeSubsumptionCaches.GetValue(g, fun _ -> Cache.Create(options))
-
member _.g = g
member _.assemblyLoader = assemblyLoader
member _.ILTypeRefToTyconRefCache = typeRefToTyconRefCache
- member _.TypeSubsumptionCache = typeSubsumptionCache.Value
+ member val TypeSubsumptionCache = getOrCreateTypeSubsumptionCache g.compilationMode
let CanImportILScopeRef (env: ImportMap) m scoref =
From 5abed612fa658d7c9d8c91063005d8e6ff1478b0 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Mon, 14 Apr 2025 19:55:33 +0200
Subject: [PATCH 20/44] fix background eviction
---
src/Compiler/Utilities/Caches.fs | 37 ++++++++++++++++----------------
1 file changed, 19 insertions(+), 18 deletions(-)
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 1038b77ef0a..e68e5dbe69e 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -58,7 +58,7 @@ type internal CacheOptions =
static member Default =
{
- MaximumCapacity = 10_000
+ MaximumCapacity = 100
PercentageToEvict = 5
Strategy = CachingStrategy.LRU
LevelOfConcurrency = Environment.ProcessorCount
@@ -80,7 +80,7 @@ type internal CachedEntity<'Value> =
module internal CacheMetrics =
let mutable cacheId = 0
- let meter = new Meter("FSharp.Compiler.Caches")
+ let createMeter () = new Meter("FSharp.Compiler.Caches")
[]
[]
@@ -89,6 +89,12 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
let cacheHit = Event<_ * _>()
let cacheMiss = Event<_>()
let eviction = Event<_>()
+
+ // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary.
+ let store = ConcurrentDictionary<_, CachedEntity<'Value>>(options.LevelOfConcurrency, capacity)
+
+ let meter = CacheMetrics.createMeter()
+ let _ = meter.CreateObservableGauge($"cache{Interlocked.Increment &CacheMetrics.cacheId}", (fun () -> store.Count))
[]
member val CacheHit = cacheHit.Publish
@@ -99,9 +105,6 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
[]
member val Eviction = eviction.Publish
- // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary.
- member val Store = ConcurrentDictionary<_, CachedEntity<'Value>>(options.LevelOfConcurrency, capacity)
-
static member Create(options: CacheOptions) =
let capacity =
options.MaximumCapacity
@@ -110,8 +113,6 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
let cts = new CancellationTokenSource()
let cache = new Cache<'Key, 'Value>(options, capacity, cts)
- CacheMetrics.meter.CreateObservableGauge($"count{Interlocked.Increment &CacheMetrics.cacheId}", (fun () -> cache.Store.Count)) |> ignore
-
if options.EvictionMethod = EvictionMethod.Background then
Task.Run(cache.TryEvictTask, cts.Token) |> ignore
@@ -131,15 +132,15 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
// |}
member private this.CalculateEvictionCount() =
- if this.Store.Count >= options.MaximumCapacity then
- (this.Store.Count - options.MaximumCapacity)
+ if store.Count >= options.MaximumCapacity then
+ (store.Count - options.MaximumCapacity)
+ (options.MaximumCapacity * options.PercentageToEvict / 100)
else
0
// TODO: All of these are proofs of concept, a very naive implementation of eviction strategies, it will always walk the dictionary to find the items to evict, this is not efficient.
member private this.TryGetPickToEvict() =
- this.Store
+ store
|> match options.Strategy with
| CachingStrategy.LRU -> ConcurrentDictionary.sortBy _.Value.LastAccessed
| CachingStrategy.LFU -> ConcurrentDictionary.sortBy _.Value.AccessCount
@@ -150,7 +151,7 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
member private this.TryEvictItems() =
if this.CalculateEvictionCount() > 0 then
for key in this.TryGetPickToEvict() do
- match this.Store.TryRemove(key) with
+ match store.TryRemove(key) with
| true, _ -> eviction.Trigger(key)
| _ -> () // TODO: We probably want to count eviction misses as well?
@@ -163,16 +164,16 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
if evictionCount > 0 then
this.TryEvictItems()
- let utilization = (this.Store.Count / options.MaximumCapacity)
+ let utilization = (float store.Count / float options.MaximumCapacity)
// So, based on utilization this will scale the delay between 0 and 1 seconds.
// Worst case scenario would be when 1 second delay happens,
// if the cache will grow rapidly (or in bursts), it will go beyond the maximum capacity.
// In this case underlying dictionary will resize, AND we will have to evict items, which will likely be slow.
// In this case, cache stats should be used to adjust MaximumCapacity and PercentageToEvict.
- let delay = 1000 - (1000 * utilization)
+ let delay = 1000.0 - (1000.0 * utilization)
- if delay > 0 then
- do! Task.Delay(delay)
+ if delay > 0.0 then
+ do! Task.Delay(int delay)
}
member this.TryEvict() =
@@ -183,7 +184,7 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
| EvictionMethod.KeepAll -> ()
member this.TryGet(key, value: outref<'Value>) =
- match this.Store.TryGetValue(key) with
+ match store.TryGetValue(key) with
| true, cachedEntity ->
// this is fine to be non-atomic, I guess, we are okay with race if the time is within the time of multiple concurrent calls.
cachedEntity.LastAccessed <- DateTimeOffset.Now.Ticks
@@ -204,10 +205,10 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
let value = CachedEntity<'Value>(value)
if update then
- let _ = this.Store.AddOrUpdate(key, value, (fun _ _ -> value))
+ let _ = store.AddOrUpdate(key, value, (fun _ _ -> value))
true
else
- this.Store.TryAdd(key, value)
+ store.TryAdd(key, value)
interface IDisposable with
member _.Dispose() = cts.Cancel()
From e1a48ff2ff8184f0da1fa255701735918d709a60 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Mon, 14 Apr 2025 22:44:51 +0200
Subject: [PATCH 21/44] wip
---
src/Compiler/Utilities/Caches.fs | 36 ++++++++++++-------
.../src/FSharp.Editor/Common/Logging.fs | 17 +++++----
2 files changed, 32 insertions(+), 21 deletions(-)
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index e68e5dbe69e..80213e76809 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -80,7 +80,20 @@ type internal CachedEntity<'Value> =
module internal CacheMetrics =
let mutable cacheId = 0
- let createMeter () = new Meter("FSharp.Compiler.Caches")
+ let addInstrumentation (store: ConcurrentDictionary<_, CachedEntity<'Value>>) =
+ let meter = new Meter("FSharp.Compiler.Caches")
+ let uid = Interlocked.Increment &cacheId
+
+ let orZero f = fun () ->
+ let vs = store.Values
+ if vs |> Seq.isEmpty then 0L else f vs
+
+ let _ = meter.CreateObservableGauge($"cache{uid}", (fun () -> int64 store.Count))
+ //let _ = meter.CreateObservableGauge($"MRA{uid}", orZero (Seq.map _.LastAccessed >> Seq.max))
+ //let _ = meter.CreateObservableGauge($"LRA{uid}", orZero (Seq.map _.LastAccessed >> Seq.min))
+ let _ = meter.CreateObservableGauge($"MFA{uid}", orZero (Seq.map _.AccessCount >> Seq.max))
+ let _ = meter.CreateObservableGauge($"LFA{uid}", orZero (Seq.map _.AccessCount >> Seq.min))
+ ()
[]
[]
@@ -93,8 +106,7 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
// Increase expected capacity by the percentage to evict, since we want to not resize the dictionary.
let store = ConcurrentDictionary<_, CachedEntity<'Value>>(options.LevelOfConcurrency, capacity)
- let meter = CacheMetrics.createMeter()
- let _ = meter.CreateObservableGauge($"cache{Interlocked.Increment &CacheMetrics.cacheId}", (fun () -> store.Count))
+ do CacheMetrics.addInstrumentation store
[]
member val CacheHit = cacheHit.Publish
@@ -164,16 +176,16 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
if evictionCount > 0 then
this.TryEvictItems()
- let utilization = (float store.Count / float options.MaximumCapacity)
- // So, based on utilization this will scale the delay between 0 and 1 seconds.
- // Worst case scenario would be when 1 second delay happens,
- // if the cache will grow rapidly (or in bursts), it will go beyond the maximum capacity.
- // In this case underlying dictionary will resize, AND we will have to evict items, which will likely be slow.
- // In this case, cache stats should be used to adjust MaximumCapacity and PercentageToEvict.
- let delay = 1000.0 - (1000.0 * utilization)
+ let utilization = (float store.Count / float options.MaximumCapacity)
+ // So, based on utilization this will scale the delay between 0 and 1 seconds.
+ // Worst case scenario would be when 1 second delay happens,
+ // if the cache will grow rapidly (or in bursts), it will go beyond the maximum capacity.
+ // In this case underlying dictionary will resize, AND we will have to evict items, which will likely be slow.
+ // In this case, cache stats should be used to adjust MaximumCapacity and PercentageToEvict.
+ let delay = 1000.0 - (1000.0 * utilization)
- if delay > 0.0 then
- do! Task.Delay(int delay)
+ if delay > 0.0 then
+ do! Task.Delay(int delay)
}
member this.TryEvict() =
diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs
index 6c5f75eb71b..beddfb8f823 100644
--- a/vsintegration/src/FSharp.Editor/Common/Logging.fs
+++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs
@@ -149,16 +149,16 @@ module FSharpServiceTelemetry =
ActivitySource.AddActivityListener(listener)
let logCacheMetricsToOutput () =
- let cacheCounts = Collections.Generic.Dictionary()
+ let instruments = Collections.Generic.Dictionary()
let listener = new MeterListener(
InstrumentPublished = fun instrument l ->
if instrument.Meter.Name = "FSharp.Compiler.Caches" then
- cacheCounts[instrument.Name] <- 0
+ instruments[instrument.Name] <- 0L
l.EnableMeasurementEvents(instrument)
)
- let callBack = MeasurementCallback(fun instr v _ _ -> cacheCounts[instr.Name] <- v)
- listener.SetMeasurementEventCallback callBack
+ let callBack = MeasurementCallback(fun instr v _ _ -> instruments[instr.Name] <- v)
+ listener.SetMeasurementEventCallback callBack
listener.Start()
let msg = Event()
@@ -167,11 +167,10 @@ module FSharpServiceTelemetry =
while true do
do! System.Threading.Tasks.Task.Delay(1000)
listener.RecordObservableInstruments()
- if cacheCounts.Count > 0 then
- let details =
- [ for kvp in cacheCounts -> $"{kvp.Key}: {kvp.Value}"]
- |> String.concat ", "
- msg.Trigger $"total: {cacheCounts.Values |> Seq.sum} | {details}"
+ if instruments.Count > 0 then
+ [ for kvp in instruments -> $"{kvp.Key}: {kvp.Value}"]
+ |> String.concat ", "
+ |> msg.Trigger
} |> ignore
msg.Publish |> Event.pairwise |> Event.filter (fun (x, y) -> x <> y) |> Event.map snd |> Event.add logMsg
From e1cd30b56e33893296d32643d1e8b3c5cc3feaa9 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Mon, 14 Apr 2025 23:53:35 +0200
Subject: [PATCH 22/44] more metrics
---
src/Compiler/Utilities/Caches.fs | 45 ++++++++++++++++++++------------
1 file changed, 28 insertions(+), 17 deletions(-)
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 80213e76809..255ed46aed8 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -93,7 +93,21 @@ module internal CacheMetrics =
//let _ = meter.CreateObservableGauge($"LRA{uid}", orZero (Seq.map _.LastAccessed >> Seq.min))
let _ = meter.CreateObservableGauge($"MFA{uid}", orZero (Seq.map _.AccessCount >> Seq.max))
let _ = meter.CreateObservableGauge($"LFA{uid}", orZero (Seq.map _.AccessCount >> Seq.min))
- ()
+
+ let mutable evictions = 0L
+ let mutable hits = 0L
+ let mutable misses = 0L
+
+ fun eviction hit miss ->
+
+ eviction |> Event.add (fun _ -> Interlocked.Increment &evictions |> ignore)
+ hit |> Event.add (fun _ -> Interlocked.Increment &hits |> ignore)
+ miss |> Event.add (fun _ -> Interlocked.Increment &misses |> ignore)
+
+ let _ = meter.CreateObservableGauge($"evicted{uid}", fun () -> Interlocked.Exchange(&evictions, 0L))
+ let _ = meter.CreateObservableGauge($"hits{uid}", fun () -> Interlocked.Exchange(&hits, 0L))
+ let _ = meter.CreateObservableGauge($"misses{uid}", fun () -> Interlocked.Exchange(&misses, 0L))
+ ()
[]
[]
@@ -106,7 +120,7 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
// Increase expected capacity by the percentage to evict, since we want to not resize the dictionary.
let store = ConcurrentDictionary<_, CachedEntity<'Value>>(options.LevelOfConcurrency, capacity)
- do CacheMetrics.addInstrumentation store
+ do CacheMetrics.addInstrumentation store eviction.Publish cacheHit.Publish cacheMiss.Publish
[]
member val CacheHit = cacheHit.Publish
@@ -171,21 +185,18 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
member private this.TryEvictTask() =
backgroundTask {
while not cts.Token.IsCancellationRequested do
- let evictionCount = this.CalculateEvictionCount()
-
- if evictionCount > 0 then
- this.TryEvictItems()
-
- let utilization = (float store.Count / float options.MaximumCapacity)
- // So, based on utilization this will scale the delay between 0 and 1 seconds.
- // Worst case scenario would be when 1 second delay happens,
- // if the cache will grow rapidly (or in bursts), it will go beyond the maximum capacity.
- // In this case underlying dictionary will resize, AND we will have to evict items, which will likely be slow.
- // In this case, cache stats should be used to adjust MaximumCapacity and PercentageToEvict.
- let delay = 1000.0 - (1000.0 * utilization)
-
- if delay > 0.0 then
- do! Task.Delay(int delay)
+ this.TryEvictItems()
+
+ let utilization = (float store.Count / float options.MaximumCapacity)
+ // So, based on utilization this will scale the delay between 0 and 1 seconds.
+ // Worst case scenario would be when 1 second delay happens,
+ // if the cache will grow rapidly (or in bursts), it will go beyond the maximum capacity.
+ // In this case underlying dictionary will resize, AND we will have to evict items, which will likely be slow.
+ // In this case, cache stats should be used to adjust MaximumCapacity and PercentageToEvict.
+ let delay = 1000.0 - (1000.0 * utilization)
+
+ if delay > 0.0 then
+ do! Task.Delay(int delay)
}
member this.TryEvict() =
From 7bdde6ad3c69091bd3eefdcafcfa5a1a6521b966 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Tue, 15 Apr 2025 10:27:49 +0200
Subject: [PATCH 23/44] background eviction needs work
---
src/Compiler/Checking/import.fs | 2 +-
src/Compiler/Utilities/Caches.fs | 11 ++++++++---
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index 5a4e454654f..2b76e9aaa50 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -109,7 +109,7 @@ let getOrCreateTypeSubsumptionCache =
EvictionMethod = EvictionMethod.KeepAll }
else
{ CacheOptions.Default with
- EvictionMethod = EvictionMethod.Background
+ EvictionMethod = EvictionMethod.Blocking // EvictionMethod.Background
PercentageToEvict = 15
MaximumCapacity = 100_000 }
cache <- Some (Cache.Create(options))
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 255ed46aed8..0e327a23b70 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -1,6 +1,7 @@
namespace FSharp.Compiler
open System
+open System.Collections.Generic
open System.Collections.Concurrent
open System.Threading
open System.Threading.Tasks
@@ -95,16 +96,19 @@ module internal CacheMetrics =
let _ = meter.CreateObservableGauge($"LFA{uid}", orZero (Seq.map _.AccessCount >> Seq.min))
let mutable evictions = 0L
+ let mutable fails = 0L
let mutable hits = 0L
let mutable misses = 0L
- fun eviction hit miss ->
+ fun eviction hit miss evictionFail ->
eviction |> Event.add (fun _ -> Interlocked.Increment &evictions |> ignore)
+ evictionFail |> Event.add (fun _ -> Interlocked.Increment &fails |> ignore)
hit |> Event.add (fun _ -> Interlocked.Increment &hits |> ignore)
miss |> Event.add (fun _ -> Interlocked.Increment &misses |> ignore)
let _ = meter.CreateObservableGauge($"evicted{uid}", fun () -> Interlocked.Exchange(&evictions, 0L))
+ let _ = meter.CreateObservableGauge($"fails{uid}", fun () -> Interlocked.Exchange(&fails, 0L))
let _ = meter.CreateObservableGauge($"hits{uid}", fun () -> Interlocked.Exchange(&hits, 0L))
let _ = meter.CreateObservableGauge($"misses{uid}", fun () -> Interlocked.Exchange(&misses, 0L))
()
@@ -116,11 +120,12 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
let cacheHit = Event<_ * _>()
let cacheMiss = Event<_>()
let eviction = Event<_>()
+ let evictionFail = Event<_>()
// Increase expected capacity by the percentage to evict, since we want to not resize the dictionary.
let store = ConcurrentDictionary<_, CachedEntity<'Value>>(options.LevelOfConcurrency, capacity)
- do CacheMetrics.addInstrumentation store eviction.Publish cacheHit.Publish cacheMiss.Publish
+ do CacheMetrics.addInstrumentation store eviction.Publish cacheHit.Publish cacheMiss.Publish evictionFail.Publish
[]
member val CacheHit = cacheHit.Publish
@@ -179,7 +184,7 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
for key in this.TryGetPickToEvict() do
match store.TryRemove(key) with
| true, _ -> eviction.Trigger(key)
- | _ -> () // TODO: We probably want to count eviction misses as well?
+ | _ -> evictionFail.Trigger(key) // TODO: We probably want to count eviction misses as well?
// TODO: Shall this be a safer task, wrapping everything in try .. with, so it's not crashing silently?
member private this.TryEvictTask() =
From e6ba27e77e92bbf7ac5591399c0af481f46991e4 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Thu, 17 Apr 2025 23:32:54 +0200
Subject: [PATCH 24/44] fix stampEquals
---
src/Compiler/Utilities/TypeHashing.fs | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/Compiler/Utilities/TypeHashing.fs b/src/Compiler/Utilities/TypeHashing.fs
index bcdface38be..1475635972d 100644
--- a/src/Compiler/Utilities/TypeHashing.fs
+++ b/src/Compiler/Utilities/TypeHashing.fs
@@ -126,9 +126,13 @@ module HashAccessibility =
module rec HashTypes =
open Microsoft.FSharp.Core.LanguagePrimitives
- let stampEquals g ty1 ty2 =
+ let rec stampEquals g ty1 ty2 =
match (stripTyEqns g ty1), (stripTyEqns g ty2) with
- | TType_app(tcref1, _, _), TType_app(tcref2, _, _) -> tcref1.Stamp.Equals(tcref2.Stamp)
+ | TType_app(tcref1, tinst1, _), TType_app(tcref2, tinst2, _) ->
+ tcref1.Stamp = tcref2.Stamp &&
+ tinst1.Length = tinst2.Length &&
+ tinst1 |> List.zip tinst2 |> List.forall (fun (t1, t2) -> stampEquals g t1 t2)
+
| TType_var(r1, _), TType_var(r2, _) -> r1.Stamp.Equals(r2.Stamp)
| _ -> false
From 5b8735662e8090f528ccfbda17f508c7ae2c0012 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Thu, 17 Apr 2025 23:33:29 +0200
Subject: [PATCH 25/44] fix hash
---
src/Compiler/Checking/import.fs | 32 +++++++++++++++++++++++---------
1 file changed, 23 insertions(+), 9 deletions(-)
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index 2b76e9aaa50..34dea504cab 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -6,6 +6,7 @@ module internal FSharp.Compiler.Import
open System.Collections.Concurrent
open System.Collections.Generic
open System.Collections.Immutable
+open System.Diagnostics
open System.Runtime.CompilerServices
open Internal.Utilities.Library
@@ -55,7 +56,8 @@ type CanCoerce =
| CanCoerce
| NoCoerce
-type [] TTypeCacheKey =
+[]
+type TTypeCacheKey =
val ty1: TType
val ty2: TType
@@ -84,14 +86,26 @@ type [] TTypeCacheKey =
| _ -> false
override this.GetHashCode() : int =
- let g = this.tcGlobals
-
- let ty1Hash = combineHash (hashStamp g this.ty1) (hashTType g this.ty1)
- let ty2Hash = combineHash (hashStamp g this.ty2) (hashTType g this.ty2)
-
- let combined = combineHash (combineHash ty1Hash ty2Hash) (hash this.canCoerce)
-
- combined
+ // TODO: we need reasonable uniformity
+ // The idea is to keep the illusion of immutability of TType.
+ // This hash must be stable during compilation, otherwise we won't be able to find the keys in the cache.
+ let rec simpleTypeHash ty =
+ match ty with
+ | TType_ucase (u, tinst) -> tinst |> hashListOrderMatters (simpleTypeHash) |> pipeToHash (hash u.CaseName)
+ | TType_app(tcref, tinst, _) -> tinst |> hashListOrderMatters (simpleTypeHash) |> pipeToHash (hash tcref.Stamp)
+ | TType_anon(info, tys) -> tys |> hashListOrderMatters (simpleTypeHash) |> pipeToHash (hash info.Stamp)
+ | TType_tuple(_ , tys) -> tys |> hashListOrderMatters (simpleTypeHash)
+ | TType_forall(tps, tau) -> tps |> Seq.map _.Stamp |> hashListOrderMatters (hash) |> pipeToHash (simpleTypeHash tau)
+ | TType_fun (d, r, _) -> simpleTypeHash d |> pipeToHash (simpleTypeHash r)
+ | TType_var _
+ | TType_measure _ -> 0
+
+ hash this.tcGlobals
+ |> pipeToHash (simpleTypeHash this.ty1)
+ |> pipeToHash (simpleTypeHash this.ty2)
+ |> pipeToHash (hash this.canCoerce)
+
+ override this.ToString () = $"{this.ty1.DebugText}-{this.ty2.DebugText}"
let getOrCreateTypeSubsumptionCache =
let mutable lockObj = obj()
From 039cc9ae0615d5db5f48211c011d53704c17101c Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Thu, 17 Apr 2025 23:34:48 +0200
Subject: [PATCH 26/44] improve allocations etc
---
src/Compiler/Checking/TypeRelations.fs | 24 +-
src/Compiler/Checking/import.fs | 7 +-
src/Compiler/Utilities/Caches.fs | 294 +++++++++++++------------
3 files changed, 172 insertions(+), 153 deletions(-)
diff --git a/src/Compiler/Checking/TypeRelations.fs b/src/Compiler/Checking/TypeRelations.fs
index daa5a656415..fd6e12bc4d7 100644
--- a/src/Compiler/Checking/TypeRelations.fs
+++ b/src/Compiler/Checking/TypeRelations.fs
@@ -103,7 +103,7 @@ let TypesFeasiblyEquivStripMeasures g amap m ty1 ty2 =
let inline TryGetCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key =
if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
- match amap.TypeSubsumptionCache.TryGet(key) with
+ match amap.TypeSubsumptionCache.TryGetValue(key) with
| true, subsumes ->
ValueSome subsumes
| false, _ ->
@@ -113,7 +113,10 @@ let inline TryGetCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key =
let inline UpdateCachedTypeSubsumption (g: TcGlobals) (amap: ImportMap) key subsumes : unit =
if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then
- amap.TypeSubsumptionCache.TryAdd(key, subsumes) |> ignore
+ amap.TypeSubsumptionCache.AddOrUpdate(key, subsumes)
+
+[]
+type ResultWorthCaching = Yes | No
/// The feasible coercion relation. Part of the language spec.
let rec TypeFeasiblySubsumesType ndeep (g: TcGlobals) (amap: ImportMap) m (ty1: TType) (canCoerce: CanCoerce) (ty2: TType) =
@@ -131,32 +134,33 @@ let rec TypeFeasiblySubsumesType ndeep (g: TcGlobals) (amap: ImportMap) m (ty1:
| ValueSome subsumes ->
subsumes
| ValueNone ->
- let subsumes =
+ let subsumes, worthCaching =
match ty1, ty2 with
| TType_measure _, TType_measure _
| TType_var _, _ | _, TType_var _ ->
- true
+ true, ResultWorthCaching.No
| TType_app (tc1, l1, _), TType_app (tc2, l2, _) when tyconRefEq g tc1 tc2 ->
- List.lengthsEqAndForall2 (TypesFeasiblyEquiv ndeep g amap m) l1 l2
+ List.lengthsEqAndForall2 (TypesFeasiblyEquiv ndeep g amap m) l1 l2, ResultWorthCaching.Yes
| TType_tuple _, TType_tuple _
| TType_anon _, TType_anon _
| TType_fun _, TType_fun _ ->
- TypesFeasiblyEquiv ndeep g amap m ty1 ty2
+ TypesFeasiblyEquiv ndeep g amap m ty1 ty2, ResultWorthCaching.Yes
| _ ->
// F# reference types are subtypes of type 'obj'
if isObjTyAnyNullness g ty1 && (canCoerce = CanCoerce || isRefTy g ty2) then
- true
+ true, ResultWorthCaching.No
elif isAppTy g ty2 && (canCoerce = CanCoerce || isRefTy g ty2) && TypeFeasiblySubsumesTypeWithSupertypeCheck g amap m ndeep ty1 ty2 then
- true
+ true, ResultWorthCaching.Yes
else
let interfaces = GetImmediateInterfacesOfType SkipUnrefInterfaces.Yes g amap m ty2
// See if any interface in type hierarchy of ty2 is a supertype of ty1
- List.exists (TypeFeasiblySubsumesType (ndeep + 1) g amap m ty1 NoCoerce) interfaces
+ List.exists (TypeFeasiblySubsumesType (ndeep + 1) g amap m ty1 NoCoerce) interfaces, ResultWorthCaching.Yes
- UpdateCachedTypeSubsumption g amap key subsumes
+ if worthCaching = ResultWorthCaching.Yes then
+ UpdateCachedTypeSubsumption g amap key subsumes
subsumes
diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs
index 34dea504cab..d96ce8b182a 100644
--- a/src/Compiler/Checking/import.fs
+++ b/src/Compiler/Checking/import.fs
@@ -120,11 +120,12 @@ let getOrCreateTypeSubsumptionCache =
if compilationMode = CompilationMode.OneOff then
{ CacheOptions.Default with
PercentageToEvict = 0
- EvictionMethod = EvictionMethod.KeepAll }
+ EvictionMethod = EvictionMethod.NoEviction }
else
{ CacheOptions.Default with
- EvictionMethod = EvictionMethod.Blocking // EvictionMethod.Background
- PercentageToEvict = 15
+ EvictionMethod = EvictionMethod.Background
+ Strategy = CachingStrategy.LRU
+ PercentageToEvict = 5
MaximumCapacity = 100_000 }
cache <- Some (Cache.Create(options))
cache.Value
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 0e327a23b70..372750de78f 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -1,40 +1,14 @@
+// LinkedList uses nulls, so we need to disable the nullability warnings for this file.
+#nowarn 3261
namespace FSharp.Compiler
open System
open System.Collections.Generic
open System.Collections.Concurrent
open System.Threading
-open System.Threading.Tasks
open System.Diagnostics
open System.Diagnostics.Metrics
-
-[]
-// Default Seq.* function have one issue - when doing `Seq.sortBy`, it will call a `ToArray` on the collection,
-// which is *not* calling `ConcurrentDictionary.ToArray`, but uses a custom one instead (treating it as `ICollection`)
-// this leads to and exception when trying to evict without locking (The index is equal to or greater than the length of the array,
-// or the number of elements in the dictionary is greater than the available space from index to the end of the destination array.)
-// this is casuedby insertions happened between reading the `Count` and doing the `CopyTo`.
-// This solution introduces a custom `ConcurrentDictionary.sortBy` which will be calling a proper `CopyTo`, the one on the ConcurrentDictionary itself.
-module internal ConcurrentDictionary =
-
- open System.Collections
- open System.Collections.Generic
-
- let inline mkSeq f =
- { new IEnumerable<'U> with
- member _.GetEnumerator() = f ()
-
- interface IEnumerable with
- member _.GetEnumerator() = (f () :> IEnumerator)
- }
-
- let inline mkDelayedSeq (f: unit -> IEnumerable<'T>) = mkSeq (fun () -> f().GetEnumerator())
-
- let inline sortBy ([] projection) (source: ConcurrentDictionary<_, _>) =
- mkDelayedSeq (fun () ->
- let array = source.ToArray()
- Array.sortInPlaceBy projection array
- array :> seq<_>)
+open Internal.Utilities.Library
[]
type internal CachingStrategy =
@@ -45,7 +19,7 @@ type internal CachingStrategy =
type internal EvictionMethod =
| Blocking
| Background
- | KeepAll
+ | NoEviction
[]
type internal CacheOptions =
@@ -67,21 +41,78 @@ type internal CacheOptions =
}
[]
-type internal CachedEntity<'Value> =
- val Value: 'Value
- val mutable LastAccessed: int64
+[]
+type internal CachedEntity<'Key, 'Value> =
+ val mutable Key: 'Key
+ val mutable Value: 'Value
val mutable AccessCount: int64
+ val mutable Node: LinkedListNode>
- new(value: 'Value) =
- {
- Value = value
- LastAccessed = DateTimeOffset.Now.Ticks
- AccessCount = 0L
- }
+ private new(key, value) = { Key = key; Value = value; AccessCount = 0L; Node = Unchecked.defaultof<_> }
+
+ static member Create(key, value) =
+ let entity = CachedEntity(key, value)
+ entity.Node <- LinkedListNode(entity)
+ entity
+
+ member this.ReUse(key, value) =
+ this.Key <- key
+ this.Value <- value
+ this.AccessCount <- 0L
+ this
+
+ override this.ToString() = $"{this.Key}"
+
+type internal EvictionQueue<'Key, 'Value>() =
+
+ let list = LinkedList>()
+ let pool = Queue>()
+
+ member _.Acquire(key, value) =
+ lock pool <| fun () ->
+ if pool.Count > 0 then
+ pool.Dequeue().ReUse(key, value)
+ else
+ CachedEntity.Create<_, _>(key, value)
+
+ member _.Add(entity: CachedEntity<'Key, 'Value>) =
+ lock list <| fun () ->
+ if isNull entity.Node.List then
+ list.AddLast(entity.Node)
+
+ member _.Update(entity: CachedEntity<'Key, 'Value>, strategy: CachingStrategy) =
+ lock list <| fun () ->
+ entity.AccessCount <- entity.AccessCount + 1L
+
+ let node = entity.Node
+
+ match strategy with
+ | CachingStrategy.LRU ->
+ // Just move this node to the end of the list.
+ list.Remove(node)
+ list.AddLast(node)
+ | CachingStrategy.LFU ->
+ // Bubble up the node in the list, linear time.
+ // TODO: frequency list approach would be faster.
+ while (isNotNull node.Next) && (node.Next.Value.AccessCount < node.Value.AccessCount) do
+ list.Remove(node)
+ list.AddAfter(node.Next, node)
+
+ member _.GetKeysToEvict(count) =
+ lock list <| fun () ->
+ list |> Seq.map _.Key |> Seq.truncate count |> Seq.toArray
+
+ member _.Remove(entity: CachedEntity<_,_>) =
+ lock list <| fun () -> list.Remove(entity.Node)
+ // Return to the pool for reuse.
+ lock pool <| fun () -> pool.Enqueue(entity)
+
+ member _.Count = list.Count
module internal CacheMetrics =
+
let mutable cacheId = 0
- let addInstrumentation (store: ConcurrentDictionary<_, CachedEntity<'Value>>) =
+ let addInstrumentation (store: ConcurrentDictionary<_, CachedEntity<_,_>>) =
let meter = new Meter("FSharp.Compiler.Caches")
let uid = Interlocked.Increment &cacheId
@@ -90,8 +121,6 @@ module internal CacheMetrics =
if vs |> Seq.isEmpty then 0L else f vs
let _ = meter.CreateObservableGauge($"cache{uid}", (fun () -> int64 store.Count))
- //let _ = meter.CreateObservableGauge($"MRA{uid}", orZero (Seq.map _.LastAccessed >> Seq.max))
- //let _ = meter.CreateObservableGauge($"LRA{uid}", orZero (Seq.map _.LastAccessed >> Seq.min))
let _ = meter.CreateObservableGauge($"MFA{uid}", orZero (Seq.map _.AccessCount >> Seq.max))
let _ = meter.CreateObservableGauge($"LFA{uid}", orZero (Seq.map _.AccessCount >> Seq.min))
@@ -115,18 +144,92 @@ module internal CacheMetrics =
[]
[]
-type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts) =
+type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> private (options: CacheOptions, capacity, cts: CancellationTokenSource) =
let cacheHit = Event<_ * _>()
let cacheMiss = Event<_>()
let eviction = Event<_>()
let evictionFail = Event<_>()
- // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary.
- let store = ConcurrentDictionary<_, CachedEntity<'Value>>(options.LevelOfConcurrency, capacity)
+ let store = ConcurrentDictionary<'Key, CachedEntity<'Key, 'Value>>(options.LevelOfConcurrency, capacity)
+
+ let evictionQueue = EvictionQueue<'Key, 'Value>()
+
+ let tryEvictItems () =
+ let count = if store.Count > options.MaximumCapacity then store.Count - options.MaximumCapacity else 0
+ for key in evictionQueue.GetKeysToEvict(count) do
+ match store.TryRemove(key) with
+ | true, removed ->
+ evictionQueue.Remove(removed)
+ eviction.Trigger(key)
+ | _ ->
+ evictionFail.Trigger(key)
+
+ let rec backgroundEviction () =
+ async {
+ tryEvictItems ()
+
+ let utilization = (float store.Count / float options.MaximumCapacity)
+ // So, based on utilization this will scale the delay between 0 and 1 seconds.
+ // Worst case scenario would be when 1 second delay happens,
+ // if the cache will grow rapidly (or in bursts), it will go beyond the maximum capacity.
+ // In this case underlying dictionary will resize, AND we will have to evict items, which will likely be slow.
+ // In this case, cache stats should be used to adjust MaximumCapacity and PercentageToEvict.
+ let delay = 1000.0 - (1000.0 * utilization)
+
+ if delay > 0.0 then
+ do! Async.Sleep (int delay)
+
+ return! backgroundEviction ()
+ }
+
+ do if options.EvictionMethod = EvictionMethod.Background then
+ Async.Start(backgroundEviction (), cancellationToken = cts.Token)
do CacheMetrics.addInstrumentation store eviction.Publish cacheHit.Publish cacheMiss.Publish evictionFail.Publish
+ let tryEvict () =
+ if options.EvictionMethod.IsBlocking then tryEvictItems()
+
+ let tryGet (key: 'Key) =
+ match store.TryGetValue(key) with
+ | true, cachedEntity ->
+ evictionQueue.Update(cachedEntity, options.Strategy)
+ Some cachedEntity
+ | _ ->
+ None
+
+ member _.TryGetValue (key: 'Key, value: outref<'Value>) =
+ match tryGet key with
+ | Some cachedEntity ->
+ cacheHit.Trigger(key, cachedEntity.Value)
+ value <- cachedEntity.Value
+ true
+ | _ ->
+ cacheMiss.Trigger(key)
+ value <- Unchecked.defaultof<'Value>
+ false
+
+ member _.TryAdd(key: 'Key, value: 'Value) =
+ tryEvict()
+
+ let cachedEntity = evictionQueue.Acquire(key, value)
+ if store.TryAdd(key, cachedEntity) then
+ evictionQueue.Add(cachedEntity)
+ true
+ else
+ false
+
+ member _.AddOrUpdate(key: 'Key, value: 'Value) =
+ tryEvict()
+
+ let entity = store.AddOrUpdate(
+ key,
+ (fun _ -> evictionQueue.Acquire(key, value)),
+ (fun _ (current: CachedEntity<_, _>) -> current.Value <- value; current)
+ )
+ evictionQueue.Add(entity)
+
[]
member val CacheHit = cacheHit.Publish
@@ -136,109 +239,20 @@ type internal Cache<'Key, 'Value> private (options: CacheOptions, capacity, cts)
[]
member val Eviction = eviction.Publish
+ []
+ member val EvictionFail = evictionFail.Publish
+
static member Create(options: CacheOptions) =
+ // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary.
let capacity =
options.MaximumCapacity
+ (options.MaximumCapacity * options.PercentageToEvict / 100)
let cts = new CancellationTokenSource()
- let cache = new Cache<'Key, 'Value>(options, capacity, cts)
-
- if options.EvictionMethod = EvictionMethod.Background then
- Task.Run(cache.TryEvictTask, cts.Token) |> ignore
-
- cache
-
- //member this.GetStats() =
- // {|
- // Capacity = options.MaximumCapacity
- // PercentageToEvict = options.PercentageToEvict
- // Strategy = options.Strategy
- // LevelOfConcurrency = options.LevelOfConcurrency
- // Count = this.Store.Count
- // MostRecentlyAccesssed = this.Store.Values |> Seq.maxBy _.LastAccessed |> _.LastAccessed
- // LeastRecentlyAccesssed = this.Store.Values |> Seq.minBy _.LastAccessed |> _.LastAccessed
- // MostFrequentlyAccessed = this.Store.Values |> Seq.maxBy _.AccessCount |> _.AccessCount
- // LeastFrequentlyAccessed = this.Store.Values |> Seq.minBy _.AccessCount |> _.AccessCount
- // |}
-
- member private this.CalculateEvictionCount() =
- if store.Count >= options.MaximumCapacity then
- (store.Count - options.MaximumCapacity)
- + (options.MaximumCapacity * options.PercentageToEvict / 100)
- else
- 0
-
- // TODO: All of these are proofs of concept, a very naive implementation of eviction strategies, it will always walk the dictionary to find the items to evict, this is not efficient.
- member private this.TryGetPickToEvict() =
- store
- |> match options.Strategy with
- | CachingStrategy.LRU -> ConcurrentDictionary.sortBy _.Value.LastAccessed
- | CachingStrategy.LFU -> ConcurrentDictionary.sortBy _.Value.AccessCount
- |> Seq.take (this.CalculateEvictionCount())
- |> Seq.map (fun x -> x.Key)
-
- // TODO: Explore an eviction shortcut, some sort of list of keys to evict first, based on the strategy.
- member private this.TryEvictItems() =
- if this.CalculateEvictionCount() > 0 then
- for key in this.TryGetPickToEvict() do
- match store.TryRemove(key) with
- | true, _ -> eviction.Trigger(key)
- | _ -> evictionFail.Trigger(key) // TODO: We probably want to count eviction misses as well?
-
- // TODO: Shall this be a safer task, wrapping everything in try .. with, so it's not crashing silently?
- member private this.TryEvictTask() =
- backgroundTask {
- while not cts.Token.IsCancellationRequested do
- this.TryEvictItems()
-
- let utilization = (float store.Count / float options.MaximumCapacity)
- // So, based on utilization this will scale the delay between 0 and 1 seconds.
- // Worst case scenario would be when 1 second delay happens,
- // if the cache will grow rapidly (or in bursts), it will go beyond the maximum capacity.
- // In this case underlying dictionary will resize, AND we will have to evict items, which will likely be slow.
- // In this case, cache stats should be used to adjust MaximumCapacity and PercentageToEvict.
- let delay = 1000.0 - (1000.0 * utilization)
-
- if delay > 0.0 then
- do! Task.Delay(int delay)
- }
-
- member this.TryEvict() =
- if this.CalculateEvictionCount() > 0 then
- match options.EvictionMethod with
- | EvictionMethod.Blocking -> this.TryEvictItems()
- | EvictionMethod.Background
- | EvictionMethod.KeepAll -> ()
-
- member this.TryGet(key, value: outref<'Value>) =
- match store.TryGetValue(key) with
- | true, cachedEntity ->
- // this is fine to be non-atomic, I guess, we are okay with race if the time is within the time of multiple concurrent calls.
- cachedEntity.LastAccessed <- DateTimeOffset.Now.Ticks
- let _ = Interlocked.Increment(&cachedEntity.AccessCount)
- cacheHit.Trigger(key, cachedEntity.Value)
- value <- cachedEntity.Value
- true
- | _ ->
- cacheMiss.Trigger(key)
- value <- Unchecked.defaultof<'Value>
- false
-
- member this.TryAdd(key, value: 'Value, ?update: bool) =
- let update = defaultArg update false
-
- this.TryEvict()
-
- let value = CachedEntity<'Value>(value)
-
- if update then
- let _ = store.AddOrUpdate(key, value, (fun _ _ -> value))
- true
- else
- store.TryAdd(key, value)
+ new Cache<'Key, 'Value>(options, capacity, cts)
interface IDisposable with
member _.Dispose() = cts.Cancel()
member this.Dispose() = (this :> IDisposable).Dispose()
+
From 0e8e6cbf36a2575fbb2d31aa48eefc2efe08ca99 Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Thu, 17 Apr 2025 23:45:31 +0200
Subject: [PATCH 27/44] format
---
src/Compiler/Driver/CompilerImports.fs | 2 +-
src/Compiler/Utilities/Caches.fs | 126 +++++++++++-------
src/Compiler/Utilities/TypeHashing.fs | 6 +-
.../src/FSharp.Editor/Common/Logging.fs | 37 ++---
.../LanguageService/LanguageService.fs | 14 +-
5 files changed, 114 insertions(+), 71 deletions(-)
diff --git a/src/Compiler/Driver/CompilerImports.fs b/src/Compiler/Driver/CompilerImports.fs
index 41cffb15506..4ab1ca3d7e4 100644
--- a/src/Compiler/Driver/CompilerImports.fs
+++ b/src/Compiler/Driver/CompilerImports.fs
@@ -1304,7 +1304,7 @@ and [] TcImports
true
| None -> false
| None -> false
-
+
member internal _.Base =
CheckDisposed()
importsBase
diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs
index 372750de78f..0e4be27ecdc 100644
--- a/src/Compiler/Utilities/Caches.fs
+++ b/src/Compiler/Utilities/Caches.fs
@@ -48,7 +48,13 @@ type internal CachedEntity<'Key, 'Value> =
val mutable AccessCount: int64
val mutable Node: LinkedListNode>
- private new(key, value) = { Key = key; Value = value; AccessCount = 0L; Node = Unchecked.defaultof<_> }
+ private new(key, value) =
+ {
+ Key = key
+ Value = value
+ AccessCount = 0L
+ Node = Unchecked.defaultof<_>
+ }
static member Create(key, value) =
let entity = CachedEntity(key, value)
@@ -69,19 +75,22 @@ type internal EvictionQueue<'Key, 'Value>() =
let pool = Queue>()
member _.Acquire(key, value) =
- lock pool <| fun () ->
- if pool.Count > 0 then
- pool.Dequeue().ReUse(key, value)
- else
- CachedEntity.Create<_, _>(key, value)
+ lock pool
+ <| fun () ->
+ if pool.Count > 0 then
+ pool.Dequeue().ReUse(key, value)
+ else
+ CachedEntity.Create<_, _>(key, value)
member _.Add(entity: CachedEntity<'Key, 'Value>) =
- lock list <| fun () ->
+ lock list
+ <| fun () ->
if isNull entity.Node.List then
list.AddLast(entity.Node)
member _.Update(entity: CachedEntity<'Key, 'Value>, strategy: CachingStrategy) =
- lock list <| fun () ->
+ lock list
+ <| fun () ->
entity.AccessCount <- entity.AccessCount + 1L
let node = entity.Node
@@ -95,34 +104,40 @@ type internal EvictionQueue<'Key, 'Value>() =
// Bubble up the node in the list, linear time.
// TODO: frequency list approach would be faster.
while (isNotNull node.Next) && (node.Next.Value.AccessCount < node.Value.AccessCount) do
- list.Remove(node)
- list.AddAfter(node.Next, node)
+ list.Remove(node)
+ list.AddAfter(node.Next, node)
member _.GetKeysToEvict(count) =
- lock list <| fun () ->
- list |> Seq.map _.Key |> Seq.truncate count |> Seq.toArray
+ lock list
+ <| fun () -> list |> Seq.map _.Key |> Seq.truncate count |> Seq.toArray
- member _.Remove(entity: CachedEntity<_,_>) =
+ member _.Remove(entity: CachedEntity<_, _>) =
lock list <| fun () -> list.Remove(entity.Node)
// Return to the pool for reuse.
lock pool <| fun () -> pool.Enqueue(entity)
- member _.Count = list.Count
+ member _.Count = list.Count
module internal CacheMetrics =
let mutable cacheId = 0
- let addInstrumentation (store: ConcurrentDictionary<_, CachedEntity<_,_>>) =
+
+ let addInstrumentation (store: ConcurrentDictionary<_, CachedEntity<_, _>>) =
let meter = new Meter("FSharp.Compiler.Caches")
let uid = Interlocked.Increment &cacheId
- let orZero f = fun () ->
- let vs = store.Values
- if vs |> Seq.isEmpty then 0L else f vs
+ let orZero f =
+ fun () ->
+ let vs = store.Values
+ if vs |> Seq.isEmpty then 0L else f vs
let _ = meter.CreateObservableGauge($"cache{uid}", (fun () -> int64 store.Count))
- let _ = meter.CreateObservableGauge($"MFA{uid}", orZero (Seq.map _.AccessCount >> Seq.max))
- let _ = meter.CreateObservableGauge($"LFA{uid}", orZero (Seq.map _.AccessCount >> Seq.min))
+
+ let _ =
+ meter.CreateObservableGauge($"MFA{uid}", orZero (Seq.map _.AccessCount >> Seq.max))
+
+ let _ =
+ meter.CreateObservableGauge($"LFA{uid}", orZero (Seq.map _.AccessCount >> Seq.min))
let mutable evictions = 0L
let mutable fails = 0L
@@ -136,36 +151,50 @@ module internal CacheMetrics =
hit |> Event.add (fun _ -> Interlocked.Increment &hits |> ignore)
miss |> Event.add (fun _ -> Interlocked.Increment &misses |> ignore)
- let _ = meter.CreateObservableGauge($"evicted{uid}", fun () -> Interlocked.Exchange(&evictions, 0L))
- let _ = meter.CreateObservableGauge($"fails{uid}", fun () -> Interlocked.Exchange(&fails, 0L))
- let _ = meter.CreateObservableGauge($"hits{uid}", fun () -> Interlocked.Exchange(&hits, 0L))
- let _ = meter.CreateObservableGauge($"misses{uid}", fun () -> Interlocked.Exchange(&misses, 0L))
+ let _ =
+ meter.CreateObservableGauge($"evicted{uid}", fun () -> Interlocked.Exchange(&evictions, 0L))
+
+ let _ =
+ meter.CreateObservableGauge($"fails{uid}", fun () -> Interlocked.Exchange(&fails, 0L))
+
+ let _ =
+ meter.CreateObservableGauge($"hits{uid}", fun () -> Interlocked.Exchange(&hits, 0L))
+
+ let _ =
+ meter.CreateObservableGauge($"misses{uid}", fun () -> Interlocked.Exchange(&misses, 0L))
+
()
[]
[]
-type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> private (options: CacheOptions, capacity, cts: CancellationTokenSource) =
+type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
+ private (options: CacheOptions, capacity, cts: CancellationTokenSource) =
let cacheHit = Event<_ * _>()
let cacheMiss = Event<_>()
let eviction = Event<_>()
let evictionFail = Event<_>()
-
- let store = ConcurrentDictionary<'Key, CachedEntity<'Key, 'Value>>(options.LevelOfConcurrency, capacity)
+
+ let store =
+ ConcurrentDictionary<'Key, CachedEntity<'Key, 'Value>>(options.LevelOfConcurrency, capacity)
let evictionQueue = EvictionQueue<'Key, 'Value>()
let tryEvictItems () =
- let count = if store.Count > options.MaximumCapacity then store.Count - options.MaximumCapacity else 0
+ let count =
+ if store.Count > options.MaximumCapacity then
+ store.Count - options.MaximumCapacity
+ else
+ 0
+
for key in evictionQueue.GetKeysToEvict(count) do
match store.TryRemove(key) with
| true, removed ->
evictionQueue.Remove(removed)
eviction.Trigger(key)
- | _ ->
- evictionFail.Trigger(key)
+ | _ -> evictionFail.Trigger(key)
- let rec backgroundEviction () =
+ let rec backgroundEviction () =
async {
tryEvictItems ()
@@ -178,28 +207,29 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> private
let delay = 1000.0 - (1000.0 * utilization)
if delay > 0.0 then
- do! Async.Sleep (int delay)
+ do! Async.Sleep(int delay)
return! backgroundEviction ()
}
- do if options.EvictionMethod = EvictionMethod.Background then
- Async.Start(backgroundEviction (), cancellationToken = cts.Token)
+ do
+ if options.EvictionMethod = EvictionMethod.Background then
+ Async.Start(backgroundEviction (), cancellationToken = cts.Token)
do CacheMetrics.addInstrumentation store eviction.Publish cacheHit.Publish cacheMiss.Publish evictionFail.Publish
let tryEvict () =
- if options.EvictionMethod.IsBlocking then tryEvictItems()
+ if options.EvictionMethod.IsBlocking then
+ tryEvictItems ()
let tryGet (key: 'Key) =
match store.TryGetValue(key) with
| true, cachedEntity ->
evictionQueue.Update(cachedEntity, options.Strategy)
Some cachedEntity
- | _ ->
- None
+ | _ -> None
- member _.TryGetValue (key: 'Key, value: outref<'Value>) =
+ member _.TryGetValue(key: 'Key, value: outref<'Value>) =
match tryGet key with
| Some cachedEntity ->
cacheHit.Trigger(key, cachedEntity.Value)
@@ -211,9 +241,10 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> private
false
member _.TryAdd(key: 'Key, value: 'Value) =
- tryEvict()
+ tryEvict ()
let cachedEntity = evictionQueue.Acquire(key, value)
+
if store.TryAdd(key, cachedEntity) then
evictionQueue.Add(cachedEntity)
true
@@ -221,13 +252,17 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> private
false
member _.AddOrUpdate(key: 'Key, value: 'Value) =
- tryEvict()
+ tryEvict ()
+
+ let entity =
+ store.AddOrUpdate(
+ key,
+ (fun _ -> evictionQueue.Acquire(key, value)),
+ (fun _ (current: CachedEntity<_, _>) ->
+ current.Value <- value
+ current)
+ )
- let entity = store.AddOrUpdate(
- key,
- (fun _ -> evictionQueue.Acquire(key, value)),
- (fun _ (current: CachedEntity<_, _>) -> current.Value <- value; current)
- )
evictionQueue.Add(entity)
[]
@@ -255,4 +290,3 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> private
member _.Dispose() = cts.Cancel()
member this.Dispose() = (this :> IDisposable).Dispose()
-
diff --git a/src/Compiler/Utilities/TypeHashing.fs b/src/Compiler/Utilities/TypeHashing.fs
index 1475635972d..37c59b48207 100644
--- a/src/Compiler/Utilities/TypeHashing.fs
+++ b/src/Compiler/Utilities/TypeHashing.fs
@@ -129,9 +129,9 @@ module rec HashTypes =
let rec stampEquals g ty1 ty2 =
match (stripTyEqns g ty1), (stripTyEqns g ty2) with
| TType_app(tcref1, tinst1, _), TType_app(tcref2, tinst2, _) ->
- tcref1.Stamp = tcref2.Stamp &&
- tinst1.Length = tinst2.Length &&
- tinst1 |> List.zip tinst2 |> List.forall (fun (t1, t2) -> stampEquals g t1 t2)
+ tcref1.Stamp = tcref2.Stamp
+ && tinst1.Length = tinst2.Length
+ && tinst1 |> List.zip tinst2 |> List.forall (fun (t1, t2) -> stampEquals g t1 t2)
| TType_var(r1, _), TType_var(r2, _) -> r1.Stamp.Equals(r2.Stamp)
| _ -> false
diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs
index beddfb8f823..0ddca9ed070 100644
--- a/vsintegration/src/FSharp.Editor/Common/Logging.fs
+++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs
@@ -150,30 +150,39 @@ module FSharpServiceTelemetry =
let logCacheMetricsToOutput () =
let instruments = Collections.Generic.Dictionary()
- let listener = new MeterListener(
- InstrumentPublished = fun instrument l ->
- if instrument.Meter.Name = "FSharp.Compiler.Caches" then
- instruments[instrument.Name] <- 0L
- l.EnableMeasurementEvents(instrument)
- )
+
+ let listener =
+ new MeterListener(
+ InstrumentPublished =
+ fun instrument l ->
+ if instrument.Meter.Name = "FSharp.Compiler.Caches" then
+ instruments[instrument.Name] <- 0L
+ l.EnableMeasurementEvents(instrument)
+ )
let callBack = MeasurementCallback(fun instr v _ _ -> instruments[instr.Name] <- v)
listener.SetMeasurementEventCallback callBack
listener.Start()
-
+
let msg = Event()
backgroundTask {
while true do
do! System.Threading.Tasks.Task.Delay(1000)
listener.RecordObservableInstruments()
+
if instruments.Count > 0 then
- [ for kvp in instruments -> $"{kvp.Key}: {kvp.Value}"]
+ [ for kvp in instruments -> $"{kvp.Key}: {kvp.Value}" ]
|> String.concat ", "
|> msg.Trigger
- } |> ignore
+ }
+ |> ignore
- msg.Publish |> Event.pairwise |> Event.filter (fun (x, y) -> x <> y) |> Event.map snd |> Event.add logMsg
+ msg.Publish
+ |> Event.pairwise
+ |> Event.filter (fun (x, y) -> x <> y)
+ |> Event.map snd
+ |> Event.add logMsg
#if DEBUG
open OpenTelemetry.Resources
@@ -183,10 +192,8 @@ module FSharpServiceTelemetry =
let export () =
let meterProvider =
// Configure OpenTelemetry metrics. Metrics can be viewed in Prometheus or other compatible tools.
- OpenTelemetry.Sdk
- .CreateMeterProviderBuilder()
- .AddOtlpExporter()
- .Build()
+ OpenTelemetry.Sdk.CreateMeterProviderBuilder().AddOtlpExporter().Build()
+
let tracerProvider =
// Configure OpenTelemetry export. Traces can be viewed in Jaeger or other compatible tools.
OpenTelemetry.Sdk
@@ -205,4 +212,4 @@ module FSharpServiceTelemetry =
meterProvider.Dispose()
let listenToAll () = listen ""
-#endif
\ No newline at end of file
+#endif
diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
index 738cf2f6135..0c75a92552e 100644
--- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
+++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs
@@ -343,15 +343,17 @@ type internal FSharpPackage() as this =
// FSI-LINKAGE-POINT: unsited init
do FSharp.Interactive.Hooks.fsiConsoleWindowPackageCtorUnsited (this :> Package)
- do Logging.FSharpServiceTelemetry.logCacheMetricsToOutput()
+ do Logging.FSharpServiceTelemetry.logCacheMetricsToOutput ()
- #if DEBUG
- let flushTelemetry = Logging.FSharpServiceTelemetry.export()
+#if DEBUG
+ let flushTelemetry = Logging.FSharpServiceTelemetry.export ()
- override this.Dispose (disposing: bool) =
+ override this.Dispose(disposing: bool) =
base.Dispose(disposing: bool)
- if disposing then flushTelemetry()
- #endif
+
+ if disposing then
+ flushTelemetry ()
+#endif
override this.InitializeAsync(cancellationToken: CancellationToken, progress: IProgress) : Tasks.Task =
// `base.` methods can't be called in the `async` builder, so we have to cache it
From 474081ae669b856d21384c07c25a037bd8b8770d Mon Sep 17 00:00:00 2001
From: Jakub Majocha <1760221+majocha@users.noreply.github.com>
Date: Thu, 17 Apr 2025 23:46:57 +0200
Subject: [PATCH 28/44] ilverify
---
...lverify_FSharp.Compiler.Service_Debug_net9.0.bsl | 13 ++++++-------
...FSharp.Compiler.Service_Debug_netstandard2.0.bsl | 13 ++++++-------
...erify_FSharp.Compiler.Service_Release_net9.0.bsl | 12 ++++++------
...harp.Compiler.Service_Release_netstandard2.0.bsl | 13 ++++++-------
4 files changed, 24 insertions(+), 27 deletions(-)
diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl
index 219a0ec11db..bd581b3d765 100644
--- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl
+++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl
@@ -5,7 +5,6 @@
[IL]: Error [UnmanagedPointer]: : FSharp.Compiler.IO.RawByteMemory::.ctor(uint8*, int32, object)][offset 0x00000009] Unmanaged pointers are not a verifiable type.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::get_Item(int32)][offset 0x0000001E][found Native Int] Expected ByRef on the stack.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::set_Item(int32, uint8)][offset 0x00000025][found Native Int] Expected ByRef on the stack.
-[IL]: Error [StackUnexpected]: : FSharp.Compiler.Cache`2::TryGetPickToEvict()][offset 0x0000005A][found ref 'object'][expected ref '[S.P.CoreLib]System.Collections.Generic.IEnumerable`1>>'] Unexpected type on the stack.
[IL]: Error [ReturnPtrToStack]: : Internal.Utilities.Text.Lexing.LexBuffer`1::get_LexemeView()][offset 0x00000019] Return type is ByRef, TypedReference, ArgHandle, or ArgIterator.
[IL]: Error [StackUnexpected]: : Internal.Utilities.Text.Lexing.UnicodeTables::scanUntilSentinel([FSharp.Compiler.Service]Internal.Utilities.Text.Lexing.LexBuffer`1, int32)][offset 0x00000079][found Short] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Xml.XmlDoc::processLines([FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1)][offset 0x00000031][found Char] Unexpected type on the stack.
@@ -22,14 +21,14 @@
[IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x00000082][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x0000008B][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+MagicAssemblyResolution::ResolveAssemblyCore([FSharp.Compiler.Service]Internal.Utilities.Library.CompilationThreadToken, [FSharp.Compiler.Service]FSharp.Compiler.Text.Range, [FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, [FSharp.Compiler.Service]FSharp.Compiler.CompilerImports+TcImports, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompiler, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiConsoleOutput, string)][offset 0x00000015][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3502-805::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001E5][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3502-812::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001E5][found Char] Unexpected type on the stack.
[IL]: Error [UnmanagedPointer]: : FSharp.Compiler.Interactive.Shell+Utilities+pointerToNativeInt@110::Invoke(object)][offset 0x00000007] Unmanaged pointers are not a verifiable type.
[IL]: Error [StackUnexpected]: : .$FSharpCheckerResults+dataTipOfReferences@2225::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000084][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-509::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000032][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-509::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000003B][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-509::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000082][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-509::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000008B][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-509::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000094][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-516::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000032][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-516::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000003B][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-516::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000082][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-516::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000008B][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-516::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000094][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.StaticLinking+TypeForwarding::followTypeForwardForILTypeRef([FSharp.Compiler.Service]FSharp.Compiler.AbstractIL.IL+ILTypeRef)][offset 0x00000010][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.CompilerOptions::getCompilerOption([FSharp.Compiler.Service]FSharp.Compiler.CompilerOptions+CompilerOption, [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1)][offset 0x000000E6][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.CompilerOptions::AddPathMapping([FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, string)][offset 0x0000000B][found Char] Unexpected type on the stack.
diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl
index 209cabb338b..752b1e98415 100644
--- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl
+++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl
@@ -5,7 +5,6 @@
[IL]: Error [UnmanagedPointer]: : FSharp.Compiler.IO.RawByteMemory::.ctor(uint8*, int32, object)][offset 0x00000009] Unmanaged pointers are not a verifiable type.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::get_Item(int32)][offset 0x0000001E][found Native Int] Expected ByRef on the stack.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::set_Item(int32, uint8)][offset 0x00000025][found Native Int] Expected ByRef on the stack.
-[IL]: Error [StackUnexpected]: : FSharp.Compiler.Cache`2::TryGetPickToEvict()][offset 0x0000005A][found ref 'object'][expected ref '[S.P.CoreLib]System.Collections.Generic.IEnumerable`1>>'] Unexpected type on the stack.
[IL]: Error [ReturnPtrToStack]: : Internal.Utilities.Text.Lexing.LexBuffer`1::get_LexemeView()][offset 0x00000019] Return type is ByRef, TypedReference, ArgHandle, or ArgIterator.
[IL]: Error [StackUnexpected]: : Internal.Utilities.Text.Lexing.UnicodeTables::scanUntilSentinel([FSharp.Compiler.Service]Internal.Utilities.Text.Lexing.LexBuffer`1, int32)][offset 0x00000079][found Short] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Xml.XmlDoc::processLines([FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1)][offset 0x00000031][found Char] Unexpected type on the stack.
@@ -29,18 +28,18 @@
[IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x0000008B][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+FsiStdinSyphon::GetLine(string, int32)][offset 0x00000039][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+MagicAssemblyResolution::ResolveAssemblyCore([FSharp.Compiler.Service]Internal.Utilities.Library.CompilationThreadToken, [FSharp.Compiler.Service]FSharp.Compiler.Text.Range, [FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, [FSharp.Compiler.Service]FSharp.Compiler.CompilerImports+TcImports, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompiler, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiConsoleOutput, string)][offset 0x00000015][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3502-805::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001E5][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3502-812::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001E5][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+FsiInteractionProcessor::CompletionsForPartialLID([FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompilerState, string)][offset 0x0000001B][found Char] Unexpected type on the stack.
[IL]: Error [UnmanagedPointer]: : FSharp.Compiler.Interactive.Shell+Utilities+pointerToNativeInt@110::Invoke(object)][offset 0x00000007] Unmanaged pointers are not a verifiable type.
[IL]: Error [StackUnexpected]: : .$FSharpCheckerResults+dataTipOfReferences@2225::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000084][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.EditorServices.AssemblyContent+traverseMemberFunctionAndValues@176::Invoke([FSharp.Compiler.Service]FSharp.Compiler.Symbols.FSharpMemberOrFunctionOrValue)][offset 0x00000059][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.EditorServices.AssemblyContent+traverseEntity@218::GenerateNext([S.P.CoreLib]System.Collections.Generic.IEnumerable`1&)][offset 0x000000DA][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.EditorServices.ParsedInput+visitor@1424-6::VisitExpr([FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1, [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2>, [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2>, [FSharp.Compiler.Service]FSharp.Compiler.Syntax.SynExpr)][offset 0x00000605][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-509::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000032][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-509::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000003B][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-509::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000082][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-509::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000008B][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-509::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000094][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-516::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000032][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-516::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000003B][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-516::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000082][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-516::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000008B][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-516::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000094][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : .$Symbols+fullName@2495-1::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000015][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.CreateILModule+MainModuleBuilder::ConvertProductVersionToILVersionInfo(string)][offset 0x00000011][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.StaticLinking+TypeForwarding::followTypeForwardForILTypeRef([FSharp.Compiler.Service]FSharp.Compiler.AbstractIL.IL+ILTypeRef)][offset 0x00000010][found Char] Unexpected type on the stack.
diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_net9.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_net9.0.bsl
index 4e7b5396676..d171cb2277a 100644
--- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_net9.0.bsl
+++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_net9.0.bsl
@@ -21,13 +21,13 @@
[IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x00000082][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x0000008B][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+MagicAssemblyResolution::ResolveAssemblyCore([FSharp.Compiler.Service]Internal.Utilities.Library.CompilationThreadToken, [FSharp.Compiler.Service]FSharp.Compiler.Text.Range, [FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, [FSharp.Compiler.Service]FSharp.Compiler.CompilerImports+TcImports, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompiler, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiConsoleOutput, string)][offset 0x00000015][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3502-849::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001C7][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3502-856::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001C7][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : .$FSharpCheckerResults+GetReferenceResolutionStructuredToolTipText@2225::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000076][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000032][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000003B][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000064][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000006D][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000076][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-537::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000032][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-537::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000003B][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-537::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000064][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-537::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000006D][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-537::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000076][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Driver+ProcessCommandLineFlags@291-1::Invoke(string)][offset 0x0000000B][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Driver+ProcessCommandLineFlags@291-1::Invoke(string)][offset 0x00000014][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.StaticLinking+TypeForwarding::followTypeForwardForILTypeRef([FSharp.Compiler.Service]FSharp.Compiler.AbstractIL.IL+ILTypeRef)][offset 0x00000010][found Char] Unexpected type on the stack.
diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl
index 8d926f8c113..3b30273904b 100644
--- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl
+++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl
@@ -5,7 +5,6 @@
[IL]: Error [UnmanagedPointer]: : FSharp.Compiler.IO.RawByteMemory::.ctor(uint8*, int32, object)][offset 0x00000009] Unmanaged pointers are not a verifiable type.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::get_Item(int32)][offset 0x0000001A][found Native Int] Expected ByRef on the stack.
[IL]: Error [StackByRef]: : FSharp.Compiler.IO.RawByteMemory::set_Item(int32, uint8)][offset 0x0000001B][found Native Int] Expected ByRef on the stack.
-[IL]: Error [StackUnexpected]: : FSharp.Compiler.Cache`2::TryGetPickToEvict()][offset 0x00000034][found ref 'object'][expected ref '[S.P.CoreLib]System.Collections.Generic.IEnumerable`1>>'] Unexpected type on the stack.
[IL]: Error [ReturnPtrToStack]: : Internal.Utilities.Text.Lexing.LexBuffer`1::get_LexemeView()][offset 0x00000017] Return type is ByRef, TypedReference, ArgHandle, or ArgIterator.
[IL]: Error [StackUnexpected]: : Internal.Utilities.Text.Lexing.UnicodeTables::scanUntilSentinel([FSharp.Compiler.Service]Internal.Utilities.Text.Lexing.LexBuffer`1, int32)][offset 0x0000008D][found Short] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Xml.XmlDoc::processLines([FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1)][offset 0x0000002C][found Char] Unexpected type on the stack.
@@ -29,17 +28,17 @@
[IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x0000008B][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+FsiStdinSyphon::GetLine(string, int32)][offset 0x00000032][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+MagicAssemblyResolution::ResolveAssemblyCore([FSharp.Compiler.Service]Internal.Utilities.Library.CompilationThreadToken, [FSharp.Compiler.Service]FSharp.Compiler.Text.Range, [FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, [FSharp.Compiler.Service]FSharp.Compiler.CompilerImports+TcImports, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompiler, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiConsoleOutput, string)][offset 0x00000015][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3502-849::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001C7][found Char] Unexpected type on the stack.
+[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3502-856::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001C7][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+FsiInteractionProcessor::CompletionsForPartialLID([FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompilerState, string)][offset 0x00000024][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : .$FSharpCheckerResults+GetReferenceResolutionStructuredToolTipText@2225::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000076][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.EditorServices.AssemblyContent+traverseMemberFunctionAndValues@176::Invoke([FSharp.Compiler.Service]FSharp.Compiler.Symbols.FSharpMemberOrFunctionOrValue)][offset 0x0000002B][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.EditorServices.AssemblyContent+traverseEntity@218::GenerateNext([S.P.CoreLib]System.Collections.Generic.IEnumerable`1&)][offset 0x000000BB][found Char] Unexpected type on the stack.
[IL]: Error [StackUnexpected]: : FSharp.Compiler.EditorServices.ParsedInput+visitor@1424-11::VisitExpr([FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1, [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2>, [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2>, [FSharp.Compiler.Service]FSharp.Compiler.Syntax.SynExpr)][offset 0x00000620][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000032][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000003B][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000064][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000006D][found Char] Unexpected type on the stack.
-[IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@921-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2