From 5233567dd362d0e95bf03c29549e62dceeb5937b Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:52:03 +0200 Subject: [PATCH 01/28] add cache --- src/Compiler/FSharp.Compiler.Service.fsproj | 2 + src/Compiler/Utilities/Caches.fs | 408 ++++++++++++++++++ src/Compiler/Utilities/Caches.fsi | 85 ++++ ...y_FSharp.Compiler.Service_Debug_net9.0.bsl | 12 +- ....Compiler.Service_Debug_netstandard2.0.bsl | 12 +- ...FSharp.Compiler.Service_Release_net9.0.bsl | 12 +- ...ompiler.Service_Release_netstandard2.0.bsl | 12 +- 7 files changed, 519 insertions(+), 24 deletions(-) create mode 100644 src/Compiler/Utilities/Caches.fs create mode 100644 src/Compiler/Utilities/Caches.fsi diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index 74e59954e8f..9754dd25a11 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -146,6 +146,8 @@ + + diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs new file mode 100644 index 00000000000..97f2b08d8af --- /dev/null +++ b/src/Compiler/Utilities/Caches.fs @@ -0,0 +1,408 @@ +// LinkedList uses nulls, so we need to disable the nullability warnings for this file. +namespace FSharp.Compiler + +open System +open System.Collections.Generic +open System.Collections.Concurrent +open System.Threading +open System.Diagnostics +open System.Diagnostics.Metrics + +[] +type CachingStrategy = + | LRU + | LFU + +[] +type EvictionMethod = + | Blocking + | Background + | NoEviction + +[] +type CacheOptions = + { + MaximumCapacity: int + PercentageToEvict: int + Strategy: CachingStrategy + EvictionMethod: EvictionMethod + LevelOfConcurrency: int + } + + static member Default = + { + MaximumCapacity = 1024 + PercentageToEvict = 5 + Strategy = CachingStrategy.LRU + LevelOfConcurrency = Environment.ProcessorCount + EvictionMethod = EvictionMethod.Blocking + } + +[] +[] +type CachedEntity<'Key, 'Value> = + val mutable Key: 'Key + val mutable Value: 'Value + val mutable AccessCount: int64 + val mutable Node: LinkedListNode> + + new(key, value) = + { + Key = key + Value = value + AccessCount = 0L + Node = Unchecked.defaultof<_> + } + + member this.WithNode() = + this.Node <- LinkedListNode(this) + this + + member this.ReUse(key, value) = + this.Key <- key + this.Value <- value + this.AccessCount <- 0L + this + + override this.ToString() = $"{this.Key}" + +type EntityPool<'Key, 'Value>(maximumCapacity, overCapacity: Event<_>) = + let pool = ConcurrentBag>() + let mutable created = 0 + + member _.Acquire(key, value) = + match pool.TryTake() with + | true, entity -> entity.ReUse(key, value) + | _ -> + if Interlocked.Increment &created > maximumCapacity then + overCapacity.Trigger() + + CachedEntity(key, value).WithNode() + + member _.Reclaim(entity: CachedEntity<'Key, 'Value>) = + if pool.Count < maximumCapacity then + pool.Add(entity) + +type IEvictionQueue<'Key, 'Value> = + abstract member Add: CachedEntity<'Key, 'Value> * CachingStrategy -> unit + abstract member Update: CachedEntity<'Key, 'Value> -> unit + abstract member GetKeysToEvict: int -> 'Key[] + abstract member Remove: CachedEntity<'Key, 'Value> -> unit + +[] +type EvictionQueue<'Key, 'Value>(strategy: CachingStrategy) = + + let list = LinkedList>() + + interface IEvictionQueue<'Key, 'Value> with + + member _.Add(entity: CachedEntity<'Key, 'Value>, strategy) = + lock list + <| fun () -> + if isNull entity.Node.List then + match strategy with + | CachingStrategy.LRU -> list.AddLast(entity.Node) + | CachingStrategy.LFU -> list.AddLast(entity.Node) + // list.AddFirst(entity.Node) + + member _.Update(entity: CachedEntity<'Key, 'Value>) = + lock list + <| fun () -> + Interlocked.Increment(&entity.AccessCount) |> ignore + + let node = entity.Node + + // Sync between store and the eviction queue is not atomic. It might be already evicted or not yet added. + if node.List = list then + + 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. + let rec bubbleUp (current: LinkedListNode>) = + match current.Next with + | NonNull next when next.Value.AccessCount < entity.AccessCount -> bubbleUp next + | _ -> current + + let next = bubbleUp node + + if next <> node then + list.Remove(node) + list.AddAfter(next, node) + + member _.GetKeysToEvict(count) = + lock list + <| fun () -> list |> Seq.map _.Key |> Seq.truncate count |> Seq.toArray + + member this.Remove(entity: CachedEntity<_, _>) = + lock list <| fun () -> list.Remove(entity.Node) + + member _.Count = list.Count + + static member NoEviction = + { new IEvictionQueue<'Key, 'Value> with + member _.Add(_, _) = () + + member _.Update(entity) = + Interlocked.Increment(&entity.AccessCount) |> ignore + + member _.GetKeysToEvict(_) = [||] + member _.Remove(_) = () + } + +type ICacheEvents = + [] + abstract member CacheHit: IEvent + + [] + abstract member CacheMiss: IEvent + + [] + abstract member Eviction: IEvent + + [] + abstract member EvictionFail: IEvent + + [] + abstract member OverCapacity: IEvent + +[] +[] +type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (options: CacheOptions, capacity, cts: CancellationTokenSource) = + + let cacheHit = Event() + let cacheMiss = Event() + let eviction = Event() + let evictionFail = Event() + let overCapacity = Event() + + let pool = EntityPool<'Key, 'Value>(capacity, overCapacity) + + let store = + ConcurrentDictionary<'Key, CachedEntity<'Key, 'Value>>(options.LevelOfConcurrency, capacity) + + let evictionQueue: IEvictionQueue<'Key, 'Value> = + match options.EvictionMethod with + | EvictionMethod.NoEviction -> EvictionQueue.NoEviction + | _ -> EvictionQueue(options.Strategy) + + let tryEvictItems () = + let count = + if store.Count > options.MaximumCapacity then + (store.Count - options.MaximumCapacity) + + int (float options.MaximumCapacity * float options.PercentageToEvict / 100.0) + else + 0 + + for key in evictionQueue.GetKeysToEvict(count) do + match store.TryRemove(key) with + | true, removed -> + evictionQueue.Remove(removed) + pool.Reclaim(removed) + eviction.Trigger() + | _ -> + failwith "eviction fail" + evictionFail.Trigger() + + 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) + + member _.TryGetValue(key: 'Key, value: outref<'Value>) = + match store.TryGetValue(key) with + | true, cachedEntity -> + cacheHit.Trigger() + evictionQueue.Update(cachedEntity) + value <- cachedEntity.Value + true + | _ -> + cacheMiss.Trigger() + value <- Unchecked.defaultof<'Value> + false + + member _.TryAdd(key: 'Key, value: 'Value) = + if options.EvictionMethod.IsBlocking then + tryEvictItems () + + let cachedEntity = pool.Acquire(key, value) + + if store.TryAdd(key, cachedEntity) then + evictionQueue.Add(cachedEntity, options.Strategy) + true + else + pool.Reclaim(cachedEntity) + false + + member _.AddOrUpdate(key: 'Key, value: 'Value) = + if options.EvictionMethod.IsBlocking then + tryEvictItems () + + let aquired = pool.Acquire(key, value) + + let entity = + store.AddOrUpdate( + key, + (fun _ -> aquired), + (fun _ (current: CachedEntity<_, _>) -> + pool.Reclaim aquired + current.Value <- value + evictionQueue.Remove(current) + current) + ) + + evictionQueue.Add(entity, options.Strategy) + + interface ICacheEvents with + + [] + member val CacheHit = cacheHit.Publish + + [] + member val CacheMiss = cacheMiss.Publish + + [] + member val Eviction = eviction.Publish + + [] + member val EvictionFail = evictionFail.Publish + + [] + member val OverCapacity = overCapacity.Publish + + interface IDisposable with + member this.Dispose() = + cts.Cancel() + CacheInstrumentation.RemoveInstrumentation(this) + GC.SuppressFinalize(this) + + member this.Dispose() = (this :> IDisposable).Dispose() + + override this.Finalize() : unit = this.Dispose() + + static member Create<'Key, 'Value>(options: CacheOptions) = + // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary. + let capacity = + options.MaximumCapacity + + int (float options.MaximumCapacity * float options.PercentageToEvict / 100.0) + + let cts = new CancellationTokenSource() + let cache = new Cache<'Key, 'Value>(options, capacity, cts) + CacheInstrumentation.AddInstrumentation cache |> ignore + cache + + member this.GetStats() = CacheInstrumentation.GetStats(this) + +and CacheInstrumentation(cache: ICacheEvents) = + static let mutable cacheId = 0 + + static let instrumentedCaches = ConcurrentDictionary() + + static let meter = new Meter(nameof CacheInstrumentation) + + let instanceId = $"cache-{Interlocked.Increment(&cacheId)}" + + let hits = meter.CreateCounter("hits", "count", instanceId) + let misses = meter.CreateCounter("misses", "count", instanceId) + let evictions = meter.CreateCounter("evictions", "count", instanceId) + + let evictionFails = + meter.CreateCounter("eviction-fails", "count", instanceId) + + let overCapacity = meter.CreateCounter("over-capacity", "count", instanceId) + + do + cache.CacheHit.Add <| fun _ -> hits.Add(1L) + cache.CacheMiss.Add <| fun _ -> misses.Add(1L) + cache.Eviction.Add <| fun _ -> evictions.Add(1L) + cache.EvictionFail.Add <| fun _ -> evictionFails.Add(1L) + cache.OverCapacity.Add <| fun _ -> overCapacity.Add(1L) + + let current = ConcurrentDictionary() + + let listener = + new MeterListener( + InstrumentPublished = + fun i l -> + if i.Meter = meter && i.Description = instanceId then + l.EnableMeasurementEvents(i) + ) + + do + listener.SetMeasurementEventCallback(fun k v _ _ -> Interlocked.Add(current.GetOrAdd(k, ref 0L), v) |> ignore) + listener.Start() + + member val CacheId = instanceId + + member val RecentStats = "-" with get, set + + member this.TryUpdateStats(clearCounts) = + let stats = + try + let ratio = + float current[hits].Value / float (current[hits].Value + current[misses].Value) + * 100.0 + + [ + for i in current.Keys do + let v = current[i].Value + + if v > 0 then + $"{i.Name}: {v}" + ] + |> String.concat ", " + |> sprintf "%s | hit ratio: %s %s" this.CacheId (if Double.IsNaN(ratio) then "-" else $"%.1f{ratio}%%") + with _ -> + "!" + + if clearCounts then + for r in current.Values do + Interlocked.Exchange(r, 0L) |> ignore + + if stats <> this.RecentStats then + this.RecentStats <- stats + true + else + false + + member this.Dispose() = listener.Dispose() + + static member GetStats(cache: ICacheEvents) = + instrumentedCaches[cache].TryUpdateStats(false) |> ignore + instrumentedCaches[cache].RecentStats + + static member GetStatsUpdateForAllCaches(clearCounts) = + [ + for i in instrumentedCaches.Values do + if i.TryUpdateStats(clearCounts) then + i.RecentStats + ] + |> String.concat "\n" + + static member AddInstrumentation(cache: ICacheEvents) = + instrumentedCaches[cache] <- new CacheInstrumentation(cache) + + static member RemoveInstrumentation(cache: ICacheEvents) = + instrumentedCaches[cache].Dispose() + instrumentedCaches.TryRemove(cache) |> ignore diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi new file mode 100644 index 00000000000..c2dd99fe041 --- /dev/null +++ b/src/Compiler/Utilities/Caches.fsi @@ -0,0 +1,85 @@ +namespace FSharp.Compiler + +open System +open System.Threading + +[] +type internal CachingStrategy = + | LRU + | LFU + +[] +type internal EvictionMethod = + | Blocking + | Background + | NoEviction + +[] +type internal CacheOptions = + { MaximumCapacity: int + PercentageToEvict: int + Strategy: CachingStrategy + EvictionMethod: EvictionMethod + LevelOfConcurrency: int } + + static member Default: CacheOptions + +[] +type internal CachedEntity<'Key, 'Value> = + new: key: 'Key * value: 'Value -> CachedEntity<'Key, 'Value> + member WithNode: unit -> CachedEntity<'Key, 'Value> + member ReUse: key: 'Key * value: 'Value -> CachedEntity<'Key, 'Value> + override ToString: unit -> string + +type internal IEvictionQueue<'Key, 'Value> = + abstract member Add: CachedEntity<'Key, 'Value> * CachingStrategy -> unit + abstract member Update: CachedEntity<'Key, 'Value> -> unit + abstract member GetKeysToEvict: int -> 'Key[] + abstract member Remove: CachedEntity<'Key, 'Value> -> unit + +[] +type internal EvictionQueue<'Key, 'Value> = + new: strategy: CachingStrategy -> EvictionQueue<'Key, 'Value> + member Count: int + static member NoEviction: IEvictionQueue<'Key, 'Value> + interface IEvictionQueue<'Key, 'Value> + +type internal ICacheEvents = + [] + abstract member CacheHit: IEvent + + [] + abstract member CacheMiss: IEvent + + [] + abstract member Eviction: IEvent + + [] + abstract member EvictionFail: IEvent + + [] + abstract member OverCapacity: IEvent + +[] +type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = + new: options: CacheOptions * capacity: int * cts: CancellationTokenSource -> Cache<'Key, 'Value> + member TryGetValue: key: 'Key * value: outref<'Value> -> bool + member TryAdd: key: 'Key * value: 'Value -> bool + member AddOrUpdate: key: 'Key * value: 'Value -> unit + member Dispose: unit -> unit + member GetStats: unit -> string + + static member Create<'Key, 'Value> : options: CacheOptions -> Cache<'Key, 'Value> + + interface ICacheEvents + interface IDisposable + +type internal CacheInstrumentation = + new: cache: ICacheEvents -> CacheInstrumentation + member CacheId: string + member RecentStats: string + member TryUpdateStats: clearCounts: bool -> bool + static member GetStats: cache: ICacheEvents -> string + static member GetStatsUpdateForAllCaches: clearCounts: bool -> string + static member AddInstrumentation: cache: ICacheEvents -> unit + static member RemoveInstrumentation: cache: ICacheEvents -> unit 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..bd581b3d765 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl @@ -21,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 6e41547cd11..752b1e98415 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl @@ -28,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 431d4e5512a..3b30273904b 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl @@ -28,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,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]: : .$Symbols+fullName@2495-3::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000030][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. From df70074078875614098e725d58b78aa55ee14633 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:55:30 +0200 Subject: [PATCH 02/28] enable typeSubsumptionCache in IDE --- src/Compiler/Checking/TypeRelations.fs | 8 ++-- src/Compiler/Checking/import.fs | 66 ++++++++++++++++++-------- src/Compiler/Checking/import.fsi | 9 ++-- src/Compiler/Utilities/TypeHashing.fs | 46 +++++++++++------- 4 files changed, 84 insertions(+), 45 deletions(-) diff --git a/src/Compiler/Checking/TypeRelations.fs b/src/Compiler/Checking/TypeRelations.fs index 2cb5dd4057a..fa900069508 100644 --- a/src/Compiler/Checking/TypeRelations.fs +++ b/src/Compiler/Checking/TypeRelations.fs @@ -102,7 +102,7 @@ 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 + if g.langVersion.SupportsFeature LanguageFeature.UseTypeSubsumptionCache then match amap.TypeSubsumptionCache.TryGetValue(key) with | true, subsumes -> ValueSome subsumes @@ -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.AddOrUpdate(key, subsumes) /// The feasible coercion relation. Part of the language spec. let rec TypeFeasiblySubsumesType ndeep (g: TcGlobals) (amap: ImportMap) m (ty1: TType) (canCoerce: CanCoerce) (ty2: TType) = @@ -125,7 +125,7 @@ let rec TypeFeasiblySubsumesType ndeep (g: TcGlobals) (amap: ImportMap) m (ty1: let ty2 = stripTyEqns g ty2 // Check if language feature supported - let key = TTypeCacheKey.FromStrippedTypes (ty1, ty2, canCoerce, g) + let key = TTypeCacheKey.FromStrippedTypes (ty1, ty2, canCoerce) match TryGetCachedTypeSubsumption g amap key with | ValueSome subsumes -> diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index c87d6cdad03..1b3ccbd64fb 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -6,17 +6,22 @@ module internal FSharp.Compiler.Import open System.Collections.Concurrent open System.Collections.Generic open System.Collections.Immutable -open FSharp.Compiler.Text.Range +open System.Diagnostics +open System.Runtime.CompilerServices +open System.Threading + 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 @@ -52,18 +57,18 @@ type CanCoerce = | CanCoerce | NoCoerce -type [] TTypeCacheKey = +[] +type TTypeCacheKey = val ty1: TType val ty2: TType val canCoerce: CanCoerce - val tcGlobals: TcGlobals - private new (ty1, ty2, canCoerce, tcGlobals) = - { ty1 = ty1; ty2 = ty2; canCoerce = canCoerce; tcGlobals = tcGlobals } + private new (ty1, ty2, canCoerce) = + { ty1 = ty1; ty2 = ty2; canCoerce = canCoerce } - static member FromStrippedTypes (ty1, ty2, canCoerce, tcGlobals) = - TTypeCacheKey(ty1, ty2, canCoerce, tcGlobals) + static member FromStrippedTypes (ty1, ty2, canCoerce) = + TTypeCacheKey(ty1, ty2, canCoerce) interface System.IEquatable with member this.Equals other = @@ -72,8 +77,8 @@ type [] TTypeCacheKey = elif this.ty1 === other.ty1 && this.ty2 === other.ty2 then true else - stampEquals this.tcGlobals this.ty1 other.ty1 - && stampEquals this.tcGlobals this.ty2 other.ty2 + HashStamps.stampEquals this.ty1 other.ty1 + && HashStamps.stampEquals this.ty2 other.ty2 override this.Equals(other:objnull) = match other with @@ -81,14 +86,37 @@ 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 + HashStamps.hashTType this.ty1 + |> pipeToHash (HashStamps.hashTType this.ty2) + |> pipeToHash (hash this.canCoerce) + + override this.ToString () = $"{this.ty1.DebugText}-{this.ty2.DebugText}" + +let getOrCreateTypeSubsumptionCache = + let mutable lockObj = obj() + let mutable cache = None + + fun compilationMode -> + lock lockObj <| fun () -> + match cache with + | Some c -> c + | _ -> + let options = + match compilationMode with + | CompilationMode.OneOff -> + // This is a one-off compilation, so we don't need to worry about eviction. + { CacheOptions.Default with + MaximumCapacity = 200_000 + EvictionMethod = EvictionMethod.NoEviction } + | _ -> + // Oncremental use, so we need to set up the cache with eviction. + { CacheOptions.Default with + EvictionMethod = EvictionMethod.Background + Strategy = CachingStrategy.LRU + PercentageToEvict = 5 + MaximumCapacity = 4 * 32768 } + cache <- Some (Cache.Create(options)) + cache.Value //------------------------------------------------------------------------- // Import an IL types as F# types. @@ -106,15 +134,13 @@ type [] TTypeCacheKey = type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) = let typeRefToTyconRefCache = ConcurrentDictionary() - let typeSubsumptionCache = ConcurrentDictionary(System.Environment.ProcessorCount, 1024) - member _.g = g member _.assemblyLoader = assemblyLoader member _.ILTypeRefToTyconRefCache = typeRefToTyconRefCache - member _.TypeSubsumptionCache = typeSubsumptionCache + member val TypeSubsumptionCache: Cache = getOrCreateTypeSubsumptionCache g.compilationMode let CanImportILScopeRef (env: ImportMap) m scoref = diff --git a/src/Compiler/Checking/import.fsi b/src/Compiler/Checking/import.fsi index c387558fcba..0ba2a635ec0 100644 --- a/src/Compiler/Checking/import.fsi +++ b/src/Compiler/Checking/import.fsi @@ -45,15 +45,14 @@ type CanCoerce = [] type TTypeCacheKey = interface System.IEquatable - private new: ty1: TType * ty2: TType * canCoerce: CanCoerce * tcGlobals: TcGlobals -> TTypeCacheKey + private new: ty1: TType * ty2: TType * canCoerce: CanCoerce -> TTypeCacheKey - static member FromStrippedTypes: - ty1: TType * ty2: TType * canCoerce: CanCoerce * tcGlobals: TcGlobals -> TTypeCacheKey + static member FromStrippedTypes: ty1: TType * ty2: TType * canCoerce: CanCoerce -> TTypeCacheKey val ty1: TType val ty2: TType val canCoerce: CanCoerce - val tcGlobals: TcGlobals + //val tcGlobals: TcGlobals override GetHashCode: unit -> int /// Represents a context used for converting AbstractIL .NET and provided types to F# internal compiler data structures. @@ -73,7 +72,7 @@ type ImportMap = member g: TcGlobals /// Type subsumption cache - member TypeSubsumptionCache: ConcurrentDictionary + member TypeSubsumptionCache: Cache module Nullness = diff --git a/src/Compiler/Utilities/TypeHashing.fs b/src/Compiler/Utilities/TypeHashing.fs index bcdface38be..7639f2dd679 100644 --- a/src/Compiler/Utilities/TypeHashing.fs +++ b/src/Compiler/Utilities/TypeHashing.fs @@ -126,22 +126,6 @@ module HashAccessibility = module rec HashTypes = open Microsoft.FSharp.Core.LanguagePrimitives - let stampEquals g ty1 ty2 = - match (stripTyEqns g ty1), (stripTyEqns g ty2) with - | TType_app(tcref1, _, _), TType_app(tcref2, _, _) -> tcref1.Stamp.Equals(tcref2.Stamp) - | TType_var(r1, _), TType_var(r2, _) -> r1.Stamp.Equals(r2.Stamp) - | _ -> false - - /// Get has for Stamp for TType_app tyconref and TType_var typar - let hashStamp g ty = - let v: Stamp = - match (stripTyEqns g ty) with - | TType_app(tcref, _, _) -> tcref.Stamp - | TType_var(r, _) -> r.Stamp - | _ -> GenericZero - - hash v - /// Hash a reference to a type let hashTyconRef tcref = hashTyconRefImpl tcref @@ -344,3 +328,33 @@ module HashTastMemberOrVals = hashNonMemberVal (g, obs) (tps, vref.Deref, tau, cxs) | Some _ -> hashMember (g, obs) emptyTyparInst vref.Deref + +module HashStamps = + let rec stampEquals ty1 ty2 = + match ty1, ty2 with + | TType_app(tcref1, tinst1, _), TType_app(tcref2, tinst2, _) -> + tcref1.Stamp = tcref2.Stamp + && tinst1.Length = tinst2.Length + && (tinst1, tinst2) ||> Seq.zip |> Seq.forall (fun (t1, t2) -> stampEquals t1 t2) + + | TType_var(r1, _), TType_var(r2, _) -> r1.Stamp = r2.Stamp + | _ -> false + + let inline hashStamp (x: int64) = uint x * 2654435761u |> int + + // 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 keys or evict from the cache. + let rec hashTType ty = + match ty with + | TType_ucase(_, tinst) -> tinst |> hashListOrderMatters (hashTType) + | TType_app(tcref, tinst, _) -> tinst |> hashListOrderMatters (hashTType) |> pipeToHash (hashStamp tcref.Stamp) + | TType_anon(info, tys) -> tys |> hashListOrderMatters (hashTType) |> pipeToHash (hashStamp info.Stamp) + | TType_tuple(_, tys) -> tys |> hashListOrderMatters (hashTType) + | TType_forall(tps, tau) -> + tps + |> Seq.map _.Stamp + |> hashListOrderMatters (hashStamp) + |> pipeToHash (hashTType tau) + | TType_fun(d, r, _) -> hashTType d |> pipeToHash (hashTType r) + | TType_var(r, _) -> hashStamp r.Stamp + | TType_measure _ -> 0 From 460f5926372649957c1520e4428416dfaad51275 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:00:54 +0200 Subject: [PATCH 03/28] add some monitoring --- tests/FSharp.Test.Utilities/XunitHelpers.fs | 18 ++++- .../src/FSharp.Editor/Common/Logging.fs | 72 ++++++++++++++----- .../LanguageService/LanguageService.fs | 20 ++++-- .../FSharp.Editor.Tests.fsproj | 3 + .../tests/Salsa/VisualFSharp.Salsa.fsproj | 3 + .../UnitTests/VisualFSharp.UnitTests.fsproj | 3 + 6 files changed, 96 insertions(+), 23 deletions(-) diff --git a/tests/FSharp.Test.Utilities/XunitHelpers.fs b/tests/FSharp.Test.Utilities/XunitHelpers.fs index 34a44df17ed..c68b2f29a14 100644 --- a/tests/FSharp.Test.Utilities/XunitHelpers.fs +++ b/tests/FSharp.Test.Utilities/XunitHelpers.fs @@ -12,9 +12,9 @@ open TestFramework open FSharp.Compiler.Diagnostics -open OpenTelemetry open OpenTelemetry.Resources open OpenTelemetry.Trace +open OpenTelemetry.Metrics /// Disables custom internal parallelization added with XUNIT_EXTRAS. /// Execute test cases in a class or a module one by one instead of all at once. Allow other collections to run simultaneously. @@ -146,12 +146,17 @@ type FSharpXunitFramework(sink: IMessageSink) = AssemblyResolver.addResolver () #endif + // On Windows forwarding localhost to wsl2 docker container sometimes does not work. Use IP address instead. + let otlpEndpoint = Uri("http://127.0.0.1:4317") + // Configure OpenTelemetry export. Traces can be viewed in Jaeger or other compatible tools. use tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() .AddSource(ActivityNames.FscSourceName) .ConfigureResource(fun r -> r.AddService("F#") |> ignore) .AddOtlpExporter(fun o -> + o.Endpoint <- otlpEndpoint + o.Protocol <- OpenTelemetry.Exporter.OtlpExportProtocol.Grpc // 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 @@ -159,6 +164,17 @@ type FSharpXunitFramework(sink: IMessageSink) = ) .Build() + use meterProvider = + OpenTelemetry.Sdk.CreateMeterProviderBuilder() + .AddMeter(nameof FSharp.Compiler.CacheInstrumentation) + .ConfigureResource(fun r -> r.AddService("F#") |> ignore) + .AddOtlpExporter(fun e m -> + e.Endpoint <- otlpEndpoint + e.Protocol <- OpenTelemetry.Exporter.OtlpExportProtocol.Grpc + m.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds <- 1000 + ) + .Build() + logConfig initialConfig log "Installing TestConsole redirection" TestConsole.install() diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs index b0f56df3234..e8d0e4d5daf 100644 --- a/vsintegration/src/FSharp.Editor/Common/Logging.fs +++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs @@ -30,6 +30,8 @@ module Config = let fsharpOutputGuid = Guid fsharpOutputGuidString open Config +open System.Diagnostics.Metrics +open System.Text [] type Logger [] ([)>] serviceProvider: IServiceProvider) = @@ -118,11 +120,7 @@ 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 FSharpServiceTelemetry = let listen filter = let indent (activity: Activity) = @@ -135,16 +133,15 @@ module Activity = String.replicate (loop activity 0) " " let collectTags (activity: Activity) = - [ for tag in activity.Tags -> $"{tag.Key}: %A{tag.Value}" ] - |> String.concat ", " + [ 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 +149,56 @@ 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 logCacheMetricsToOutput () = + + let timer = new System.Timers.Timer(1000.0, AutoReset = true) + + timer.Elapsed.Add(fun _ -> + let stats = + FSharp.Compiler.CacheInstrumentation.GetStatsUpdateForAllCaches(clearCounts = true) + + if stats <> "" then + logMsg $"\n{stats}") + + timer.Start() + +#if DEBUG + open OpenTelemetry.Resources + open OpenTelemetry.Trace + open OpenTelemetry.Metrics + + let otelExport () = + // On Windows forwarding localhost to wsl2 docker container sometimes does not work. Use IP address instead. + let otlpEndpoint = Uri("http://127.0.0.1:4317") + + let meterProvider = + // Configure OpenTelemetry metrics. Metrics can be viewed in Prometheus or other compatible tools. + OpenTelemetry.Sdk + .CreateMeterProviderBuilder() + .ConfigureResource(fun r -> r.AddService("F#") |> ignore) + .AddMeter(nameof FSharp.Compiler.CacheInstrumentation) + .AddOtlpExporter(fun e m -> + e.Endpoint <- otlpEndpoint + m.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds <- 1000 + m.TemporalityPreference <- MetricReaderTemporalityPreference.Cumulative) + .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 e -> e.Endpoint <- otlpEndpoint) + .Build() + + let a = Activity.startNoTags "FSharpPackage" + + fun () -> + a.Dispose() + tracerProvider.ForceFlush(5000) |> ignore + tracerProvider.Dispose() + meterProvider.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..d12d66bf660 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -340,15 +340,23 @@ 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 - // FSI-LINKAGE-POINT: unsited init do FSharp.Interactive.Hooks.fsiConsoleWindowPackageCtorUnsited (this :> Package) + // Uncomment to view cache metrics in the output window + // do Logging.FSharpServiceTelemetry.logCacheMetricsToOutput () + +#if DEBUG + + let flushTelemetry = Logging.FSharpServiceTelemetry.otelExport () + + 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) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index 738a3e1323c..25f46d3505f 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -17,6 +17,9 @@ + + XunitSetup.fs + diff --git a/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj b/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj index 7f24444a6e5..ba2efee9706 100644 --- a/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj +++ b/vsintegration/tests/Salsa/VisualFSharp.Salsa.fsproj @@ -16,6 +16,9 @@ + + XunitSetup.fs + diff --git a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj index 77c7d12e017..c7f69001d9e 100644 --- a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj +++ b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj @@ -17,6 +17,9 @@ + + XunitSetup.fs + From b2a130a1822699d72258d51f60784b36cab29d28 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:49:17 +0200 Subject: [PATCH 04/28] yeet LFU --- src/Compiler/Checking/import.fs | 1 - src/Compiler/Utilities/Caches.fs | 50 ++++++++----------------------- src/Compiler/Utilities/Caches.fsi | 10 ++----- 3 files changed, 15 insertions(+), 46 deletions(-) diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index 1b3ccbd64fb..649a412bca6 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -112,7 +112,6 @@ let getOrCreateTypeSubsumptionCache = // Oncremental use, so we need to set up the cache with eviction. { CacheOptions.Default with EvictionMethod = EvictionMethod.Background - Strategy = CachingStrategy.LRU PercentageToEvict = 5 MaximumCapacity = 4 * 32768 } cache <- Some (Cache.Create(options)) diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 97f2b08d8af..e93d9a168a7 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -8,11 +8,6 @@ open System.Threading open System.Diagnostics open System.Diagnostics.Metrics -[] -type CachingStrategy = - | LRU - | LFU - [] type EvictionMethod = | Blocking @@ -24,7 +19,6 @@ type CacheOptions = { MaximumCapacity: int PercentageToEvict: int - Strategy: CachingStrategy EvictionMethod: EvictionMethod LevelOfConcurrency: int } @@ -33,7 +27,6 @@ type CacheOptions = { MaximumCapacity = 1024 PercentageToEvict = 5 - Strategy = CachingStrategy.LRU LevelOfConcurrency = Environment.ProcessorCount EvictionMethod = EvictionMethod.Blocking } @@ -84,26 +77,25 @@ type EntityPool<'Key, 'Value>(maximumCapacity, overCapacity: Event<_>) = pool.Add(entity) type IEvictionQueue<'Key, 'Value> = - abstract member Add: CachedEntity<'Key, 'Value> * CachingStrategy -> unit + abstract member Add: CachedEntity<'Key, 'Value> -> unit abstract member Update: CachedEntity<'Key, 'Value> -> unit abstract member GetKeysToEvict: int -> 'Key[] abstract member Remove: CachedEntity<'Key, 'Value> -> unit [] -type EvictionQueue<'Key, 'Value>(strategy: CachingStrategy) = +type EvictionQueue<'Key, 'Value>() = let list = LinkedList>() interface IEvictionQueue<'Key, 'Value> with - member _.Add(entity: CachedEntity<'Key, 'Value>, strategy) = + member _.Add(entity: CachedEntity<'Key, 'Value>) = lock list <| fun () -> if isNull entity.Node.List then - match strategy with - | CachingStrategy.LRU -> list.AddLast(entity.Node) - | CachingStrategy.LFU -> list.AddLast(entity.Node) - // list.AddFirst(entity.Node) + list.AddLast(entity.Node) + else + assert false member _.Update(entity: CachedEntity<'Key, 'Value>) = lock list @@ -114,25 +106,9 @@ type EvictionQueue<'Key, 'Value>(strategy: CachingStrategy) = // Sync between store and the eviction queue is not atomic. It might be already evicted or not yet added. if node.List = list then - - 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. - let rec bubbleUp (current: LinkedListNode>) = - match current.Next with - | NonNull next when next.Value.AccessCount < entity.AccessCount -> bubbleUp next - | _ -> current - - let next = bubbleUp node - - if next <> node then - list.Remove(node) - list.AddAfter(next, node) + // Just move this node to the end of the list. + list.Remove(node) + list.AddLast(node) member _.GetKeysToEvict(count) = lock list @@ -145,7 +121,7 @@ type EvictionQueue<'Key, 'Value>(strategy: CachingStrategy) = static member NoEviction = { new IEvictionQueue<'Key, 'Value> with - member _.Add(_, _) = () + member _.Add(_) = () member _.Update(entity) = Interlocked.Increment(&entity.AccessCount) |> ignore @@ -188,7 +164,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option let evictionQueue: IEvictionQueue<'Key, 'Value> = match options.EvictionMethod with | EvictionMethod.NoEviction -> EvictionQueue.NoEviction - | _ -> EvictionQueue(options.Strategy) + | _ -> EvictionQueue() let tryEvictItems () = let count = @@ -249,7 +225,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option let cachedEntity = pool.Acquire(key, value) if store.TryAdd(key, cachedEntity) then - evictionQueue.Add(cachedEntity, options.Strategy) + evictionQueue.Add(cachedEntity) true else pool.Reclaim(cachedEntity) @@ -272,7 +248,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option current) ) - evictionQueue.Add(entity, options.Strategy) + evictionQueue.Add(entity) interface ICacheEvents with diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index c2dd99fe041..c2c8257a0ff 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -3,11 +3,6 @@ namespace FSharp.Compiler open System open System.Threading -[] -type internal CachingStrategy = - | LRU - | LFU - [] type internal EvictionMethod = | Blocking @@ -18,7 +13,6 @@ type internal EvictionMethod = type internal CacheOptions = { MaximumCapacity: int PercentageToEvict: int - Strategy: CachingStrategy EvictionMethod: EvictionMethod LevelOfConcurrency: int } @@ -32,14 +26,14 @@ type internal CachedEntity<'Key, 'Value> = override ToString: unit -> string type internal IEvictionQueue<'Key, 'Value> = - abstract member Add: CachedEntity<'Key, 'Value> * CachingStrategy -> unit + abstract member Add: CachedEntity<'Key, 'Value> -> unit abstract member Update: CachedEntity<'Key, 'Value> -> unit abstract member GetKeysToEvict: int -> 'Key[] abstract member Remove: CachedEntity<'Key, 'Value> -> unit [] type internal EvictionQueue<'Key, 'Value> = - new: strategy: CachingStrategy -> EvictionQueue<'Key, 'Value> + new: unit -> EvictionQueue<'Key, 'Value> member Count: int static member NoEviction: IEvictionQueue<'Key, 'Value> interface IEvictionQueue<'Key, 'Value> From 4c7e044ea3a7df01a087e641c876e82fe01436f9 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:49:59 +0200 Subject: [PATCH 05/28] flesh out the comparer --- src/Compiler/Utilities/TypeHashing.fs | 44 +++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Compiler/Utilities/TypeHashing.fs b/src/Compiler/Utilities/TypeHashing.fs index 7639f2dd679..1d847180e80 100644 --- a/src/Compiler/Utilities/TypeHashing.fs +++ b/src/Compiler/Utilities/TypeHashing.fs @@ -329,15 +329,47 @@ module HashTastMemberOrVals = hashNonMemberVal (g, obs) (tps, vref.Deref, tau, cxs) | Some _ -> hashMember (g, obs) emptyTyparInst vref.Deref +/// Practical TType comparer strictly for the use with cache keys. module HashStamps = - let rec stampEquals ty1 ty2 = + let rec typeInstStampsEqual (tys1: TypeInst) (tys2: TypeInst) = + tys1.Length = tys2.Length + && (tys1, tys2) ||> Seq.zip |> Seq.forall (fun (t1, t2) -> stampEquals t1 t2) + + and inline typarStampEquals (t1: Typar) (t2: Typar) = t1.Stamp = t2.Stamp + + and typarsStampsEqual (tps1: Typars) (tps2: Typars) = + tps1.Length = tps2.Length && (tps1, tps2) ||> Seq.forall2 typarStampEquals + + and measureStampEquals (m1: Measure) (m2: Measure) = + match m1, m2 with + | Measure.Var(mv1), Measure.Var(mv2) -> mv1.Stamp = mv2.Stamp + | Measure.Const(t1, _), Measure.Const(t2, _) -> t1.Stamp = t2.Stamp + | Measure.Prod(m1, m2, _), Measure.Prod(m3, m4, _) -> measureStampEquals m1 m3 && measureStampEquals m2 m4 + | Measure.Inv m1, Measure.Inv m2 -> measureStampEquals m1 m2 + | Measure.One _, Measure.One _ -> true + | Measure.RationalPower(m1, r1), Measure.RationalPower(m2, r2) -> r1 = r2 && measureStampEquals m1 m2 + | _ -> false + + and nullnessEquals (n1: Nullness) (n2: Nullness) = + match n1, n2 with + | Nullness.Known n1, Nullness.Known n2 -> n1 = n2 + | Nullness.Variable _, Nullness.Variable _ -> true + | _ -> false + + and stampEquals ty1 ty2 = match ty1, ty2 with - | TType_app(tcref1, tinst1, _), TType_app(tcref2, tinst2, _) -> + | TType_ucase(u, tys1), TType_ucase(v, tys2) -> u.CaseName = v.CaseName && typeInstStampsEqual tys1 tys2 + | TType_app(tcref1, tinst1, Nullness.Known n1), TType_app(tcref2, tinst2, Nullness.Known n2) -> + n1 = n2 && tcref1.Stamp = tcref2.Stamp && typeInstStampsEqual tinst1 tinst2 + | TType_app(tcref1, tinst1, n1), TType_app(tcref2, tinst2, n2) -> tcref1.Stamp = tcref2.Stamp - && tinst1.Length = tinst2.Length - && (tinst1, tinst2) ||> Seq.zip |> Seq.forall (fun (t1, t2) -> stampEquals t1 t2) - - | TType_var(r1, _), TType_var(r2, _) -> r1.Stamp = r2.Stamp + && nullnessEquals n1 n2 + && typeInstStampsEqual tinst1 tinst2 + | TType_anon(info1, tys1), TType_anon(info2, tys2) -> info1.Stamp = info2.Stamp && typeInstStampsEqual tys1 tys2 + | TType_tuple(c1, tys1), TType_tuple(c2, tys2) -> c1 = c2 && typeInstStampsEqual tys1 tys2 + | TType_forall(tps1, tau1), TType_forall(tps2, tau2) -> typarsStampsEqual tps1 tps2 && stampEquals tau1 tau2 + | TType_var(r1, n1), TType_var(r2, n2) -> r1.Stamp = r2.Stamp && nullnessEquals n1 n2 + | TType_measure m1, TType_measure m2 -> measureStampEquals m1 m2 | _ -> false let inline hashStamp (x: int64) = uint x * 2654435761u |> int From a269b6afb21575d8337463f03c376ce29d2d8047 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 24 Apr 2025 17:10:41 +0200 Subject: [PATCH 06/28] fix sources --- src/Compiler/FSharp.Compiler.Service.fsproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index 9754dd25a11..bbddf424684 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -146,8 +146,8 @@ - - + + From 874d9c1a84c346aaed191741f88ac36987617258 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Thu, 24 Apr 2025 23:53:20 +0200 Subject: [PATCH 07/28] try to deal with CI memory overload --- src/Compiler/Checking/import.fs | 4 +- src/Compiler/Utilities/Caches.fs | 48 +++++++++++-------- src/Compiler/Utilities/Caches.fsi | 7 +-- tests/FSharp.Test.Utilities/XunitHelpers.fs | 10 +++- .../src/FSharp.Editor/Common/Logging.fs | 1 + 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index 649a412bca6..d5e9eeb6852 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -106,10 +106,10 @@ let getOrCreateTypeSubsumptionCache = | CompilationMode.OneOff -> // This is a one-off compilation, so we don't need to worry about eviction. { CacheOptions.Default with - MaximumCapacity = 200_000 + MaximumCapacity = 4 * 1024 EvictionMethod = EvictionMethod.NoEviction } | _ -> - // Oncremental use, so we need to set up the cache with eviction. + // Incremental use, so we need to set up the cache with eviction. { CacheOptions.Default with EvictionMethod = EvictionMethod.Background PercentageToEvict = 5 diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index e93d9a168a7..5e7023cd242 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -10,7 +10,6 @@ open System.Diagnostics.Metrics [] type EvictionMethod = - | Blocking | Background | NoEviction @@ -28,7 +27,7 @@ type CacheOptions = MaximumCapacity = 1024 PercentageToEvict = 5 LevelOfConcurrency = Environment.ProcessorCount - EvictionMethod = EvictionMethod.Blocking + EvictionMethod = EvictionMethod.Background } [] @@ -219,9 +218,6 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option false member _.TryAdd(key: 'Key, value: 'Value) = - if options.EvictionMethod.IsBlocking then - tryEvictItems () - let cachedEntity = pool.Acquire(key, value) if store.TryAdd(key, cachedEntity) then @@ -232,9 +228,6 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option false member _.AddOrUpdate(key: 'Key, value: 'Value) = - if options.EvictionMethod.IsBlocking then - tryEvictItems () - let aquired = pool.Acquire(key, value) let entity = @@ -277,17 +270,6 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option override this.Finalize() : unit = this.Dispose() - static member Create<'Key, 'Value>(options: CacheOptions) = - // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary. - let capacity = - options.MaximumCapacity - + int (float options.MaximumCapacity * float options.PercentageToEvict / 100.0) - - let cts = new CancellationTokenSource() - let cache = new Cache<'Key, 'Value>(options, capacity, cts) - CacheInstrumentation.AddInstrumentation cache |> ignore - cache - member this.GetStats() = CacheInstrumentation.GetStats(this) and CacheInstrumentation(cache: ICacheEvents) = @@ -382,3 +364,31 @@ and CacheInstrumentation(cache: ICacheEvents) = static member RemoveInstrumentation(cache: ICacheEvents) = instrumentedCaches[cache].Dispose() instrumentedCaches.TryRemove(cache) |> ignore + +module Cache = + + // During testing a lot of compilations are started in app domains and subprocesses. + // This is a reliable way to pass the override to all of them. + [] + let private overrideVariable = "FSHARP_CACHE_OVERRIDE" + + /// Use for testing purposes to reduce memory consumption in testhost and its subprocesses. + let OverrideMaxCapacityForTesting () = + Environment.SetEnvironmentVariable(overrideVariable, "true", EnvironmentVariableTarget.Process) + + let Create<'Key, 'Value when 'Key: not null and 'Key: equality> (options: CacheOptions) = + + let options = + match Environment.GetEnvironmentVariable(overrideVariable) with + | null -> options + | _ -> { options with MaximumCapacity = 8 * 1024 } + + // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary. + let capacity = + options.MaximumCapacity + + int (float options.MaximumCapacity * float options.PercentageToEvict / 100.0) + + let cts = new CancellationTokenSource() + let cache = new Cache<'Key, 'Value>(options, capacity, cts) + CacheInstrumentation.AddInstrumentation cache |> ignore + cache diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index c2c8257a0ff..1b3db0174f4 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -5,7 +5,6 @@ open System.Threading [] type internal EvictionMethod = - | Blocking | Background | NoEviction @@ -63,8 +62,6 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = member Dispose: unit -> unit member GetStats: unit -> string - static member Create<'Key, 'Value> : options: CacheOptions -> Cache<'Key, 'Value> - interface ICacheEvents interface IDisposable @@ -77,3 +74,7 @@ type internal CacheInstrumentation = static member GetStatsUpdateForAllCaches: clearCounts: bool -> string static member AddInstrumentation: cache: ICacheEvents -> unit static member RemoveInstrumentation: cache: ICacheEvents -> unit + +module internal Cache = + val OverrideMaxCapacityForTesting: unit -> unit + val Create<'Key, 'Value when 'Key: not null and 'Key: equality> : options: CacheOptions -> Cache<'Key, 'Value> diff --git a/tests/FSharp.Test.Utilities/XunitHelpers.fs b/tests/FSharp.Test.Utilities/XunitHelpers.fs index c68b2f29a14..902335b6568 100644 --- a/tests/FSharp.Test.Utilities/XunitHelpers.fs +++ b/tests/FSharp.Test.Utilities/XunitHelpers.fs @@ -145,6 +145,11 @@ type FSharpXunitFramework(sink: IMessageSink) = // We need AssemblyResolver already here, because OpenTelemetry loads some assemblies dynamically. AssemblyResolver.addResolver () #endif + + // Override cache capacity to reduce memory usage in CI. + FSharp.Compiler.Cache.OverrideMaxCapacityForTesting() + + let testRunName = $"RunTests_{assemblyName.Name} {Runtime.InteropServices.RuntimeInformation.FrameworkDescription}" // On Windows forwarding localhost to wsl2 docker container sometimes does not work. Use IP address instead. let otlpEndpoint = Uri("http://127.0.0.1:4317") @@ -167,7 +172,8 @@ type FSharpXunitFramework(sink: IMessageSink) = use meterProvider = OpenTelemetry.Sdk.CreateMeterProviderBuilder() .AddMeter(nameof FSharp.Compiler.CacheInstrumentation) - .ConfigureResource(fun r -> r.AddService("F#") |> ignore) + .AddMeter("System.Runtime") + .ConfigureResource(fun r -> r.AddService(testRunName) |> ignore) .AddOtlpExporter(fun e m -> e.Endpoint <- otlpEndpoint e.Protocol <- OpenTelemetry.Exporter.OtlpExportProtocol.Grpc @@ -180,7 +186,7 @@ type FSharpXunitFramework(sink: IMessageSink) = TestConsole.install() begin - use _ = Activity.startNoTags $"RunTests_{assemblyName.Name} {Runtime.InteropServices.RuntimeInformation.FrameworkDescription}" + use _ = Activity.startNoTags testRunName // We can't just call base.RunTestCases here, because it's implementation is async void. use runner = new XunitTestAssemblyRunner (x.TestAssembly, testCases, x.DiagnosticMessageSink, executionMessageSink, executionOptions) runner.RunAsync().Wait() diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs index e8d0e4d5daf..524abb31830 100644 --- a/vsintegration/src/FSharp.Editor/Common/Logging.fs +++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs @@ -177,6 +177,7 @@ module FSharpServiceTelemetry = .CreateMeterProviderBuilder() .ConfigureResource(fun r -> r.AddService("F#") |> ignore) .AddMeter(nameof FSharp.Compiler.CacheInstrumentation) + .AddMeter("System.Runtime") .AddOtlpExporter(fun e m -> e.Endpoint <- otlpEndpoint m.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds <- 1000 From 4bca2df7180309ed68d653dace10004d54b09b4a Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Fri, 25 Apr 2025 02:17:47 +0200 Subject: [PATCH 08/28] replace singleton with CWT again --- src/Compiler/Checking/import.fs | 43 +++++++++++---------------- src/Compiler/Utilities/TypeHashing.fs | 3 +- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index d5e9eeb6852..2138e070001 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -92,30 +92,23 @@ type TTypeCacheKey = override this.ToString () = $"{this.ty1.DebugText}-{this.ty2.DebugText}" -let getOrCreateTypeSubsumptionCache = - let mutable lockObj = obj() - let mutable cache = None - - fun compilationMode -> - lock lockObj <| fun () -> - match cache with - | Some c -> c - | _ -> - let options = - match compilationMode with - | CompilationMode.OneOff -> - // This is a one-off compilation, so we don't need to worry about eviction. - { CacheOptions.Default with - MaximumCapacity = 4 * 1024 - EvictionMethod = EvictionMethod.NoEviction } - | _ -> - // Incremental use, so we need to set up the cache with eviction. - { CacheOptions.Default with - EvictionMethod = EvictionMethod.Background - PercentageToEvict = 5 - MaximumCapacity = 4 * 32768 } - cache <- Some (Cache.Create(options)) - cache.Value +let createTypeSubsumptionCache (g: TcGlobals) = + let options = + match g.compilationMode with + | CompilationMode.OneOff -> + // This is a one-off compilation, so we don't need to worry about eviction. + { CacheOptions.Default with + MaximumCapacity = 4 * 1024 + EvictionMethod = EvictionMethod.NoEviction } + | _ -> + // Incremental use, so we need to set up the cache with eviction. + { CacheOptions.Default with + EvictionMethod = EvictionMethod.Background + PercentageToEvict = 5 + MaximumCapacity = 4 * 32768 } + Cache.Create(options) + +let typeSubsumptionCaches = ConditionalWeakTable>() //------------------------------------------------------------------------- // Import an IL types as F# types. @@ -139,7 +132,7 @@ type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) = member _.ILTypeRefToTyconRefCache = typeRefToTyconRefCache - member val TypeSubsumptionCache: Cache = getOrCreateTypeSubsumptionCache g.compilationMode + member val TypeSubsumptionCache: Cache = typeSubsumptionCaches.GetValue(g, createTypeSubsumptionCache) let CanImportILScopeRef (env: ImportMap) m scoref = diff --git a/src/Compiler/Utilities/TypeHashing.fs b/src/Compiler/Utilities/TypeHashing.fs index 1d847180e80..c066a8ece18 100644 --- a/src/Compiler/Utilities/TypeHashing.fs +++ b/src/Compiler/Utilities/TypeHashing.fs @@ -332,8 +332,7 @@ module HashTastMemberOrVals = /// Practical TType comparer strictly for the use with cache keys. module HashStamps = let rec typeInstStampsEqual (tys1: TypeInst) (tys2: TypeInst) = - tys1.Length = tys2.Length - && (tys1, tys2) ||> Seq.zip |> Seq.forall (fun (t1, t2) -> stampEquals t1 t2) + tys1.Length = tys2.Length && (tys1, tys2) ||> Seq.forall2 stampEquals and inline typarStampEquals (t1: Typar) (t2: Typar) = t1.Stamp = t2.Stamp From 9ba405d3c64f5228614afb9eade533f3a728acaa Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Fri, 25 Apr 2025 02:18:20 +0200 Subject: [PATCH 09/28] remove sketchy logic --- src/Compiler/Checking/TypeRelations.fs | 2 +- src/Compiler/Utilities/Caches.fs | 23 +++++------------------ src/Compiler/Utilities/Caches.fsi | 1 - 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/Compiler/Checking/TypeRelations.fs b/src/Compiler/Checking/TypeRelations.fs index fa900069508..fddf6027875 100644 --- a/src/Compiler/Checking/TypeRelations.fs +++ b/src/Compiler/Checking/TypeRelations.fs @@ -113,7 +113,7 @@ 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.AddOrUpdate(key, subsumes) + 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/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 5e7023cd242..b2ce43985e5 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -114,7 +114,10 @@ type EvictionQueue<'Key, 'Value>() = <| fun () -> list |> Seq.map _.Key |> Seq.truncate count |> Seq.toArray member this.Remove(entity: CachedEntity<_, _>) = - lock list <| fun () -> list.Remove(entity.Node) + lock list + <| fun () -> + if entity.Node.List = list then + list.Remove(entity.Node) member _.Count = list.Count @@ -227,22 +230,6 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option pool.Reclaim(cachedEntity) false - member _.AddOrUpdate(key: 'Key, value: 'Value) = - let aquired = pool.Acquire(key, value) - - let entity = - store.AddOrUpdate( - key, - (fun _ -> aquired), - (fun _ (current: CachedEntity<_, _>) -> - pool.Reclaim aquired - current.Value <- value - evictionQueue.Remove(current) - current) - ) - - evictionQueue.Add(entity) - interface ICacheEvents with [] @@ -381,7 +368,7 @@ module Cache = let options = match Environment.GetEnvironmentVariable(overrideVariable) with | null -> options - | _ -> { options with MaximumCapacity = 8 * 1024 } + | _ -> { options with MaximumCapacity = 1024 } // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary. let capacity = diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index 1b3db0174f4..22785871114 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -58,7 +58,6 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = new: options: CacheOptions * capacity: int * cts: CancellationTokenSource -> Cache<'Key, 'Value> member TryGetValue: key: 'Key * value: outref<'Value> -> bool member TryAdd: key: 'Key * value: 'Value -> bool - member AddOrUpdate: key: 'Key * value: 'Value -> unit member Dispose: unit -> unit member GetStats: unit -> string From 5c6a5005ad4049a8668374e60edee736275c542a Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Fri, 25 Apr 2025 14:08:07 +0200 Subject: [PATCH 10/28] cache of caches deal with memory restrictions in CI, dispose caches --- src/Compiler/Checking/import.fs | 7 +++++-- src/Compiler/Utilities/Caches.fs | 28 ++++++++++++++++++++-------- src/Compiler/Utilities/Caches.fsi | 6 +++++- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index 2138e070001..de21f7ae7f3 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -108,7 +108,9 @@ let createTypeSubsumptionCache (g: TcGlobals) = MaximumCapacity = 4 * 32768 } Cache.Create(options) -let typeSubsumptionCaches = ConditionalWeakTable>() +let typeSubsumptionCaches = Cache.Create>({ CacheOptions.Default with MaximumCapacity = 16 }) + +do typeSubsumptionCaches.ValueEvicted.Add <| _.Dispose() //------------------------------------------------------------------------- // Import an IL types as F# types. @@ -132,7 +134,8 @@ type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) = member _.ILTypeRefToTyconRefCache = typeRefToTyconRefCache - member val TypeSubsumptionCache: Cache = typeSubsumptionCaches.GetValue(g, createTypeSubsumptionCache) + member val TypeSubsumptionCache: Cache = + typeSubsumptionCaches.GetOrCreate(g, createTypeSubsumptionCache) let CanImportILScopeRef (env: ImportMap) m scoref = diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index b2ce43985e5..b3c5495b38f 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -5,9 +5,12 @@ open System open System.Collections.Generic open System.Collections.Concurrent open System.Threading +open System.Threading.Tasks open System.Diagnostics open System.Diagnostics.Metrics +open FSharp.Compiler.Diagnostics + [] type EvictionMethod = | Background @@ -154,7 +157,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option let cacheHit = Event() let cacheMiss = Event() - let eviction = Event() + let eviction = Event<'Value>() let evictionFail = Event() let overCapacity = Event() @@ -181,7 +184,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option | true, removed -> evictionQueue.Remove(removed) pool.Reclaim(removed) - eviction.Trigger() + eviction.Trigger(removed.Value) | _ -> failwith "eviction fail" evictionFail.Trigger() @@ -230,6 +233,17 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option pool.Reclaim(cachedEntity) false + member this.GetOrCreate(key: 'Key, valueFactory: 'Key -> 'Value) = + match this.TryGetValue(key) with + | true, value -> value + | _ -> + let value = valueFactory key + this.TryAdd(key, value) |> ignore + value + + [] + member val ValueEvicted = eviction.Publish + interface ICacheEvents with [] @@ -239,7 +253,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option member val CacheMiss = cacheMiss.Publish [] - member val Eviction = eviction.Publish + member val Eviction = eviction.Publish |> Event.map ignore [] member val EvictionFail = evictionFail.Publish @@ -249,14 +263,12 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option interface IDisposable with member this.Dispose() = + store.Clear() cts.Cancel() CacheInstrumentation.RemoveInstrumentation(this) - GC.SuppressFinalize(this) member this.Dispose() = (this :> IDisposable).Dispose() - override this.Finalize() : unit = this.Dispose() - member this.GetStats() = CacheInstrumentation.GetStats(this) and CacheInstrumentation(cache: ICacheEvents) = @@ -367,8 +379,8 @@ module Cache = let options = match Environment.GetEnvironmentVariable(overrideVariable) with - | null -> options - | _ -> { options with MaximumCapacity = 1024 } + | NonNull _ when options.MaximumCapacity > 1024 -> { options with MaximumCapacity = 1024 } + | _ -> options // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary. let capacity = diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index 22785871114..c382c141131 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -58,8 +58,12 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = new: options: CacheOptions * capacity: int * cts: CancellationTokenSource -> Cache<'Key, 'Value> member TryGetValue: key: 'Key * value: outref<'Value> -> bool member TryAdd: key: 'Key * value: 'Value -> bool - member Dispose: unit -> unit + member GetOrCreate: key: 'Key * valueFactory: ('Key -> 'Value) -> 'Value member GetStats: unit -> string + member Dispose: unit -> unit + + [] + member ValueEvicted: IEvent<'Value> interface ICacheEvents interface IDisposable From 3046f881d99fe422e2e4e1a6f75e8f9306772922 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:03:51 +0200 Subject: [PATCH 11/28] ilver --- src/Compiler/Utilities/Caches.fs | 16 +++++++++++----- ...rify_FSharp.Compiler.Service_Debug_net9.0.bsl | 12 ++++++------ ...arp.Compiler.Service_Debug_netstandard2.0.bsl | 12 ++++++------ ...fy_FSharp.Compiler.Service_Release_net9.0.bsl | 12 ++++++------ ...p.Compiler.Service_Release_netstandard2.0.bsl | 12 ++++++------ 5 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index b3c5495b38f..0bb849310b7 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -375,13 +375,19 @@ module Cache = let OverrideMaxCapacityForTesting () = Environment.SetEnvironmentVariable(overrideVariable, "true", EnvironmentVariableTarget.Process) - let Create<'Key, 'Value when 'Key: not null and 'Key: equality> (options: CacheOptions) = - - let options = + let applyOverride (options: CacheOptions) = + let capacity = match Environment.GetEnvironmentVariable(overrideVariable) with - | NonNull _ when options.MaximumCapacity > 1024 -> { options with MaximumCapacity = 1024 } - | _ -> options + | NonNull _ when options.MaximumCapacity < 100 -> 3 + | NonNull _ -> 512 + | _ -> options.MaximumCapacity + { options with + MaximumCapacity = capacity + } + + let Create<'Key, 'Value when 'Key: not null and 'Key: equality> (options: CacheOptions) = + let options = applyOverride options // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary. let capacity = options.MaximumCapacity 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 bd581b3d765..72270ed5f02 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl @@ -21,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-812::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-817::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-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]: : .$ServiceLexing+clo@921-521::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-521::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-521::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-521::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-521::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 752b1e98415..841c59db0d6 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl @@ -28,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-812::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-817::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-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]: : .$ServiceLexing+clo@921-521::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-521::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-521::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-521::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-521::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 d171cb2277a..e120d20c4c0 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-856::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-861::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-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]: : .$ServiceLexing+clo@921-542::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-542::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-542::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-542::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-542::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 3b30273904b..cf89979c03f 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl @@ -28,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-856::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-861::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-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]: : .$ServiceLexing+clo@921-542::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-542::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-542::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-542::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-542::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000076][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$Symbols+fullName@2495-3::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000030][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. From 80dc554b5fe99b46df29eeb48b746f0485059edb Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:15:39 +0200 Subject: [PATCH 12/28] events -> counters --- src/Compiler/Utilities/Caches.fs | 246 ++++++++---------- src/Compiler/Utilities/Caches.fsi | 29 +-- tests/FSharp.Test.Utilities/XunitHelpers.fs | 2 +- .../src/FSharp.Editor/Common/Logging.fs | 2 +- .../LanguageService/LanguageService.fs | 2 +- 5 files changed, 114 insertions(+), 167 deletions(-) diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 0bb849310b7..c449335d01d 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -61,16 +61,95 @@ type CachedEntity<'Key, 'Value> = override this.ToString() = $"{this.Key}" -type EntityPool<'Key, 'Value>(maximumCapacity, overCapacity: Event<_>) = +module CacheMetrics = + let meter = new Meter("FSharp.Compiler.Cache") + +type CacheMetrics(cacheId) = + + static let instrumentedCaches = ConcurrentDictionary() + + let readings = ConcurrentDictionary() + + let listener = + new MeterListener( + InstrumentPublished = + fun i l -> + if i.Meter = CacheMetrics.meter && i.Description = cacheId then + l.EnableMeasurementEvents(i) + ) + + do + listener.SetMeasurementEventCallback(fun k v _ _ -> Interlocked.Add(readings.GetOrAdd(k.Name, ref 0L), v) |> ignore) + listener.Start() + + member val CacheId = cacheId + + member val RecentStats = "-" with get, set + + member this.TryUpdateStats(clearCounts) = + let stats = + try + let ratio = + float readings["hits"].Value + / float (readings["hits"].Value + readings["misses"].Value) + * 100.0 + + [ + for name in readings.Keys do + let v = readings[name].Value + + if v > 0 then + $"{name}: {v}" + ] + |> String.concat ", " + |> sprintf "%s | hit ratio: %s %s" this.CacheId (if Double.IsNaN(ratio) then "-" else $"%.1f{ratio}%%") + with _ -> + "!" + + if clearCounts then + for r in readings.Values do + Interlocked.Exchange(r, 0L) |> ignore + + if stats <> this.RecentStats then + this.RecentStats <- stats + true + else + false + + member this.Dispose() = listener.Dispose() + + static member GetStats(cacheId) = + instrumentedCaches[cacheId].TryUpdateStats(false) |> ignore + instrumentedCaches[cacheId].RecentStats + + static member GetStatsUpdateForAllCaches(clearCounts) = + [ + for i in instrumentedCaches.Values do + if i.TryUpdateStats(clearCounts) then + i.RecentStats + ] + |> String.concat "\n" + + static member AddInstrumentation(cacheId) = + instrumentedCaches[cacheId] <- new CacheMetrics(cacheId) + + static member RemoveInstrumentation(cacheId) = + instrumentedCaches[cacheId].Dispose() + instrumentedCaches.TryRemove(cacheId) |> ignore + +type EntityPool<'Key, 'Value>(maximumCapacity, cacheId) = let pool = ConcurrentBag>() let mutable created = 0 + let overCapacity = + CacheMetrics.meter.CreateCounter("over-capacity", "count", cacheId) + member _.Acquire(key, value) = match pool.TryTake() with | true, entity -> entity.ReUse(key, value) | _ -> if Interlocked.Increment &created > maximumCapacity then - overCapacity.Trigger() + overCapacity.Add 1L CachedEntity(key, value).WithNode() @@ -135,33 +214,27 @@ type EvictionQueue<'Key, 'Value>() = member _.Remove(_) = () } -type ICacheEvents = - [] - abstract member CacheHit: IEvent +[] +[] +type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> + internal (options: CacheOptions, capacity, cts: CancellationTokenSource, ?name) = - [] - abstract member CacheMiss: IEvent + static let mutable cacheId = 0 - [] - abstract member Eviction: IEvent + let instanceId = defaultArg name $"cache-{Interlocked.Increment(&cacheId)}" - [] - abstract member EvictionFail: IEvent + let hits = CacheMetrics.meter.CreateCounter("hits", "count", instanceId) + let misses = CacheMetrics.meter.CreateCounter("misses", "count", instanceId) - [] - abstract member OverCapacity: IEvent + let evictions = + CacheMetrics.meter.CreateCounter("evictions", "count", instanceId) -[] -[] -type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (options: CacheOptions, capacity, cts: CancellationTokenSource) = + let evictionFails = + CacheMetrics.meter.CreateCounter("eviction-fails", "count", instanceId) - let cacheHit = Event() - let cacheMiss = Event() let eviction = Event<'Value>() - let evictionFail = Event() - let overCapacity = Event() - let pool = EntityPool<'Key, 'Value>(capacity, overCapacity) + let pool = EntityPool<'Key, 'Value>(capacity, instanceId) let store = ConcurrentDictionary<'Key, CachedEntity<'Key, 'Value>>(options.LevelOfConcurrency, capacity) @@ -185,9 +258,8 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option evictionQueue.Remove(removed) pool.Reclaim(removed) eviction.Trigger(removed.Value) - | _ -> - failwith "eviction fail" - evictionFail.Trigger() + evictions.Add 1L + | _ -> evictionFails.Add 1L let rec backgroundEviction () = async { @@ -211,15 +283,17 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option if options.EvictionMethod = EvictionMethod.Background then Async.Start(backgroundEviction (), cancellationToken = cts.Token) + member val Name = instanceId + member _.TryGetValue(key: 'Key, value: outref<'Value>) = match store.TryGetValue(key) with | true, cachedEntity -> - cacheHit.Trigger() + hits.Add 1L evictionQueue.Update(cachedEntity) value <- cachedEntity.Value true | _ -> - cacheMiss.Trigger() + misses.Add 1L value <- Unchecked.defaultof<'Value> false @@ -244,125 +318,15 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (option [] member val ValueEvicted = eviction.Publish - interface ICacheEvents with - - [] - member val CacheHit = cacheHit.Publish - - [] - member val CacheMiss = cacheMiss.Publish - - [] - member val Eviction = eviction.Publish |> Event.map ignore - - [] - member val EvictionFail = evictionFail.Publish - - [] - member val OverCapacity = overCapacity.Publish - interface IDisposable with member this.Dispose() = store.Clear() cts.Cancel() - CacheInstrumentation.RemoveInstrumentation(this) + CacheMetrics.RemoveInstrumentation(this.Name) member this.Dispose() = (this :> IDisposable).Dispose() - member this.GetStats() = CacheInstrumentation.GetStats(this) - -and CacheInstrumentation(cache: ICacheEvents) = - static let mutable cacheId = 0 - - static let instrumentedCaches = ConcurrentDictionary() - - static let meter = new Meter(nameof CacheInstrumentation) - - let instanceId = $"cache-{Interlocked.Increment(&cacheId)}" - - let hits = meter.CreateCounter("hits", "count", instanceId) - let misses = meter.CreateCounter("misses", "count", instanceId) - let evictions = meter.CreateCounter("evictions", "count", instanceId) - - let evictionFails = - meter.CreateCounter("eviction-fails", "count", instanceId) - - let overCapacity = meter.CreateCounter("over-capacity", "count", instanceId) - - do - cache.CacheHit.Add <| fun _ -> hits.Add(1L) - cache.CacheMiss.Add <| fun _ -> misses.Add(1L) - cache.Eviction.Add <| fun _ -> evictions.Add(1L) - cache.EvictionFail.Add <| fun _ -> evictionFails.Add(1L) - cache.OverCapacity.Add <| fun _ -> overCapacity.Add(1L) - - let current = ConcurrentDictionary() - - let listener = - new MeterListener( - InstrumentPublished = - fun i l -> - if i.Meter = meter && i.Description = instanceId then - l.EnableMeasurementEvents(i) - ) - - do - listener.SetMeasurementEventCallback(fun k v _ _ -> Interlocked.Add(current.GetOrAdd(k, ref 0L), v) |> ignore) - listener.Start() - - member val CacheId = instanceId - - member val RecentStats = "-" with get, set - - member this.TryUpdateStats(clearCounts) = - let stats = - try - let ratio = - float current[hits].Value / float (current[hits].Value + current[misses].Value) - * 100.0 - - [ - for i in current.Keys do - let v = current[i].Value - - if v > 0 then - $"{i.Name}: {v}" - ] - |> String.concat ", " - |> sprintf "%s | hit ratio: %s %s" this.CacheId (if Double.IsNaN(ratio) then "-" else $"%.1f{ratio}%%") - with _ -> - "!" - - if clearCounts then - for r in current.Values do - Interlocked.Exchange(r, 0L) |> ignore - - if stats <> this.RecentStats then - this.RecentStats <- stats - true - else - false - - member this.Dispose() = listener.Dispose() - - static member GetStats(cache: ICacheEvents) = - instrumentedCaches[cache].TryUpdateStats(false) |> ignore - instrumentedCaches[cache].RecentStats - - static member GetStatsUpdateForAllCaches(clearCounts) = - [ - for i in instrumentedCaches.Values do - if i.TryUpdateStats(clearCounts) then - i.RecentStats - ] - |> String.concat "\n" - - static member AddInstrumentation(cache: ICacheEvents) = - instrumentedCaches[cache] <- new CacheInstrumentation(cache) - - static member RemoveInstrumentation(cache: ICacheEvents) = - instrumentedCaches[cache].Dispose() - instrumentedCaches.TryRemove(cache) |> ignore + member this.GetStats() = CacheMetrics.GetStats(this.Name) module Cache = @@ -378,8 +342,8 @@ module Cache = let applyOverride (options: CacheOptions) = let capacity = match Environment.GetEnvironmentVariable(overrideVariable) with - | NonNull _ when options.MaximumCapacity < 100 -> 3 - | NonNull _ -> 512 + | NonNull _ when options.MaximumCapacity < 100 -> 5 + | NonNull _ -> 8912 | _ -> options.MaximumCapacity { options with @@ -395,5 +359,5 @@ module Cache = let cts = new CancellationTokenSource() let cache = new Cache<'Key, 'Value>(options, capacity, cts) - CacheInstrumentation.AddInstrumentation cache |> ignore + CacheMetrics.AddInstrumentation cache.Name |> ignore cache diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index c382c141131..0cd58d85742 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -37,25 +37,9 @@ type internal EvictionQueue<'Key, 'Value> = static member NoEviction: IEvictionQueue<'Key, 'Value> interface IEvictionQueue<'Key, 'Value> -type internal ICacheEvents = - [] - abstract member CacheHit: IEvent - - [] - abstract member CacheMiss: IEvent - - [] - abstract member Eviction: IEvent - - [] - abstract member EvictionFail: IEvent - - [] - abstract member OverCapacity: IEvent - [] type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = - new: options: CacheOptions * capacity: int * cts: CancellationTokenSource -> Cache<'Key, 'Value> + new: options: CacheOptions * capacity: int * cts: CancellationTokenSource * ?name: string -> Cache<'Key, 'Value> member TryGetValue: key: 'Key * value: outref<'Value> -> bool member TryAdd: key: 'Key * value: 'Value -> bool member GetOrCreate: key: 'Key * valueFactory: ('Key -> 'Value) -> 'Value @@ -65,18 +49,17 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = [] member ValueEvicted: IEvent<'Value> - interface ICacheEvents interface IDisposable -type internal CacheInstrumentation = - new: cache: ICacheEvents -> CacheInstrumentation +type internal CacheMetrics = + new: cacheId: string -> CacheMetrics member CacheId: string member RecentStats: string member TryUpdateStats: clearCounts: bool -> bool - static member GetStats: cache: ICacheEvents -> string + static member GetStats: cacheId: string -> string static member GetStatsUpdateForAllCaches: clearCounts: bool -> string - static member AddInstrumentation: cache: ICacheEvents -> unit - static member RemoveInstrumentation: cache: ICacheEvents -> unit + static member AddInstrumentation: cacheId: string -> unit + static member RemoveInstrumentation: cacheId: string -> unit module internal Cache = val OverrideMaxCapacityForTesting: unit -> unit diff --git a/tests/FSharp.Test.Utilities/XunitHelpers.fs b/tests/FSharp.Test.Utilities/XunitHelpers.fs index 902335b6568..a05809c2caa 100644 --- a/tests/FSharp.Test.Utilities/XunitHelpers.fs +++ b/tests/FSharp.Test.Utilities/XunitHelpers.fs @@ -171,7 +171,7 @@ type FSharpXunitFramework(sink: IMessageSink) = use meterProvider = OpenTelemetry.Sdk.CreateMeterProviderBuilder() - .AddMeter(nameof FSharp.Compiler.CacheInstrumentation) + .AddMeter(nameof FSharp.Compiler.CacheMetrics) .AddMeter("System.Runtime") .ConfigureResource(fun r -> r.AddService(testRunName) |> ignore) .AddOtlpExporter(fun e m -> diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs index 524abb31830..08f8ca468b4 100644 --- a/vsintegration/src/FSharp.Editor/Common/Logging.fs +++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs @@ -155,7 +155,7 @@ module FSharpServiceTelemetry = timer.Elapsed.Add(fun _ -> let stats = - FSharp.Compiler.CacheInstrumentation.GetStatsUpdateForAllCaches(clearCounts = true) + FSharp.Compiler.CacheMetrics.GetStatsUpdateForAllCaches(clearCounts = true) if stats <> "" then logMsg $"\n{stats}") diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index d12d66bf660..4c0c384b6b6 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -344,7 +344,7 @@ type internal FSharpPackage() as this = do FSharp.Interactive.Hooks.fsiConsoleWindowPackageCtorUnsited (this :> Package) // Uncomment to view cache metrics in the output window - // do Logging.FSharpServiceTelemetry.logCacheMetricsToOutput () + do Logging.FSharpServiceTelemetry.logCacheMetricsToOutput () #if DEBUG From 6926f9703d064f8d30d01386fe10740568f17ca4 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Fri, 25 Apr 2025 23:30:41 +0200 Subject: [PATCH 13/28] precompute hash --- src/Compiler/Checking/import.fs | 16 ++++++------ src/Compiler/Checking/import.fsi | 4 +-- src/Compiler/Utilities/TypeHashing.fs | 25 +++++++++++-------- .../LanguageService/LanguageService.fs | 2 +- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index de21f7ae7f3..a59f0e38e85 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -63,12 +63,17 @@ type TTypeCacheKey = val ty1: TType val ty2: TType val canCoerce: CanCoerce + val hashCode: int - private new (ty1, ty2, canCoerce) = - { ty1 = ty1; ty2 = ty2; canCoerce = canCoerce } + private new (ty1, ty2, canCoerce, hashCode) = + { ty1 = ty1; ty2 = ty2; canCoerce = canCoerce; hashCode = hashCode } static member FromStrippedTypes (ty1, ty2, canCoerce) = - TTypeCacheKey(ty1, ty2, canCoerce) + let hashCode = + HashStamps.hashTType ty1 + |> pipeToHash (HashStamps.hashTType ty2) + |> pipeToHash (hash canCoerce) + TTypeCacheKey(ty1, ty2, canCoerce, hashCode) interface System.IEquatable with member this.Equals other = @@ -85,10 +90,7 @@ type TTypeCacheKey = | :? TTypeCacheKey as p -> (this :> System.IEquatable).Equals p | _ -> false - override this.GetHashCode() : int = - HashStamps.hashTType this.ty1 - |> pipeToHash (HashStamps.hashTType this.ty2) - |> pipeToHash (hash this.canCoerce) + override this.GetHashCode () : int = this.hashCode override this.ToString () = $"{this.ty1.DebugText}-{this.ty2.DebugText}" diff --git a/src/Compiler/Checking/import.fsi b/src/Compiler/Checking/import.fsi index 0ba2a635ec0..893e28baea0 100644 --- a/src/Compiler/Checking/import.fsi +++ b/src/Compiler/Checking/import.fsi @@ -45,14 +45,14 @@ type CanCoerce = [] type TTypeCacheKey = interface System.IEquatable - private new: ty1: TType * ty2: TType * canCoerce: CanCoerce -> TTypeCacheKey + private new: ty1: TType * ty2: TType * canCoerce: CanCoerce * hashCode: int -> TTypeCacheKey static member FromStrippedTypes: ty1: TType * ty2: TType * canCoerce: CanCoerce -> TTypeCacheKey val ty1: TType val ty2: TType val canCoerce: CanCoerce - //val tcGlobals: TcGlobals + val hashCode: int override GetHashCode: unit -> int /// Represents a context used for converting AbstractIL .NET and provided types to F# internal compiler data structures. diff --git a/src/Compiler/Utilities/TypeHashing.fs b/src/Compiler/Utilities/TypeHashing.fs index c066a8ece18..cca9963a7b7 100644 --- a/src/Compiler/Utilities/TypeHashing.fs +++ b/src/Compiler/Utilities/TypeHashing.fs @@ -358,34 +358,39 @@ module HashStamps = and stampEquals ty1 ty2 = match ty1, ty2 with | TType_ucase(u, tys1), TType_ucase(v, tys2) -> u.CaseName = v.CaseName && typeInstStampsEqual tys1 tys2 - | TType_app(tcref1, tinst1, Nullness.Known n1), TType_app(tcref2, tinst2, Nullness.Known n2) -> - n1 = n2 && tcref1.Stamp = tcref2.Stamp && typeInstStampsEqual tinst1 tinst2 | TType_app(tcref1, tinst1, n1), TType_app(tcref2, tinst2, n2) -> tcref1.Stamp = tcref2.Stamp && nullnessEquals n1 n2 && typeInstStampsEqual tinst1 tinst2 | TType_anon(info1, tys1), TType_anon(info2, tys2) -> info1.Stamp = info2.Stamp && typeInstStampsEqual tys1 tys2 | TType_tuple(c1, tys1), TType_tuple(c2, tys2) -> c1 = c2 && typeInstStampsEqual tys1 tys2 - | TType_forall(tps1, tau1), TType_forall(tps2, tau2) -> typarsStampsEqual tps1 tps2 && stampEquals tau1 tau2 + | TType_forall(tps1, tau1), TType_forall(tps2, tau2) -> stampEquals tau1 tau2 && typarsStampsEqual tps1 tps2 | TType_var(r1, n1), TType_var(r2, n2) -> r1.Stamp = r2.Stamp && nullnessEquals n1 n2 | TType_measure m1, TType_measure m2 -> measureStampEquals m1 m2 | _ -> false - let inline hashStamp (x: int64) = uint x * 2654435761u |> int + let inline hashStamp (x: Stamp) : Hash = uint x * 2654435761u |> int // 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 keys or evict from the cache. - let rec hashTType ty = + let rec hashTType ty : Hash = match ty with - | TType_ucase(_, tinst) -> tinst |> hashListOrderMatters (hashTType) - | TType_app(tcref, tinst, _) -> tinst |> hashListOrderMatters (hashTType) |> pipeToHash (hashStamp tcref.Stamp) + | TType_ucase(u, tinst) -> tinst |> hashListOrderMatters (hashTType) |> pipeToHash (hash u.CaseName) + | TType_app(tcref, tinst, Nullness.Known n) -> + tinst + |> hashListOrderMatters (hashTType) + |> pipeToHash (hashStamp tcref.Stamp) + |> pipeToHash (hash n) + | TType_app(tcref, tinst, Nullness.Variable _) -> tinst |> hashListOrderMatters (hashTType) |> pipeToHash (hashStamp tcref.Stamp) | TType_anon(info, tys) -> tys |> hashListOrderMatters (hashTType) |> pipeToHash (hashStamp info.Stamp) - | TType_tuple(_, tys) -> tys |> hashListOrderMatters (hashTType) + | TType_tuple(c, tys) -> tys |> hashListOrderMatters (hashTType) |> pipeToHash (hash c) | TType_forall(tps, tau) -> tps |> Seq.map _.Stamp |> hashListOrderMatters (hashStamp) |> pipeToHash (hashTType tau) - | TType_fun(d, r, _) -> hashTType d |> pipeToHash (hashTType r) - | TType_var(r, _) -> hashStamp r.Stamp + | TType_fun(d, r, Nullness.Known n) -> hashTType d |> pipeToHash (hashTType r) |> pipeToHash (hash n) + | TType_fun(d, r, Nullness.Variable _) -> hashTType d |> pipeToHash (hashTType r) + | TType_var(r, Nullness.Known n) -> hashStamp r.Stamp |> pipeToHash (hash n) + | TType_var(r, Nullness.Variable _) -> hashStamp r.Stamp | TType_measure _ -> 0 diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 4c0c384b6b6..d12d66bf660 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -344,7 +344,7 @@ type internal FSharpPackage() as this = do FSharp.Interactive.Hooks.fsiConsoleWindowPackageCtorUnsited (this :> Package) // Uncomment to view cache metrics in the output window - do Logging.FSharpServiceTelemetry.logCacheMetricsToOutput () + // do Logging.FSharpServiceTelemetry.logCacheMetricsToOutput () #if DEBUG From ec8fab4d49193dfad72c420d996a6b955cb3483c Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 26 Apr 2025 09:52:05 +0200 Subject: [PATCH 14/28] no metrics in ci --- eng/eng.sln | 35 ++++++++++ src/Compiler/Checking/import.fs | 4 +- src/Compiler/Utilities/Caches.fs | 11 +-- tests/FSharp.Test.Utilities/XunitHelpers.fs | 74 ++++++++++++--------- 4 files changed, 86 insertions(+), 38 deletions(-) create mode 100644 eng/eng.sln diff --git a/eng/eng.sln b/eng/eng.sln new file mode 100644 index 00000000000..fa866ee4e96 --- /dev/null +++ b/eng/eng.sln @@ -0,0 +1,35 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DumpPackageRoot", "DumpPackageRoot\DumpPackageRoot.csproj", "{B89EE536-0BE7-2F71-3534-80C865BF7F34}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{881589EE-639B-0A35-DBCE-5D94E4FC90C0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools", "common\internal\Tools.csproj", "{EC3C2A6A-F9C1-3B52-791D-863B6891FEFB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B89EE536-0BE7-2F71-3534-80C865BF7F34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B89EE536-0BE7-2F71-3534-80C865BF7F34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B89EE536-0BE7-2F71-3534-80C865BF7F34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B89EE536-0BE7-2F71-3534-80C865BF7F34}.Release|Any CPU.Build.0 = Release|Any CPU + {EC3C2A6A-F9C1-3B52-791D-863B6891FEFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC3C2A6A-F9C1-3B52-791D-863B6891FEFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC3C2A6A-F9C1-3B52-791D-863B6891FEFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC3C2A6A-F9C1-3B52-791D-863B6891FEFB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {EC3C2A6A-F9C1-3B52-791D-863B6891FEFB} = {881589EE-639B-0A35-DBCE-5D94E4FC90C0} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {69994860-C020-43F2-8564-C0EAC60E82FE} + EndGlobalSection +EndGlobal diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index a59f0e38e85..9b0491824ec 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -100,7 +100,7 @@ let createTypeSubsumptionCache (g: TcGlobals) = | CompilationMode.OneOff -> // This is a one-off compilation, so we don't need to worry about eviction. { CacheOptions.Default with - MaximumCapacity = 4 * 1024 + MaximumCapacity = 4 * 32768 EvictionMethod = EvictionMethod.NoEviction } | _ -> // Incremental use, so we need to set up the cache with eviction. @@ -110,7 +110,7 @@ let createTypeSubsumptionCache (g: TcGlobals) = MaximumCapacity = 4 * 32768 } Cache.Create(options) -let typeSubsumptionCaches = Cache.Create>({ CacheOptions.Default with MaximumCapacity = 16 }) +let typeSubsumptionCaches = Cache.Create>({ CacheOptions.Default with MaximumCapacity = 24 }) do typeSubsumptionCaches.ValueEvicted.Add <| _.Dispose() diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index c449335d01d..b093defeee9 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -70,6 +70,7 @@ type CacheMetrics(cacheId) = let readings = ConcurrentDictionary() +#if DEBUG let listener = new MeterListener( InstrumentPublished = @@ -82,6 +83,11 @@ type CacheMetrics(cacheId) = listener.SetMeasurementEventCallback(fun k v _ _ -> Interlocked.Add(readings.GetOrAdd(k.Name, ref 0L), v) |> ignore) listener.Start() + member this.Dispose() = listener.Dispose() +#else + member this.Dispose() = () +#endif + member val CacheId = cacheId member val RecentStats = "-" with get, set @@ -116,8 +122,6 @@ type CacheMetrics(cacheId) = else false - member this.Dispose() = listener.Dispose() - static member GetStats(cacheId) = instrumentedCaches[cacheId].TryUpdateStats(false) |> ignore instrumentedCaches[cacheId].RecentStats @@ -342,8 +346,7 @@ module Cache = let applyOverride (options: CacheOptions) = let capacity = match Environment.GetEnvironmentVariable(overrideVariable) with - | NonNull _ when options.MaximumCapacity < 100 -> 5 - | NonNull _ -> 8912 + | NonNull _ when options.MaximumCapacity > 1024 -> 1024 | _ -> options.MaximumCapacity { options with diff --git a/tests/FSharp.Test.Utilities/XunitHelpers.fs b/tests/FSharp.Test.Utilities/XunitHelpers.fs index a05809c2caa..446753fa65d 100644 --- a/tests/FSharp.Test.Utilities/XunitHelpers.fs +++ b/tests/FSharp.Test.Utilities/XunitHelpers.fs @@ -130,6 +130,46 @@ type CustomTheoryTestCase = #endif + +type OpenTelemetryExport(testRunName, enable) = + // On Windows forwarding localhost to wsl2 docker container sometimes does not work. Use IP address instead. + let otlpEndpoint = Uri("http://127.0.0.1:4317") + + // Configure OpenTelemetry export. + let providers : IDisposable list = + if not enable then [] else + [ + // Configure OpenTelemetry tracing 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 -> + o.Endpoint <- otlpEndpoint + o.Protocol <- OpenTelemetry.Exporter.OtlpExportProtocol.Grpc + // 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() + + // Configure OpenTelemetry metrics export. Metrics can be viewed in Prometheus or other compatible tools. + OpenTelemetry.Sdk.CreateMeterProviderBuilder() + .AddMeter(nameof FSharp.Compiler.CacheMetrics) + .AddMeter("System.Runtime") + .ConfigureResource(fun r -> r.AddService(testRunName) |> ignore) + .AddOtlpExporter(fun e m -> + e.Endpoint <- otlpEndpoint + e.Protocol <- OpenTelemetry.Exporter.OtlpExportProtocol.Grpc + m.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds <- 1000 + ) + .Build() + ] + + interface IDisposable with + member this.Dispose() = + for p in providers do p.Dispose() + /// `XunitTestFramework` providing parallel console support and conditionally enabling optional xUnit customizations. type FSharpXunitFramework(sink: IMessageSink) = inherit XunitTestFramework(sink) @@ -150,37 +190,9 @@ type FSharpXunitFramework(sink: IMessageSink) = FSharp.Compiler.Cache.OverrideMaxCapacityForTesting() let testRunName = $"RunTests_{assemblyName.Name} {Runtime.InteropServices.RuntimeInformation.FrameworkDescription}" - - // On Windows forwarding localhost to wsl2 docker container sometimes does not work. Use IP address instead. - let otlpEndpoint = Uri("http://127.0.0.1:4317") - - // Configure OpenTelemetry export. Traces can be viewed in Jaeger or other compatible tools. - use tracerProvider = - OpenTelemetry.Sdk.CreateTracerProviderBuilder() - .AddSource(ActivityNames.FscSourceName) - .ConfigureResource(fun r -> r.AddService("F#") |> ignore) - .AddOtlpExporter(fun o -> - o.Endpoint <- otlpEndpoint - o.Protocol <- OpenTelemetry.Exporter.OtlpExportProtocol.Grpc - // 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() - - use meterProvider = - OpenTelemetry.Sdk.CreateMeterProviderBuilder() - .AddMeter(nameof FSharp.Compiler.CacheMetrics) - .AddMeter("System.Runtime") - .ConfigureResource(fun r -> r.AddService(testRunName) |> ignore) - .AddOtlpExporter(fun e m -> - e.Endpoint <- otlpEndpoint - e.Protocol <- OpenTelemetry.Exporter.OtlpExportProtocol.Grpc - m.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds <- 1000 - ) - .Build() + use _ = new OpenTelemetryExport(testRunName, Environment.GetEnvironmentVariable("FSHARP_OTEL_EXPORT") <> null) + logConfig initialConfig log "Installing TestConsole redirection" TestConsole.install() @@ -192,8 +204,6 @@ type FSharpXunitFramework(sink: IMessageSink) = runner.RunAsync().Wait() end - tracerProvider.ForceFlush() |> ignore - cleanUpTemporaryDirectoryOfThisTestRun () } From 59cb94e8ed901730888a80a35d9d545e5815dbfa Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 26 Apr 2025 13:25:49 +0200 Subject: [PATCH 15/28] simplify --- src/Compiler/Checking/import.fs | 44 +++--- src/Compiler/Utilities/Caches.fs | 126 ++++++------------ src/Compiler/Utilities/Caches.fsi | 23 ---- .../src/FSharp.Editor/Common/Logging.fs | 2 +- .../LanguageService/LanguageService.fs | 4 +- 5 files changed, 63 insertions(+), 136 deletions(-) diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index 9b0491824ec..2c7fbe0ab1a 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -94,25 +94,27 @@ type TTypeCacheKey = override this.ToString () = $"{this.ty1.DebugText}-{this.ty2.DebugText}" -let createTypeSubsumptionCache (g: TcGlobals) = - let options = - match g.compilationMode with - | CompilationMode.OneOff -> - // This is a one-off compilation, so we don't need to worry about eviction. - { CacheOptions.Default with - MaximumCapacity = 4 * 32768 - EvictionMethod = EvictionMethod.NoEviction } - | _ -> - // Incremental use, so we need to set up the cache with eviction. - { CacheOptions.Default with - EvictionMethod = EvictionMethod.Background - PercentageToEvict = 5 - MaximumCapacity = 4 * 32768 } - Cache.Create(options) - -let typeSubsumptionCaches = Cache.Create>({ CacheOptions.Default with MaximumCapacity = 24 }) - -do typeSubsumptionCaches.ValueEvicted.Add <| _.Dispose() +//let createTypeSubsumptionCache (g: TcGlobals) = +// let options = +// match g.compilationMode with +// | CompilationMode.OneOff -> + +// { CacheOptions.Default with +// MaximumCapacity = 4 * 32768 +// // This is a one-off compilation, so we don't need to worry about eviction. +// PercentageToEvict = 0 } +// | _ -> +// // Incremental use, so we need to set up the cache with eviction. +// { CacheOptions.Default with +// PercentageToEvict = 5 +// MaximumCapacity = 4 * 32768 } +// Cache.Create(options) + +//let typeSubsumptionCaches = Cache.Create>({ CacheOptions.Default with MaximumCapacity = 24 }) + +let typeSubsumptionCache = lazy Cache.Create({ CacheOptions.Default with MaximumCapacity = 4 * 32768 }) + +//do typeSubsumptionCaches.ValueEvicted.Add <| _.Value.Dispose() //------------------------------------------------------------------------- // Import an IL types as F# types. @@ -136,8 +138,8 @@ type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) = member _.ILTypeRefToTyconRefCache = typeRefToTyconRefCache - member val TypeSubsumptionCache: Cache = - typeSubsumptionCaches.GetOrCreate(g, createTypeSubsumptionCache) + member val TypeSubsumptionCache: Cache = typeSubsumptionCache.Value + //typeSubsumptionCaches.GetOrCreate(g, createTypeSubsumptionCache) let CanImportILScopeRef (env: ImportMap) m scoref = diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index b093defeee9..5ab05142339 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -11,17 +11,11 @@ open System.Diagnostics.Metrics open FSharp.Compiler.Diagnostics -[] -type EvictionMethod = - | Background - | NoEviction - [] type CacheOptions = { MaximumCapacity: int PercentageToEvict: int - EvictionMethod: EvictionMethod LevelOfConcurrency: int } @@ -30,7 +24,6 @@ type CacheOptions = MaximumCapacity = 1024 PercentageToEvict = 5 LevelOfConcurrency = Environment.ProcessorCount - EvictionMethod = EvictionMethod.Background } [] @@ -161,63 +154,6 @@ type EntityPool<'Key, 'Value>(maximumCapacity, cacheId) = if pool.Count < maximumCapacity then pool.Add(entity) -type IEvictionQueue<'Key, 'Value> = - abstract member Add: CachedEntity<'Key, 'Value> -> unit - abstract member Update: CachedEntity<'Key, 'Value> -> unit - abstract member GetKeysToEvict: int -> 'Key[] - abstract member Remove: CachedEntity<'Key, 'Value> -> unit - -[] -type EvictionQueue<'Key, 'Value>() = - - let list = LinkedList>() - - interface IEvictionQueue<'Key, 'Value> with - - member _.Add(entity: CachedEntity<'Key, 'Value>) = - lock list - <| fun () -> - if isNull entity.Node.List then - list.AddLast(entity.Node) - else - assert false - - member _.Update(entity: CachedEntity<'Key, 'Value>) = - lock list - <| fun () -> - Interlocked.Increment(&entity.AccessCount) |> ignore - - let node = entity.Node - - // Sync between store and the eviction queue is not atomic. It might be already evicted or not yet added. - if node.List = list then - // Just move this node to the end of the list. - list.Remove(node) - list.AddLast(node) - - member _.GetKeysToEvict(count) = - lock list - <| fun () -> list |> Seq.map _.Key |> Seq.truncate count |> Seq.toArray - - member this.Remove(entity: CachedEntity<_, _>) = - lock list - <| fun () -> - if entity.Node.List = list then - list.Remove(entity.Node) - - member _.Count = list.Count - - static member NoEviction = - { new IEvictionQueue<'Key, 'Value> with - member _.Add(_) = () - - member _.Update(entity) = - Interlocked.Increment(&entity.AccessCount) |> ignore - - member _.GetKeysToEvict(_) = [||] - member _.Remove(_) = () - } - [] [] type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> @@ -236,39 +172,52 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> let evictionFails = CacheMetrics.meter.CreateCounter("eviction-fails", "count", instanceId) - let eviction = Event<'Value>() - let pool = EntityPool<'Key, 'Value>(capacity, instanceId) let store = ConcurrentDictionary<'Key, CachedEntity<'Key, 'Value>>(options.LevelOfConcurrency, capacity) - let evictionQueue: IEvictionQueue<'Key, 'Value> = - match options.EvictionMethod with - | EvictionMethod.NoEviction -> EvictionQueue.NoEviction - | _ -> EvictionQueue() + let evictionQueue = LinkedList>() - let tryEvictItems () = - let count = - if store.Count > options.MaximumCapacity then - (store.Count - options.MaximumCapacity) - + int (float options.MaximumCapacity * float options.PercentageToEvict / 100.0) + let addToEvictionQueue (entity: CachedEntity<'Key, 'Value>) = + lock evictionQueue + <| fun () -> + if isNull entity.Node.List then + evictionQueue.AddLast(entity.Node) else - 0 + assert false + + let updateEvictionQueue (entity: CachedEntity<'Key, 'Value>) = + lock evictionQueue + <| fun () -> + + let node = entity.Node - for key in evictionQueue.GetKeysToEvict(count) do - match store.TryRemove(key) with + // Sync between store and the eviction queue is not atomic. It might be already evicted or not yet added. + if node.List = evictionQueue then + // Just move this node to the end of the list. + evictionQueue.Remove(node) + evictionQueue.AddLast(node) + + let targetCount = + options.MaximumCapacity + - int (float options.MaximumCapacity * float options.PercentageToEvict / 100.0) + + do assert (targetCount >= 0) + + let tryEvictOne () = + match evictionQueue.First with + | null -> evictionFails.Add 1L + | first -> + match store.TryRemove(first.Value.Key) with | true, removed -> - evictionQueue.Remove(removed) + lock evictionQueue <| fun () -> evictionQueue.Remove(removed.Node) pool.Reclaim(removed) - eviction.Trigger(removed.Value) evictions.Add 1L | _ -> evictionFails.Add 1L 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, @@ -280,11 +229,14 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> if delay > 0.0 then do! Async.Sleep(int delay) + while store.Count > targetCount && evictionQueue.Count > 0 do + tryEvictOne () + return! backgroundEviction () } do - if options.EvictionMethod = EvictionMethod.Background then + if options.PercentageToEvict > 0 then Async.Start(backgroundEviction (), cancellationToken = cts.Token) member val Name = instanceId @@ -293,7 +245,8 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> match store.TryGetValue(key) with | true, cachedEntity -> hits.Add 1L - evictionQueue.Update(cachedEntity) + Interlocked.Increment(&cachedEntity.AccessCount) |> ignore + updateEvictionQueue cachedEntity value <- cachedEntity.Value true | _ -> @@ -305,7 +258,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> let cachedEntity = pool.Acquire(key, value) if store.TryAdd(key, cachedEntity) then - evictionQueue.Add(cachedEntity) + addToEvictionQueue cachedEntity true else pool.Reclaim(cachedEntity) @@ -319,9 +272,6 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> this.TryAdd(key, value) |> ignore value - [] - member val ValueEvicted = eviction.Publish - interface IDisposable with member this.Dispose() = store.Clear() diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index 0cd58d85742..eb57ea715ab 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -3,16 +3,10 @@ namespace FSharp.Compiler open System open System.Threading -[] -type internal EvictionMethod = - | Background - | NoEviction - [] type internal CacheOptions = { MaximumCapacity: int PercentageToEvict: int - EvictionMethod: EvictionMethod LevelOfConcurrency: int } static member Default: CacheOptions @@ -24,31 +18,14 @@ type internal CachedEntity<'Key, 'Value> = member ReUse: key: 'Key * value: 'Value -> CachedEntity<'Key, 'Value> override ToString: unit -> string -type internal IEvictionQueue<'Key, 'Value> = - abstract member Add: CachedEntity<'Key, 'Value> -> unit - abstract member Update: CachedEntity<'Key, 'Value> -> unit - abstract member GetKeysToEvict: int -> 'Key[] - abstract member Remove: CachedEntity<'Key, 'Value> -> unit - -[] -type internal EvictionQueue<'Key, 'Value> = - new: unit -> EvictionQueue<'Key, 'Value> - member Count: int - static member NoEviction: IEvictionQueue<'Key, 'Value> - interface IEvictionQueue<'Key, 'Value> - [] type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = new: options: CacheOptions * capacity: int * cts: CancellationTokenSource * ?name: string -> Cache<'Key, 'Value> member TryGetValue: key: 'Key * value: outref<'Value> -> bool member TryAdd: key: 'Key * value: 'Value -> bool member GetOrCreate: key: 'Key * valueFactory: ('Key -> 'Value) -> 'Value - member GetStats: unit -> string member Dispose: unit -> unit - [] - member ValueEvicted: IEvent<'Value> - interface IDisposable type internal CacheMetrics = diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs index 08f8ca468b4..44ba9ee26ee 100644 --- a/vsintegration/src/FSharp.Editor/Common/Logging.fs +++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs @@ -176,7 +176,7 @@ module FSharpServiceTelemetry = OpenTelemetry.Sdk .CreateMeterProviderBuilder() .ConfigureResource(fun r -> r.AddService("F#") |> ignore) - .AddMeter(nameof FSharp.Compiler.CacheInstrumentation) + .AddMeter(nameof FSharp.Compiler.CacheMetrics) .AddMeter("System.Runtime") .AddOtlpExporter(fun e m -> e.Endpoint <- otlpEndpoint diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index d12d66bf660..4eb399a39f3 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -343,10 +343,8 @@ type internal FSharpPackage() as this = // FSI-LINKAGE-POINT: unsited init do FSharp.Interactive.Hooks.fsiConsoleWindowPackageCtorUnsited (this :> Package) - // Uncomment to view cache metrics in the output window - // do Logging.FSharpServiceTelemetry.logCacheMetricsToOutput () - #if DEBUG + do Logging.FSharpServiceTelemetry.logCacheMetricsToOutput () let flushTelemetry = Logging.FSharpServiceTelemetry.otelExport () From b8ba293ae84a8278121173657a5d51a5a9fdb1a6 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 26 Apr 2025 17:55:41 +0200 Subject: [PATCH 16/28] simplify --- src/Compiler/Utilities/Caches.fs | 44 +++++++++++++------------------ src/Compiler/Utilities/Caches.fsi | 1 - 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 5ab05142339..fb8ba4d43d3 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -5,12 +5,9 @@ open System open System.Collections.Generic open System.Collections.Concurrent open System.Threading -open System.Threading.Tasks open System.Diagnostics open System.Diagnostics.Metrics -open FSharp.Compiler.Diagnostics - [] type CacheOptions = { @@ -54,25 +51,25 @@ type CachedEntity<'Key, 'Value> = override this.ToString() = $"{this.Key}" -module CacheMetrics = - let meter = new Meter("FSharp.Compiler.Cache") - +// Currently the Cache itself exposes Metrics.Counters that count raw cache events: hits, misses, evictions etc. +// This class observes those counters and keeps a snapshot of readings. For now this is used only to print cache stats in debug mode. +// TODO: We could add some System.Diagnostics.Metrics.Gauge instruments to this class, to get computed stats also exposed as metrics. type CacheMetrics(cacheId) = + static let meter = new Meter("FSharp.Compiler.Cache") static let instrumentedCaches = ConcurrentDictionary() let readings = ConcurrentDictionary() #if DEBUG - let listener = - new MeterListener( - InstrumentPublished = - fun i l -> - if i.Meter = CacheMetrics.meter && i.Description = cacheId then - l.EnableMeasurementEvents(i) - ) + let listener = new MeterListener() do + listener.InstrumentPublished <- + fun i l -> + if i.Meter = meter && i.Description = cacheId then + l.EnableMeasurementEvents(i) + listener.SetMeasurementEventCallback(fun k v _ _ -> Interlocked.Add(readings.GetOrAdd(k.Name, ref 0L), v) |> ignore) listener.Start() @@ -83,6 +80,8 @@ type CacheMetrics(cacheId) = member val CacheId = cacheId + static member val Meter = meter + member val RecentStats = "-" with get, set member this.TryUpdateStats(clearCounts) = @@ -139,7 +138,7 @@ type EntityPool<'Key, 'Value>(maximumCapacity, cacheId) = let mutable created = 0 let overCapacity = - CacheMetrics.meter.CreateCounter("over-capacity", "count", cacheId) + CacheMetrics.Meter.CreateCounter("over-capacity", "count", cacheId) member _.Acquire(key, value) = match pool.TryTake() with @@ -163,14 +162,14 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> let instanceId = defaultArg name $"cache-{Interlocked.Increment(&cacheId)}" - let hits = CacheMetrics.meter.CreateCounter("hits", "count", instanceId) - let misses = CacheMetrics.meter.CreateCounter("misses", "count", instanceId) + let hits = CacheMetrics.Meter.CreateCounter("hits", "count", instanceId) + let misses = CacheMetrics.Meter.CreateCounter("misses", "count", instanceId) let evictions = - CacheMetrics.meter.CreateCounter("evictions", "count", instanceId) + CacheMetrics.Meter.CreateCounter("evictions", "count", instanceId) let evictionFails = - CacheMetrics.meter.CreateCounter("eviction-fails", "count", instanceId) + CacheMetrics.Meter.CreateCounter("eviction-fails", "count", instanceId) let pool = EntityPool<'Key, 'Value>(capacity, instanceId) @@ -199,6 +198,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> evictionQueue.Remove(node) evictionQueue.AddLast(node) + // If items count exceeds this, evictions ensue. let targetCount = options.MaximumCapacity - int (float options.MaximumCapacity * float options.PercentageToEvict / 100.0) @@ -264,14 +264,6 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> pool.Reclaim(cachedEntity) false - member this.GetOrCreate(key: 'Key, valueFactory: 'Key -> 'Value) = - match this.TryGetValue(key) with - | true, value -> value - | _ -> - let value = valueFactory key - this.TryAdd(key, value) |> ignore - value - interface IDisposable with member this.Dispose() = store.Clear() diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index eb57ea715ab..07146dc1ad5 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -23,7 +23,6 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = new: options: CacheOptions * capacity: int * cts: CancellationTokenSource * ?name: string -> Cache<'Key, 'Value> member TryGetValue: key: 'Key * value: outref<'Value> -> bool member TryAdd: key: 'Key * value: 'Value -> bool - member GetOrCreate: key: 'Key * valueFactory: ('Key -> 'Value) -> 'Value member Dispose: unit -> unit interface IDisposable From 13cded832fbae58fc732b058a02bdb9460ef3f01 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Sat, 26 Apr 2025 20:19:23 +0200 Subject: [PATCH 17/28] clean up --- eng/eng.sln | 35 -------------------------------- src/Compiler/Utilities/Caches.fs | 1 + 2 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 eng/eng.sln diff --git a/eng/eng.sln b/eng/eng.sln deleted file mode 100644 index fa866ee4e96..00000000000 --- a/eng/eng.sln +++ /dev/null @@ -1,35 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.2.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DumpPackageRoot", "DumpPackageRoot\DumpPackageRoot.csproj", "{B89EE536-0BE7-2F71-3534-80C865BF7F34}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{881589EE-639B-0A35-DBCE-5D94E4FC90C0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools", "common\internal\Tools.csproj", "{EC3C2A6A-F9C1-3B52-791D-863B6891FEFB}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B89EE536-0BE7-2F71-3534-80C865BF7F34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B89EE536-0BE7-2F71-3534-80C865BF7F34}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B89EE536-0BE7-2F71-3534-80C865BF7F34}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B89EE536-0BE7-2F71-3534-80C865BF7F34}.Release|Any CPU.Build.0 = Release|Any CPU - {EC3C2A6A-F9C1-3B52-791D-863B6891FEFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC3C2A6A-F9C1-3B52-791D-863B6891FEFB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC3C2A6A-F9C1-3B52-791D-863B6891FEFB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC3C2A6A-F9C1-3B52-791D-863B6891FEFB}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {EC3C2A6A-F9C1-3B52-791D-863B6891FEFB} = {881589EE-639B-0A35-DBCE-5D94E4FC90C0} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {69994860-C020-43F2-8564-C0EAC60E82FE} - EndGlobalSection -EndGlobal diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index fb8ba4d43d3..6e44ffa3ca7 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -186,6 +186,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> else assert false + // Only LRU currrently. We can add other strategies when needed. let updateEvictionQueue (entity: CachedEntity<'Key, 'Value>) = lock evictionQueue <| fun () -> From 365ae5b288939642ce8b65b3e147be630666f88a Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:46:41 +0200 Subject: [PATCH 18/28] cut unnecessary stuff --- src/Compiler/Checking/import.fs | 42 +++--------- src/Compiler/Checking/import.fsi | 4 +- src/Compiler/Utilities/Caches.fs | 71 ++++++++------------- src/Compiler/Utilities/Caches.fsi | 9 ++- src/Compiler/Utilities/TypeHashing.fs | 2 +- tests/FSharp.Test.Utilities/XunitHelpers.fs | 2 +- 6 files changed, 44 insertions(+), 86 deletions(-) diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index 2c7fbe0ab1a..6368bf620ba 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -7,13 +7,10 @@ open System.Collections.Concurrent open System.Collections.Generic open System.Collections.Immutable open System.Diagnostics -open System.Runtime.CompilerServices -open System.Threading 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 @@ -63,17 +60,12 @@ type TTypeCacheKey = val ty1: TType val ty2: TType val canCoerce: CanCoerce - val hashCode: int - private new (ty1, ty2, canCoerce, hashCode) = - { ty1 = ty1; ty2 = ty2; canCoerce = canCoerce; hashCode = hashCode } + private new (ty1, ty2, canCoerce) = + { ty1 = ty1; ty2 = ty2; canCoerce = canCoerce } static member FromStrippedTypes (ty1, ty2, canCoerce) = - let hashCode = - HashStamps.hashTType ty1 - |> pipeToHash (HashStamps.hashTType ty2) - |> pipeToHash (hash canCoerce) - TTypeCacheKey(ty1, ty2, canCoerce, hashCode) + TTypeCacheKey(ty1, ty2, canCoerce) interface System.IEquatable with member this.Equals other = @@ -90,31 +82,14 @@ type TTypeCacheKey = | :? TTypeCacheKey as p -> (this :> System.IEquatable).Equals p | _ -> false - override this.GetHashCode () : int = this.hashCode + override this.GetHashCode () : int = + HashStamps.hashTType this.ty1 + |> pipeToHash (HashStamps.hashTType this.ty2) + |> pipeToHash (hash this.canCoerce) override this.ToString () = $"{this.ty1.DebugText}-{this.ty2.DebugText}" -//let createTypeSubsumptionCache (g: TcGlobals) = -// let options = -// match g.compilationMode with -// | CompilationMode.OneOff -> - -// { CacheOptions.Default with -// MaximumCapacity = 4 * 32768 -// // This is a one-off compilation, so we don't need to worry about eviction. -// PercentageToEvict = 0 } -// | _ -> -// // Incremental use, so we need to set up the cache with eviction. -// { CacheOptions.Default with -// PercentageToEvict = 5 -// MaximumCapacity = 4 * 32768 } -// Cache.Create(options) - -//let typeSubsumptionCaches = Cache.Create>({ CacheOptions.Default with MaximumCapacity = 24 }) - -let typeSubsumptionCache = lazy Cache.Create({ CacheOptions.Default with MaximumCapacity = 4 * 32768 }) - -//do typeSubsumptionCaches.ValueEvicted.Add <| _.Value.Dispose() +let typeSubsumptionCache = lazy Cache.Create({ CacheOptions.Default with Capacity = 131072 }) //------------------------------------------------------------------------- // Import an IL types as F# types. @@ -139,7 +114,6 @@ type ImportMap(g: TcGlobals, assemblyLoader: AssemblyLoader) = member _.ILTypeRefToTyconRefCache = typeRefToTyconRefCache member val TypeSubsumptionCache: Cache = typeSubsumptionCache.Value - //typeSubsumptionCaches.GetOrCreate(g, createTypeSubsumptionCache) let CanImportILScopeRef (env: ImportMap) m scoref = diff --git a/src/Compiler/Checking/import.fsi b/src/Compiler/Checking/import.fsi index 893e28baea0..120c3b67810 100644 --- a/src/Compiler/Checking/import.fsi +++ b/src/Compiler/Checking/import.fsi @@ -45,14 +45,14 @@ type CanCoerce = [] type TTypeCacheKey = interface System.IEquatable - private new: ty1: TType * ty2: TType * canCoerce: CanCoerce * hashCode: int -> TTypeCacheKey + private new: ty1: TType * ty2: TType * canCoerce: CanCoerce -> TTypeCacheKey static member FromStrippedTypes: ty1: TType * ty2: TType * canCoerce: CanCoerce -> TTypeCacheKey val ty1: TType val ty2: TType val canCoerce: CanCoerce - val hashCode: int + override GetHashCode: unit -> int /// Represents a context used for converting AbstractIL .NET and provided types to F# internal compiler data structures. diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 6e44ffa3ca7..40a33377a2b 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -11,16 +11,14 @@ open System.Diagnostics.Metrics [] type CacheOptions = { - MaximumCapacity: int - PercentageToEvict: int - LevelOfConcurrency: int + Capacity: int + HeadroomPercentage: int } static member Default = { - MaximumCapacity = 1024 - PercentageToEvict = 5 - LevelOfConcurrency = Environment.ProcessorCount + Capacity = 1024 + HeadroomPercentage = 10 } [] @@ -133,7 +131,7 @@ type CacheMetrics(cacheId) = instrumentedCaches[cacheId].Dispose() instrumentedCaches.TryRemove(cacheId) |> ignore -type EntityPool<'Key, 'Value>(maximumCapacity, cacheId) = +type EntityPool<'Key, 'Value>(totalCapacity, cacheId) = let pool = ConcurrentBag>() let mutable created = 0 @@ -144,19 +142,19 @@ type EntityPool<'Key, 'Value>(maximumCapacity, cacheId) = match pool.TryTake() with | true, entity -> entity.ReUse(key, value) | _ -> - if Interlocked.Increment &created > maximumCapacity then + if Interlocked.Increment &created > totalCapacity then overCapacity.Add 1L CachedEntity(key, value).WithNode() member _.Reclaim(entity: CachedEntity<'Key, 'Value>) = - if pool.Count < maximumCapacity then + if pool.Count < totalCapacity then pool.Add(entity) [] [] type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> - internal (options: CacheOptions, capacity, cts: CancellationTokenSource, ?name) = + internal (capacity, headroom, cts: CancellationTokenSource, ?name) = static let mutable cacheId = 0 @@ -171,20 +169,19 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> let evictionFails = CacheMetrics.Meter.CreateCounter("eviction-fails", "count", instanceId) - let pool = EntityPool<'Key, 'Value>(capacity, instanceId) + let totalCapacity = capacity + headroom + + let pool = EntityPool<'Key, 'Value>(totalCapacity, instanceId) let store = - ConcurrentDictionary<'Key, CachedEntity<'Key, 'Value>>(options.LevelOfConcurrency, capacity) + ConcurrentDictionary<'Key, CachedEntity<'Key, 'Value>>(Environment.ProcessorCount, totalCapacity) let evictionQueue = LinkedList>() let addToEvictionQueue (entity: CachedEntity<'Key, 'Value>) = lock evictionQueue <| fun () -> - if isNull entity.Node.List then - evictionQueue.AddLast(entity.Node) - else - assert false + evictionQueue.AddLast(entity.Node) // Only LRU currrently. We can add other strategies when needed. let updateEvictionQueue (entity: CachedEntity<'Key, 'Value>) = @@ -199,27 +196,20 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> evictionQueue.Remove(node) evictionQueue.AddLast(node) - // If items count exceeds this, evictions ensue. - let targetCount = - options.MaximumCapacity - - int (float options.MaximumCapacity * float options.PercentageToEvict / 100.0) - - do assert (targetCount >= 0) - let tryEvictOne () = match evictionQueue.First with | null -> evictionFails.Add 1L | first -> match store.TryRemove(first.Value.Key) with | true, removed -> - lock evictionQueue <| fun () -> evictionQueue.Remove(removed.Node) + lock evictionQueue <| fun () -> evictionQueue.Remove(first) pool.Reclaim(removed) evictions.Add 1L | _ -> evictionFails.Add 1L let rec backgroundEviction () = async { - let utilization = (float store.Count / float options.MaximumCapacity) + let utilization = (float store.Count / float totalCapacity) // 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. @@ -230,15 +220,14 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> if delay > 0.0 then do! Async.Sleep(int delay) - while store.Count > targetCount && evictionQueue.Count > 0 do + while store.Count > capacity && evictionQueue.Count > 0 do tryEvictOne () return! backgroundEviction () } do - if options.PercentageToEvict > 0 then - Async.Start(backgroundEviction (), cancellationToken = cts.Token) + Async.Start(backgroundEviction (), cancellationToken = cts.Token) member val Name = instanceId @@ -283,27 +272,23 @@ module Cache = let private overrideVariable = "FSHARP_CACHE_OVERRIDE" /// Use for testing purposes to reduce memory consumption in testhost and its subprocesses. - let OverrideMaxCapacityForTesting () = + let OverrideCapacityForTesting () = Environment.SetEnvironmentVariable(overrideVariable, "true", EnvironmentVariableTarget.Process) - let applyOverride (options: CacheOptions) = - let capacity = - match Environment.GetEnvironmentVariable(overrideVariable) with - | NonNull _ when options.MaximumCapacity > 1024 -> 1024 - | _ -> options.MaximumCapacity - - { options with - MaximumCapacity = capacity - } + let applyOverride (capacity: int) = + match Environment.GetEnvironmentVariable(overrideVariable) with + | NonNull _ when capacity > 1024 -> 1024 + | _ -> capacity let Create<'Key, 'Value when 'Key: not null and 'Key: equality> (options: CacheOptions) = - let options = applyOverride options + if options.Capacity < 0 then invalidArg "Capacity" "Capacity must be positive" + if options.HeadroomPercentage < 0 then invalidArg "HeadroomPercentage" "HeadroomPercentage must be positive" + + let capacity = applyOverride options.Capacity // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary. - let capacity = - options.MaximumCapacity - + int (float options.MaximumCapacity * float options.PercentageToEvict / 100.0) + let headroom = int (float options.Capacity * float options.HeadroomPercentage / 100.0) let cts = new CancellationTokenSource() - let cache = new Cache<'Key, 'Value>(options, capacity, cts) + let cache = new Cache<'Key, 'Value>(capacity, headroom, cts) CacheMetrics.AddInstrumentation cache.Name |> ignore cache diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index 07146dc1ad5..e90f7098ef7 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -5,9 +5,8 @@ open System.Threading [] type internal CacheOptions = - { MaximumCapacity: int - PercentageToEvict: int - LevelOfConcurrency: int } + { Capacity: int + HeadroomPercentage: int } static member Default: CacheOptions @@ -20,7 +19,7 @@ type internal CachedEntity<'Key, 'Value> = [] type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = - new: options: CacheOptions * capacity: int * cts: CancellationTokenSource * ?name: string -> Cache<'Key, 'Value> + new: capacity: int * headroom: int * cts: CancellationTokenSource * ?name: string -> Cache<'Key, 'Value> member TryGetValue: key: 'Key * value: outref<'Value> -> bool member TryAdd: key: 'Key * value: 'Value -> bool member Dispose: unit -> unit @@ -38,5 +37,5 @@ type internal CacheMetrics = static member RemoveInstrumentation: cacheId: string -> unit module internal Cache = - val OverrideMaxCapacityForTesting: unit -> unit + val OverrideCapacityForTesting: unit -> unit val Create<'Key, 'Value when 'Key: not null and 'Key: equality> : options: CacheOptions -> Cache<'Key, 'Value> diff --git a/src/Compiler/Utilities/TypeHashing.fs b/src/Compiler/Utilities/TypeHashing.fs index cca9963a7b7..7907c2148dc 100644 --- a/src/Compiler/Utilities/TypeHashing.fs +++ b/src/Compiler/Utilities/TypeHashing.fs @@ -351,7 +351,7 @@ module HashStamps = and nullnessEquals (n1: Nullness) (n2: Nullness) = match n1, n2 with - | Nullness.Known n1, Nullness.Known n2 -> n1 = n2 + | Nullness.Known k1, Nullness.Known k2 -> k1 = k2 | Nullness.Variable _, Nullness.Variable _ -> true | _ -> false diff --git a/tests/FSharp.Test.Utilities/XunitHelpers.fs b/tests/FSharp.Test.Utilities/XunitHelpers.fs index 446753fa65d..8502cd98f06 100644 --- a/tests/FSharp.Test.Utilities/XunitHelpers.fs +++ b/tests/FSharp.Test.Utilities/XunitHelpers.fs @@ -187,7 +187,7 @@ type FSharpXunitFramework(sink: IMessageSink) = #endif // Override cache capacity to reduce memory usage in CI. - FSharp.Compiler.Cache.OverrideMaxCapacityForTesting() + FSharp.Compiler.Cache.OverrideCapacityForTesting() let testRunName = $"RunTests_{assemblyName.Name} {Runtime.InteropServices.RuntimeInformation.FrameworkDescription}" From 96fa9668519e1fac24deb277167a5601edc5bf77 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Sun, 27 Apr 2025 08:30:28 +0200 Subject: [PATCH 19/28] add name --- src/Compiler/Checking/import.fs | 2 +- src/Compiler/Utilities/Caches.fs | 4 ++-- src/Compiler/Utilities/Caches.fsi | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index 6368bf620ba..803f9270d19 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -89,7 +89,7 @@ type TTypeCacheKey = override this.ToString () = $"{this.ty1.DebugText}-{this.ty2.DebugText}" -let typeSubsumptionCache = lazy Cache.Create({ CacheOptions.Default with Capacity = 131072 }) +let typeSubsumptionCache = lazy Cache.Create("TypeSubsumptionCache", { CacheOptions.Default with Capacity = 131072 }) //------------------------------------------------------------------------- // Import an IL types as F# types. diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 40a33377a2b..ea6315aad44 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -280,7 +280,7 @@ module Cache = | NonNull _ when capacity > 1024 -> 1024 | _ -> capacity - let Create<'Key, 'Value when 'Key: not null and 'Key: equality> (options: CacheOptions) = + let Create<'Key, 'Value when 'Key: not null and 'Key: equality> (name, options: CacheOptions) = if options.Capacity < 0 then invalidArg "Capacity" "Capacity must be positive" if options.HeadroomPercentage < 0 then invalidArg "HeadroomPercentage" "HeadroomPercentage must be positive" @@ -289,6 +289,6 @@ module Cache = let headroom = int (float options.Capacity * float options.HeadroomPercentage / 100.0) let cts = new CancellationTokenSource() - let cache = new Cache<'Key, 'Value>(capacity, headroom, cts) + let cache = new Cache<'Key, 'Value>(capacity, headroom, cts, name) CacheMetrics.AddInstrumentation cache.Name |> ignore cache diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index e90f7098ef7..1655f27cba7 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -38,4 +38,4 @@ type internal CacheMetrics = module internal Cache = val OverrideCapacityForTesting: unit -> unit - val Create<'Key, 'Value when 'Key: not null and 'Key: equality> : options: CacheOptions -> Cache<'Key, 'Value> + val Create<'Key, 'Value when 'Key: not null and 'Key: equality> : name: string * options: CacheOptions -> Cache<'Key, 'Value> From 2743a4dc540cbba0096b61e899bf99bd816ba351 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:07:46 +0200 Subject: [PATCH 20/28] naming --- src/Compiler/Checking/import.fs | 5 +- src/Compiler/Checking/import.fsi | 3 +- src/Compiler/Utilities/Caches.fs | 85 ++++++++++--------- src/Compiler/Utilities/Caches.fsi | 12 ++- tests/FSharp.Test.Utilities/XunitHelpers.fs | 5 +- .../src/FSharp.Editor/Common/Logging.fs | 6 +- 6 files changed, 63 insertions(+), 53 deletions(-) diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index 803f9270d19..ac64cada5a9 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -24,6 +24,7 @@ open FSharp.Compiler.TypedTree open FSharp.Compiler.TypedTreeBasics open FSharp.Compiler.TypedTreeOps open FSharp.Compiler.TcGlobals +open FSharp.Compiler.Caches #if !NO_TYPEPROVIDERS open FSharp.Compiler.TypeProviders @@ -89,7 +90,9 @@ type TTypeCacheKey = override this.ToString () = $"{this.ty1.DebugText}-{this.ty2.DebugText}" -let typeSubsumptionCache = lazy Cache.Create("TypeSubsumptionCache", { CacheOptions.Default with Capacity = 131072 }) +let typeSubsumptionCache = + // Leave most of the capacity in reserve for bursts. + lazy Cache.Create("TypeSubsumptionCache", { TotalCapacity = 131072; HeadroomPercentage = 75 }) //------------------------------------------------------------------------- // Import an IL types as F# types. diff --git a/src/Compiler/Checking/import.fsi b/src/Compiler/Checking/import.fsi index 120c3b67810..72611e12bf7 100644 --- a/src/Compiler/Checking/import.fsi +++ b/src/Compiler/Checking/import.fsi @@ -5,13 +5,12 @@ module internal FSharp.Compiler.Import open Internal.Utilities.Library open FSharp.Compiler.AbstractIL.IL +open FSharp.Compiler.Caches open FSharp.Compiler.TcGlobals open FSharp.Compiler.Text open FSharp.Compiler.Xml open FSharp.Compiler.TypedTree -open System.Collections.Concurrent - #if !NO_TYPEPROVIDERS open FSharp.Compiler.TypeProviders #endif diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index ea6315aad44..18561b7c5d0 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -1,5 +1,5 @@ // LinkedList uses nulls, so we need to disable the nullability warnings for this file. -namespace FSharp.Compiler +namespace FSharp.Compiler.Caches open System open System.Collections.Generic @@ -11,13 +11,16 @@ open System.Diagnostics.Metrics [] type CacheOptions = { - Capacity: int + /// Total capacity, determines the size of the underlying store. + TotalCapacity: int + + /// Safety margin size as a percentage of TotalCapacity. HeadroomPercentage: int } static member Default = { - Capacity = 1024 + TotalCapacity = 1024 HeadroomPercentage = 10 } @@ -83,24 +86,24 @@ type CacheMetrics(cacheId) = member val RecentStats = "-" with get, set member this.TryUpdateStats(clearCounts) = - let stats = + let ratio = try - let ratio = - float readings["hits"].Value - / float (readings["hits"].Value + readings["misses"].Value) - * 100.0 - - [ - for name in readings.Keys do - let v = readings[name].Value - - if v > 0 then - $"{name}: {v}" - ] - |> String.concat ", " - |> sprintf "%s | hit ratio: %s %s" this.CacheId (if Double.IsNaN(ratio) then "-" else $"%.1f{ratio}%%") + float readings["hits"].Value + / float (readings["hits"].Value + readings["misses"].Value) + * 100.0 with _ -> - "!" + Double.NaN + + let stats = + [ + for name in readings.Keys do + let v = readings[name].Value + + if v > 0 then + $"{name}: {v}" + ] + |> String.concat ", " + |> sprintf "%s | hit ratio: %s %s" this.CacheId (if Double.IsNaN(ratio) then "-" else $"%.1f{ratio}%%") if clearCounts then for r in readings.Values do @@ -153,23 +156,19 @@ type EntityPool<'Key, 'Value>(totalCapacity, cacheId) = [] [] -type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> - internal (capacity, headroom, cts: CancellationTokenSource, ?name) = +type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalCapacity, headroom, cts: CancellationTokenSource, ?name) = static let mutable cacheId = 0 let instanceId = defaultArg name $"cache-{Interlocked.Increment(&cacheId)}" - let hits = CacheMetrics.Meter.CreateCounter("hits", "count", instanceId) - let misses = CacheMetrics.Meter.CreateCounter("misses", "count", instanceId) - - let evictions = - CacheMetrics.Meter.CreateCounter("evictions", "count", instanceId) + let meter = CacheMetrics.Meter + let hits = meter.CreateCounter("hits", "count", instanceId) + let misses = meter.CreateCounter("misses", "count", instanceId) + let evictions = meter.CreateCounter("evictions", "count", instanceId) let evictionFails = - CacheMetrics.Meter.CreateCounter("eviction-fails", "count", instanceId) - - let totalCapacity = capacity + headroom + meter.CreateCounter("eviction-fails", "count", instanceId) let pool = EntityPool<'Key, 'Value>(totalCapacity, instanceId) @@ -179,9 +178,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> let evictionQueue = LinkedList>() let addToEvictionQueue (entity: CachedEntity<'Key, 'Value>) = - lock evictionQueue - <| fun () -> - evictionQueue.AddLast(entity.Node) + lock evictionQueue <| fun () -> evictionQueue.AddLast(entity.Node) // Only LRU currrently. We can add other strategies when needed. let updateEvictionQueue (entity: CachedEntity<'Key, 'Value>) = @@ -207,6 +204,9 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> evictions.Add 1L | _ -> evictionFails.Add 1L + // Non-evictable capacity. + let capacity = totalCapacity - headroom + let rec backgroundEviction () = async { let utilization = (float store.Count / float totalCapacity) @@ -220,14 +220,13 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> if delay > 0.0 then do! Async.Sleep(int delay) - while store.Count > capacity && evictionQueue.Count > 0 do + while store.Count > capacity - headroom && evictionQueue.Count > 0 do tryEvictOne () return! backgroundEviction () } - do - Async.Start(backgroundEviction (), cancellationToken = cts.Token) + do Async.Start(backgroundEviction (), cancellationToken = cts.Token) member val Name = instanceId @@ -281,14 +280,18 @@ module Cache = | _ -> capacity let Create<'Key, 'Value when 'Key: not null and 'Key: equality> (name, options: CacheOptions) = - if options.Capacity < 0 then invalidArg "Capacity" "Capacity must be positive" - if options.HeadroomPercentage < 0 then invalidArg "HeadroomPercentage" "HeadroomPercentage must be positive" + if options.TotalCapacity < 0 then + invalidArg "Capacity" "Capacity must be positive" + + if options.HeadroomPercentage < 0 then + invalidArg "HeadroomPercentage" "HeadroomPercentage must be positive" - let capacity = applyOverride options.Capacity - // Increase expected capacity by the percentage to evict, since we want to not resize the dictionary. - let headroom = int (float options.Capacity * float options.HeadroomPercentage / 100.0) + let totalCapacity = applyOverride options.TotalCapacity + // Determine evictable headroom as the percentage of total capcity, since we want to not resize the dictionary. + let headroom = + int (float options.TotalCapacity * float options.HeadroomPercentage / 100.0) let cts = new CancellationTokenSource() - let cache = new Cache<'Key, 'Value>(capacity, headroom, cts, name) + let cache = new Cache<'Key, 'Value>(totalCapacity, headroom, cts, name) CacheMetrics.AddInstrumentation cache.Name |> ignore cache diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index 1655f27cba7..0fa0a5b1230 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -1,11 +1,12 @@ -namespace FSharp.Compiler +namespace FSharp.Compiler.Caches open System +open System.Diagnostics.Metrics open System.Threading [] type internal CacheOptions = - { Capacity: int + { TotalCapacity: int HeadroomPercentage: int } static member Default: CacheOptions @@ -19,7 +20,7 @@ type internal CachedEntity<'Key, 'Value> = [] type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = - new: capacity: int * headroom: int * cts: CancellationTokenSource * ?name: string -> Cache<'Key, 'Value> + new: totalCapacity: int * headroom: int * cts: CancellationTokenSource * ?name: string -> Cache<'Key, 'Value> member TryGetValue: key: 'Key * value: outref<'Value> -> bool member TryAdd: key: 'Key * value: 'Value -> bool member Dispose: unit -> unit @@ -31,6 +32,7 @@ type internal CacheMetrics = member CacheId: string member RecentStats: string member TryUpdateStats: clearCounts: bool -> bool + static member Meter: Meter static member GetStats: cacheId: string -> string static member GetStatsUpdateForAllCaches: clearCounts: bool -> string static member AddInstrumentation: cacheId: string -> unit @@ -38,4 +40,6 @@ type internal CacheMetrics = module internal Cache = val OverrideCapacityForTesting: unit -> unit - val Create<'Key, 'Value when 'Key: not null and 'Key: equality> : name: string * options: CacheOptions -> Cache<'Key, 'Value> + + val Create<'Key, 'Value when 'Key: not null and 'Key: equality> : + name: string * options: CacheOptions -> Cache<'Key, 'Value> diff --git a/tests/FSharp.Test.Utilities/XunitHelpers.fs b/tests/FSharp.Test.Utilities/XunitHelpers.fs index 8502cd98f06..50deb42ea68 100644 --- a/tests/FSharp.Test.Utilities/XunitHelpers.fs +++ b/tests/FSharp.Test.Utilities/XunitHelpers.fs @@ -10,6 +10,7 @@ open Xunit.Abstractions open TestFramework +open FSharp.Compiler.Caches open FSharp.Compiler.Diagnostics open OpenTelemetry.Resources @@ -155,7 +156,7 @@ type OpenTelemetryExport(testRunName, enable) = // Configure OpenTelemetry metrics export. Metrics can be viewed in Prometheus or other compatible tools. OpenTelemetry.Sdk.CreateMeterProviderBuilder() - .AddMeter(nameof FSharp.Compiler.CacheMetrics) + .AddMeter(CacheMetrics.Meter.Name) .AddMeter("System.Runtime") .ConfigureResource(fun r -> r.AddService(testRunName) |> ignore) .AddOtlpExporter(fun e m -> @@ -187,7 +188,7 @@ type FSharpXunitFramework(sink: IMessageSink) = #endif // Override cache capacity to reduce memory usage in CI. - FSharp.Compiler.Cache.OverrideCapacityForTesting() + Cache.OverrideCapacityForTesting() let testRunName = $"RunTests_{assemblyName.Name} {Runtime.InteropServices.RuntimeInformation.FrameworkDescription}" diff --git a/vsintegration/src/FSharp.Editor/Common/Logging.fs b/vsintegration/src/FSharp.Editor/Common/Logging.fs index 44ba9ee26ee..de96d46e254 100644 --- a/vsintegration/src/FSharp.Editor/Common/Logging.fs +++ b/vsintegration/src/FSharp.Editor/Common/Logging.fs @@ -121,6 +121,7 @@ module Logging = logErrorf "Context: %s\nException Message: %s\nStack Trace: %s" context ex.Message ex.StackTrace module FSharpServiceTelemetry = + open FSharp.Compiler.Caches let listen filter = let indent (activity: Activity) = @@ -154,8 +155,7 @@ module FSharpServiceTelemetry = let timer = new System.Timers.Timer(1000.0, AutoReset = true) timer.Elapsed.Add(fun _ -> - let stats = - FSharp.Compiler.CacheMetrics.GetStatsUpdateForAllCaches(clearCounts = true) + let stats = CacheMetrics.GetStatsUpdateForAllCaches(clearCounts = true) if stats <> "" then logMsg $"\n{stats}") @@ -176,7 +176,7 @@ module FSharpServiceTelemetry = OpenTelemetry.Sdk .CreateMeterProviderBuilder() .ConfigureResource(fun r -> r.AddService("F#") |> ignore) - .AddMeter(nameof FSharp.Compiler.CacheMetrics) + .AddMeter(CacheMetrics.Meter.Name) .AddMeter("System.Runtime") .AddOtlpExporter(fun e m -> e.Endpoint <- otlpEndpoint From 8b2b986cdc27dad446a3895e2a51f405b96b5756 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:07:43 +0200 Subject: [PATCH 21/28] basic tests --- src/Compiler/Checking/import.fs | 2 +- src/Compiler/Utilities/Caches.fs | 55 ++++++++++++------- src/Compiler/Utilities/Caches.fsi | 37 ++++++------- .../CompilerService/Caches.fs | 48 ++++++++++++++++ .../FSharp.Compiler.ComponentTests.fsproj | 1 + 5 files changed, 101 insertions(+), 42 deletions(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs diff --git a/src/Compiler/Checking/import.fs b/src/Compiler/Checking/import.fs index ac64cada5a9..f1f44090f19 100644 --- a/src/Compiler/Checking/import.fs +++ b/src/Compiler/Checking/import.fs @@ -92,7 +92,7 @@ type TTypeCacheKey = let typeSubsumptionCache = // Leave most of the capacity in reserve for bursts. - lazy Cache.Create("TypeSubsumptionCache", { TotalCapacity = 131072; HeadroomPercentage = 75 }) + lazy Cache.Create({ TotalCapacity = 131072; HeadroomPercentage = 75 }, name = "TypeSubsumptionCache") //------------------------------------------------------------------------- // Import an IL types as F# types. diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 18561b7c5d0..6b11c03e931 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -128,6 +128,9 @@ type CacheMetrics(cacheId) = |> String.concat "\n" static member AddInstrumentation(cacheId) = + if instrumentedCaches.ContainsKey cacheId then + invalidArg "cacheId" "cache with that name already exists" + instrumentedCaches[cacheId] <- new CacheMetrics(cacheId) static member RemoveInstrumentation(cacheId) = @@ -154,6 +157,22 @@ type EntityPool<'Key, 'Value>(totalCapacity, cacheId) = if pool.Count < totalCapacity then pool.Add(entity) +module Cache = + + // During testing a lot of compilations are started in app domains and subprocesses. + // This is a reliable way to pass the override to all of them. + [] + let private overrideVariable = "FSHARP_CACHE_OVERRIDE" + + /// Use for testing purposes to reduce memory consumption in testhost and its subprocesses. + let OverrideCapacityForTesting () = + Environment.SetEnvironmentVariable(overrideVariable, "true", EnvironmentVariableTarget.Process) + + let applyOverride (capacity: int) = + match Environment.GetEnvironmentVariable(overrideVariable) with + | NonNull _ when capacity > 1024 -> 1024 + | _ -> capacity + [] [] type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalCapacity, headroom, cts: CancellationTokenSource, ?name) = @@ -207,6 +226,14 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC // Non-evictable capacity. let capacity = totalCapacity - headroom + let backgroundEvictionComplete = Event<_>() + + let evictItems () = + while store.Count > capacity - headroom && evictionQueue.Count > 0 do + tryEvictOne () + + backgroundEvictionComplete.Trigger() + let rec backgroundEviction () = async { let utilization = (float store.Count / float totalCapacity) @@ -220,14 +247,16 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC if delay > 0.0 then do! Async.Sleep(int delay) - while store.Count > capacity - headroom && evictionQueue.Count > 0 do - tryEvictOne () + if store.Count > capacity then + evictItems () return! backgroundEviction () } do Async.Start(backgroundEviction (), cancellationToken = cts.Token) + member val BackgroundEvictionComplete = backgroundEvictionComplete.Publish + member val Name = instanceId member _.TryGetValue(key: 'Key, value: outref<'Value>) = @@ -263,35 +292,19 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC member this.GetStats() = CacheMetrics.GetStats(this.Name) -module Cache = - - // During testing a lot of compilations are started in app domains and subprocesses. - // This is a reliable way to pass the override to all of them. - [] - let private overrideVariable = "FSHARP_CACHE_OVERRIDE" - - /// Use for testing purposes to reduce memory consumption in testhost and its subprocesses. - let OverrideCapacityForTesting () = - Environment.SetEnvironmentVariable(overrideVariable, "true", EnvironmentVariableTarget.Process) - - let applyOverride (capacity: int) = - match Environment.GetEnvironmentVariable(overrideVariable) with - | NonNull _ when capacity > 1024 -> 1024 - | _ -> capacity - - let Create<'Key, 'Value when 'Key: not null and 'Key: equality> (name, options: CacheOptions) = + static member Create<'Key, 'Value>(options: CacheOptions, ?name) = if options.TotalCapacity < 0 then invalidArg "Capacity" "Capacity must be positive" if options.HeadroomPercentage < 0 then invalidArg "HeadroomPercentage" "HeadroomPercentage must be positive" - let totalCapacity = applyOverride options.TotalCapacity + let totalCapacity = Cache.applyOverride options.TotalCapacity // Determine evictable headroom as the percentage of total capcity, since we want to not resize the dictionary. let headroom = int (float options.TotalCapacity * float options.HeadroomPercentage / 100.0) let cts = new CancellationTokenSource() - let cache = new Cache<'Key, 'Value>(totalCapacity, headroom, cts, name) + let cache = new Cache<_, _>(totalCapacity, headroom, cts, ?name = name) CacheMetrics.AddInstrumentation cache.Name |> ignore cache diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index 0fa0a5b1230..79d61ca5a1d 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -6,40 +6,37 @@ open System.Threading [] type internal CacheOptions = - { TotalCapacity: int - HeadroomPercentage: int } + { + /// Total capacity, determines the size of the underlying store. + TotalCapacity: int + + /// Safety margin size as a percentage of TotalCapacity. + HeadroomPercentage: int + } static member Default: CacheOptions -[] -type internal CachedEntity<'Key, 'Value> = - new: key: 'Key * value: 'Value -> CachedEntity<'Key, 'Value> - member WithNode: unit -> CachedEntity<'Key, 'Value> - member ReUse: key: 'Key * value: 'Value -> CachedEntity<'Key, 'Value> - override ToString: unit -> string +module internal Cache = + val OverrideCapacityForTesting: unit -> unit [] type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = new: totalCapacity: int * headroom: int * cts: CancellationTokenSource * ?name: string -> Cache<'Key, 'Value> member TryGetValue: key: 'Key * value: outref<'Value> -> bool member TryAdd: key: 'Key * value: 'Value -> bool + /// Cancels the background eviction task. member Dispose: unit -> unit interface IDisposable + /// For testing only + member BackgroundEvictionComplete: IEvent + + static member Create<'Key, 'Value> : options: CacheOptions * ?name: string -> Cache<'Key, 'Value> + +[] type internal CacheMetrics = - new: cacheId: string -> CacheMetrics - member CacheId: string - member RecentStats: string - member TryUpdateStats: clearCounts: bool -> bool static member Meter: Meter static member GetStats: cacheId: string -> string + /// Retrieves current hit ratio, hits, misses, evictions etc. formatted for printing or logging. static member GetStatsUpdateForAllCaches: clearCounts: bool -> string - static member AddInstrumentation: cacheId: string -> unit - static member RemoveInstrumentation: cacheId: string -> unit - -module internal Cache = - val OverrideCapacityForTesting: unit -> unit - - val Create<'Key, 'Value when 'Key: not null and 'Key: equality> : - name: string * options: CacheOptions -> Cache<'Key, 'Value> diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs b/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs new file mode 100644 index 00000000000..31c2120c8ed --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs @@ -0,0 +1,48 @@ +module CompilerService.Caches + +open FSharp.Compiler.Caches + +open System.Threading +open Xunit + +[] +let ``Basic add and retrieve`` () = + use cache = Cache.Create(CacheOptions.Default) + + cache.TryAdd("key1", 1) |> ignore + cache.TryAdd("key2", 2) |> ignore + + let mutable value = 0 + Assert.True(cache.TryGetValue("key1", &value), "Should retrieve key1") + Assert.Equal(1, value) + Assert.True(cache.TryGetValue("key2", &value), "Should retrieve key2") + Assert.Equal(2, value) + Assert.False(cache.TryGetValue("key3", &value), "Should not retrieve non-existent key3") + + cache.Dispose() + +[] +let ``Eviction of least recently used`` () = + use cache = Cache.Create({ TotalCapacity = 2; HeadroomPercentage = 0 }) + + cache.TryAdd("key1", 1) |> ignore + cache.TryAdd("key2", 2) |> ignore + + // Make key1 recently used by accessing it + let mutable value = 0 + cache.TryGetValue("key1", &value) |> ignore + + let evictionComplete = new ManualResetEvent(false) + cache.BackgroundEvictionComplete.Add(fun _ -> evictionComplete.Set() |> ignore) + + // Add a third item, which should schedule key2 for eviction + cache.TryAdd("key3", 3) |> ignore + + // Wait for eviction to complete using the event + evictionComplete.WaitOne() |> ignore + + Assert.False(cache.TryGetValue("key2", &value), "key2 should have been evicted") + Assert.True(cache.TryGetValue("key1", &value), "key1 should still be in cache") + Assert.Equal(1, value) + Assert.True(cache.TryGetValue("key3", &value), "key3 should be in cache") + Assert.Equal(3, value) diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 49b459e2796..1aad3e73fe1 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -300,6 +300,7 @@ + From bf17afac63f98ae3023014018f3d9103e906de67 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:16:07 +0200 Subject: [PATCH 22/28] rn --- docs/release-notes/.FSharp.Compiler.Service/9.0.300.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md b/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md index 16f5721e22a..7f5b327a7a9 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md +++ b/docs/release-notes/.FSharp.Compiler.Service/9.0.300.md @@ -37,6 +37,7 @@ * Type parameter constraint `null` in generic code will now automatically imply `not struct` ([Issue #18320](https://github.com/dotnet/fsharp/issues/18320), [PR #18323](https://github.com/dotnet/fsharp/pull/18323)) * Add a switch to determine whether to generate a default implementation body for overridden method when completing. [PR #18341](https://github.com/dotnet/fsharp/pull/18341) * Use a more accurate range for CE Combine methods. [PR #18394](https://github.com/dotnet/fsharp/pull/18394) +* Enable TypeSubsumptionCache for IDE use. [PR #18499](https://github.com/dotnet/fsharp/pull/18499) ### Changed From d49e2767fd3d4b1ab5f374458c343ac068c61c93 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 28 Apr 2025 16:28:36 +0200 Subject: [PATCH 23/28] fix tests --- src/Compiler/Utilities/Caches.fs | 35 +++++++------ src/Compiler/Utilities/Caches.fsi | 8 ++- .../CompilerService/Caches.fs | 50 +++++++++++++++++-- 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 6b11c03e931..6648a4a15c5 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -20,8 +20,8 @@ type CacheOptions = static member Default = { - TotalCapacity = 1024 - HeadroomPercentage = 10 + TotalCapacity = 128 + HeadroomPercentage = 50 } [] @@ -62,7 +62,6 @@ type CacheMetrics(cacheId) = let readings = ConcurrentDictionary() -#if DEBUG let listener = new MeterListener() do @@ -75,9 +74,6 @@ type CacheMetrics(cacheId) = listener.Start() member this.Dispose() = listener.Dispose() -#else - member this.Dispose() = () -#endif member val CacheId = cacheId @@ -115,6 +111,7 @@ type CacheMetrics(cacheId) = else false + // TODO: Should return a Map, not a string static member GetStats(cacheId) = instrumentedCaches[cacheId].TryUpdateStats(false) |> ignore instrumentedCaches[cacheId].RecentStats @@ -129,7 +126,7 @@ type CacheMetrics(cacheId) = static member AddInstrumentation(cacheId) = if instrumentedCaches.ContainsKey cacheId then - invalidArg "cacheId" "cache with that name already exists" + invalidArg "cacheId" $"cache with name {cacheId} already exists" instrumentedCaches[cacheId] <- new CacheMetrics(cacheId) @@ -158,7 +155,6 @@ type EntityPool<'Key, 'Value>(totalCapacity, cacheId) = pool.Add(entity) module Cache = - // During testing a lot of compilations are started in app domains and subprocesses. // This is a reliable way to pass the override to all of them. [] @@ -175,11 +171,16 @@ module Cache = [] [] -type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalCapacity, headroom, cts: CancellationTokenSource, ?name) = +type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> + internal (totalCapacity, headroom, cts: CancellationTokenSource, ?name, ?observeMetrics) = + + let instanceId = defaultArg name (Guid.NewGuid().ToString()) - static let mutable cacheId = 0 + let observeMetrics = defaultArg observeMetrics false - let instanceId = defaultArg name $"cache-{Interlocked.Increment(&cacheId)}" + do + if observeMetrics then + CacheMetrics.AddInstrumentation instanceId let meter = CacheMetrics.Meter let hits = meter.CreateCounter("hits", "count", instanceId) @@ -286,13 +287,15 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC member this.Dispose() = store.Clear() cts.Cancel() - CacheMetrics.RemoveInstrumentation(this.Name) + + if observeMetrics then + CacheMetrics.RemoveInstrumentation instanceId member this.Dispose() = (this :> IDisposable).Dispose() member this.GetStats() = CacheMetrics.GetStats(this.Name) - static member Create<'Key, 'Value>(options: CacheOptions, ?name) = + static member Create<'Key, 'Value>(options: CacheOptions, ?name, ?observeMetrics) = if options.TotalCapacity < 0 then invalidArg "Capacity" "Capacity must be positive" @@ -305,6 +308,8 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC int (float options.TotalCapacity * float options.HeadroomPercentage / 100.0) let cts = new CancellationTokenSource() - let cache = new Cache<_, _>(totalCapacity, headroom, cts, ?name = name) - CacheMetrics.AddInstrumentation cache.Name |> ignore + + let cache = + new Cache<_, _>(totalCapacity, headroom, cts, ?name = name, ?observeMetrics = observeMetrics) + cache diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index 79d61ca5a1d..620beec349c 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -21,7 +21,10 @@ module internal Cache = [] type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = - new: totalCapacity: int * headroom: int * cts: CancellationTokenSource * ?name: string -> Cache<'Key, 'Value> + new: + totalCapacity: int * headroom: int * cts: CancellationTokenSource * ?name: string * ?observeMetrics: bool -> + Cache<'Key, 'Value> + member TryGetValue: key: 'Key * value: outref<'Value> -> bool member TryAdd: key: 'Key * value: 'Value -> bool /// Cancels the background eviction task. @@ -32,7 +35,8 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = /// For testing only member BackgroundEvictionComplete: IEvent - static member Create<'Key, 'Value> : options: CacheOptions * ?name: string -> Cache<'Key, 'Value> + static member Create<'Key, 'Value> : + options: CacheOptions * ?name: string * ?observeMetrics: bool -> Cache<'Key, 'Value> [] type internal CacheMetrics = diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs b/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs index 31c2120c8ed..aaa213e23e5 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs @@ -5,9 +5,29 @@ open FSharp.Compiler.Caches open System.Threading open Xunit +[] +let ``Create and dispose many`` () = + let caches = + [ + for _ in 1 .. 100 do + Cache.Create(CacheOptions.Default, observeMetrics = true) + ] + + for c in caches do c.Dispose() + +[] +let ``Create and dispose many named`` () = + let caches = + [ + for i in 1 .. 100 do + Cache.Create(CacheOptions.Default, name = $"testCache{i}", observeMetrics = true) + ] + + for c in caches do c.Dispose() + [] let ``Basic add and retrieve`` () = - use cache = Cache.Create(CacheOptions.Default) + use cache = Cache.Create(CacheOptions.Default, observeMetrics = true) cache.TryAdd("key1", 1) |> ignore cache.TryAdd("key2", 2) |> ignore @@ -18,12 +38,10 @@ let ``Basic add and retrieve`` () = Assert.True(cache.TryGetValue("key2", &value), "Should retrieve key2") Assert.Equal(2, value) Assert.False(cache.TryGetValue("key3", &value), "Should not retrieve non-existent key3") - - cache.Dispose() [] let ``Eviction of least recently used`` () = - use cache = Cache.Create({ TotalCapacity = 2; HeadroomPercentage = 0 }) + use cache = Cache.Create({ TotalCapacity = 2; HeadroomPercentage = 0 }, observeMetrics = true) cache.TryAdd("key1", 1) |> ignore cache.TryAdd("key2", 2) |> ignore @@ -46,3 +64,27 @@ let ``Eviction of least recently used`` () = Assert.Equal(1, value) Assert.True(cache.TryGetValue("key3", &value), "key3 should be in cache") Assert.Equal(3, value) + +[] +let ``Metrics can be retrieved`` () = + use cache = Cache.Create({ TotalCapacity = 2; HeadroomPercentage = 0 }, name = "test_metrics", observeMetrics = true) + + cache.TryAdd("key1", 1) |> ignore + cache.TryAdd("key2", 2) |> ignore + + // Make key1 recently used by accessing it + let mutable value = 0 + cache.TryGetValue("key1", &value) |> ignore + + let evictionComplete = new ManualResetEvent(false) + cache.BackgroundEvictionComplete.Add(fun _ -> evictionComplete.Set() |> ignore) + + // Add a third item, which should schedule key2 for eviction + cache.TryAdd("key3", 3) |> ignore + + // Wait for eviction to complete using the event + evictionComplete.WaitOne() |> ignore + + let metrics = CacheMetrics.GetStats "test_metrics" + + Assert.Contains("test_metrics | hit ratio", metrics) From aa76a84598b1e13f66aa8ab8255c3c7b7c27b3ae Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:03:59 +0200 Subject: [PATCH 24/28] ilver --- src/Compiler/Utilities/Caches.fs | 16 ++++++++-------- ...rify_FSharp.Compiler.Service_Debug_net9.0.bsl | 12 ++++++------ ...arp.Compiler.Service_Debug_netstandard2.0.bsl | 12 ++++++------ ...fy_FSharp.Compiler.Service_Release_net9.0.bsl | 12 ++++++------ ...p.Compiler.Service_Release_netstandard2.0.bsl | 12 ++++++------ 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 6648a4a15c5..af94f14fca2 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -58,7 +58,7 @@ type CachedEntity<'Key, 'Value> = type CacheMetrics(cacheId) = static let meter = new Meter("FSharp.Compiler.Cache") - static let instrumentedCaches = ConcurrentDictionary() + static let observedCaches = ConcurrentDictionary() let readings = ConcurrentDictionary() @@ -113,26 +113,26 @@ type CacheMetrics(cacheId) = // TODO: Should return a Map, not a string static member GetStats(cacheId) = - instrumentedCaches[cacheId].TryUpdateStats(false) |> ignore - instrumentedCaches[cacheId].RecentStats + observedCaches[cacheId].TryUpdateStats(false) |> ignore + observedCaches[cacheId].RecentStats static member GetStatsUpdateForAllCaches(clearCounts) = [ - for i in instrumentedCaches.Values do + for i in observedCaches.Values do if i.TryUpdateStats(clearCounts) then i.RecentStats ] |> String.concat "\n" static member AddInstrumentation(cacheId) = - if instrumentedCaches.ContainsKey cacheId then + if observedCaches.ContainsKey cacheId then invalidArg "cacheId" $"cache with name {cacheId} already exists" - instrumentedCaches[cacheId] <- new CacheMetrics(cacheId) + observedCaches[cacheId] <- new CacheMetrics(cacheId) static member RemoveInstrumentation(cacheId) = - instrumentedCaches[cacheId].Dispose() - instrumentedCaches.TryRemove(cacheId) |> ignore + observedCaches[cacheId].Dispose() + observedCaches.TryRemove(cacheId) |> ignore type EntityPool<'Key, 'Value>(totalCapacity, cacheId) = let pool = ConcurrentBag>() 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 72270ed5f02..a7afde62fee 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl @@ -21,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-817::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-815::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-521::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-521::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-521::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-521::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-521::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-519::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-519::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-519::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-519::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-519::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 841c59db0d6..14f46fb9d5c 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl @@ -28,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-817::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-815::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-521::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-521::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-521::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-521::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-521::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-519::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-519::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-519::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-519::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-519::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 e120d20c4c0..23c81a0d0ff 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-861::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-859::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-542::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-542::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-542::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-542::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-542::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-540::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-540::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-540::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-540::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-540::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 cf89979c03f..b16e109ec20 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl @@ -28,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-861::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-859::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-542::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-542::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-542::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-542::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-542::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-540::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-540::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-540::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-540::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-540::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000076][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$Symbols+fullName@2495-3::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000030][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. From a182b367377a9ec747ca4b619e31380a342d5d2c Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:15:25 +0200 Subject: [PATCH 25/28] use MailboxProcessor for lock-free eviction handling --- src/Compiler/Utilities/Caches.fs | 118 +++++++++--------- src/Compiler/Utilities/Caches.fsi | 6 +- .../CompilerService/Caches.fs | 12 +- 3 files changed, 65 insertions(+), 71 deletions(-) diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index af94f14fca2..3bddd43599b 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -24,24 +24,28 @@ type CacheOptions = HeadroomPercentage = 50 } +// It is important that this is not a struct, because LinkedListNode holds a reference to it, +// and it holds the reference to that Node, in a circular way. [] [] type CachedEntity<'Key, 'Value> = val mutable Key: 'Key val mutable Value: 'Value val mutable AccessCount: int64 - val mutable Node: LinkedListNode> + val mutable Node: LinkedListNode> voption new(key, value) = { Key = key Value = value AccessCount = 0L - Node = Unchecked.defaultof<_> + Node = ValueNone } + // This is one time initialization, outside of the constructor because of circular reference. + // The contract is that each CachedEntity that the EntityPool produces, has Node assigned. member this.WithNode() = - this.Node <- LinkedListNode(this) + this.Node <- ValueSome(LinkedListNode this) this member this.ReUse(key, value) = @@ -134,6 +138,8 @@ type CacheMetrics(cacheId) = observedCaches[cacheId].Dispose() observedCaches.TryRemove(cacheId) |> ignore +// Creates and after reclaiming holds entities for reuse. +// More than totalCapacity can be created, but it will hold for reuse at most totalCapacity. type EntityPool<'Key, 'Value>(totalCapacity, cacheId) = let pool = ConcurrentBag>() let mutable created = 0 @@ -148,6 +154,8 @@ type EntityPool<'Key, 'Value>(totalCapacity, cacheId) = if Interlocked.Increment &created > totalCapacity then overCapacity.Add 1L + // Associate a LinkedListNode with freshly created entity. + // This is a one time initialization. CachedEntity(key, value).WithNode() member _.Reclaim(entity: CachedEntity<'Key, 'Value>) = @@ -169,10 +177,14 @@ module Cache = | NonNull _ when capacity > 1024 -> 1024 | _ -> capacity +[] +type EvictionQueueMessage<'Key, 'Value> = + | Add of CachedEntity<'Key, 'Value> + | Update of CachedEntity<'Key, 'Value> + [] [] -type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> - internal (totalCapacity, headroom, cts: CancellationTokenSource, ?name, ?observeMetrics) = +type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalCapacity, headroom, ?name, ?observeMetrics) = let instanceId = defaultArg name (Guid.NewGuid().ToString()) @@ -197,66 +209,53 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> let evictionQueue = LinkedList>() - let addToEvictionQueue (entity: CachedEntity<'Key, 'Value>) = - lock evictionQueue <| fun () -> evictionQueue.AddLast(entity.Node) - - // Only LRU currrently. We can add other strategies when needed. - let updateEvictionQueue (entity: CachedEntity<'Key, 'Value>) = - lock evictionQueue - <| fun () -> - - let node = entity.Node - - // Sync between store and the eviction queue is not atomic. It might be already evicted or not yet added. - if node.List = evictionQueue then - // Just move this node to the end of the list. - evictionQueue.Remove(node) - evictionQueue.AddLast(node) - - let tryEvictOne () = - match evictionQueue.First with - | null -> evictionFails.Add 1L - | first -> - match store.TryRemove(first.Value.Key) with - | true, removed -> - lock evictionQueue <| fun () -> evictionQueue.Remove(first) - pool.Reclaim(removed) - evictions.Add 1L - | _ -> evictionFails.Add 1L - // Non-evictable capacity. let capacity = totalCapacity - headroom - let backgroundEvictionComplete = Event<_>() + let evicted = Event<_>() - let evictItems () = - while store.Count > capacity - headroom && evictionQueue.Count > 0 do - tryEvictOne () + let evictionProcessor = + new MailboxProcessor>(fun mb -> + let rec processNext () = + async { + match! mb.Receive() with + | EvictionQueueMessage.Add entity -> - backgroundEvictionComplete.Trigger() + assert entity.Node.IsSome - let rec backgroundEviction () = - async { - let utilization = (float store.Count / float totalCapacity) - // 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) + evictionQueue.AddLast(entity.Node.Value) - if delay > 0.0 then - do! Async.Sleep(int delay) + // Evict one immediately if necessary. + if evictionQueue.Count > capacity then + let first = nonNull evictionQueue.First - if store.Count > capacity then - evictItems () + match store.TryRemove(first.Value.Key) with + | true, removed -> + evictionQueue.Remove(first) + pool.Reclaim(removed) + evictions.Add 1L + evicted.Trigger() + | _ -> evictionFails.Add 1L - return! backgroundEviction () - } + | EvictionQueueMessage.Update entity -> + entity.AccessCount <- entity.AccessCount + 1L + + assert entity.Node.IsSome - do Async.Start(backgroundEviction (), cancellationToken = cts.Token) + let node = entity.Node.Value + assert (node.List = evictionQueue) + // Just move this node to the end of the list. + evictionQueue.Remove(node) + evictionQueue.AddLast(node) - member val BackgroundEvictionComplete = backgroundEvictionComplete.Publish + do! processNext () + } + + processNext ()) + + do evictionProcessor.Start() + + member val Evicted = evicted.Publish member val Name = instanceId @@ -264,8 +263,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> match store.TryGetValue(key) with | true, cachedEntity -> hits.Add 1L - Interlocked.Increment(&cachedEntity.AccessCount) |> ignore - updateEvictionQueue cachedEntity + evictionProcessor.Post(EvictionQueueMessage.Update cachedEntity) value <- cachedEntity.Value true | _ -> @@ -277,7 +275,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> let cachedEntity = pool.Acquire(key, value) if store.TryAdd(key, cachedEntity) then - addToEvictionQueue cachedEntity + evictionProcessor.Post(EvictionQueueMessage.Add cachedEntity) true else pool.Reclaim(cachedEntity) @@ -285,8 +283,8 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> interface IDisposable with member this.Dispose() = + evictionProcessor.Dispose() store.Clear() - cts.Cancel() if observeMetrics then CacheMetrics.RemoveInstrumentation instanceId @@ -307,9 +305,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> let headroom = int (float options.TotalCapacity * float options.HeadroomPercentage / 100.0) - let cts = new CancellationTokenSource() - let cache = - new Cache<_, _>(totalCapacity, headroom, cts, ?name = name, ?observeMetrics = observeMetrics) + new Cache<_, _>(totalCapacity, headroom, ?name = name, ?observeMetrics = observeMetrics) cache diff --git a/src/Compiler/Utilities/Caches.fsi b/src/Compiler/Utilities/Caches.fsi index 620beec349c..565342bf7f5 100644 --- a/src/Compiler/Utilities/Caches.fsi +++ b/src/Compiler/Utilities/Caches.fsi @@ -21,9 +21,7 @@ module internal Cache = [] type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = - new: - totalCapacity: int * headroom: int * cts: CancellationTokenSource * ?name: string * ?observeMetrics: bool -> - Cache<'Key, 'Value> + new: totalCapacity: int * headroom: int * ?name: string * ?observeMetrics: bool -> Cache<'Key, 'Value> member TryGetValue: key: 'Key * value: outref<'Value> -> bool member TryAdd: key: 'Key * value: 'Value -> bool @@ -33,7 +31,7 @@ type internal Cache<'Key, 'Value when 'Key: not null and 'Key: equality> = interface IDisposable /// For testing only - member BackgroundEvictionComplete: IEvent + member Evicted: IEvent static member Create<'Key, 'Value> : options: CacheOptions * ?name: string * ?observeMetrics: bool -> Cache<'Key, 'Value> diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs b/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs index aaa213e23e5..3849cd9addd 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerService/Caches.fs @@ -50,14 +50,14 @@ let ``Eviction of least recently used`` () = let mutable value = 0 cache.TryGetValue("key1", &value) |> ignore - let evictionComplete = new ManualResetEvent(false) - cache.BackgroundEvictionComplete.Add(fun _ -> evictionComplete.Set() |> ignore) + let evicted = new ManualResetEvent(false) + cache.Evicted.Add(fun _ -> evicted.Set() |> ignore) // Add a third item, which should schedule key2 for eviction cache.TryAdd("key3", 3) |> ignore // Wait for eviction to complete using the event - evictionComplete.WaitOne() |> ignore + evicted.WaitOne() |> ignore Assert.False(cache.TryGetValue("key2", &value), "key2 should have been evicted") Assert.True(cache.TryGetValue("key1", &value), "key1 should still be in cache") @@ -76,14 +76,14 @@ let ``Metrics can be retrieved`` () = let mutable value = 0 cache.TryGetValue("key1", &value) |> ignore - let evictionComplete = new ManualResetEvent(false) - cache.BackgroundEvictionComplete.Add(fun _ -> evictionComplete.Set() |> ignore) + let evicted = new ManualResetEvent(false) + cache.Evicted.Add(fun _ -> evicted.Set() |> ignore) // Add a third item, which should schedule key2 for eviction cache.TryAdd("key3", 3) |> ignore // Wait for eviction to complete using the event - evictionComplete.WaitOne() |> ignore + evicted.WaitOne() |> ignore let metrics = CacheMetrics.GetStats "test_metrics" From d6b3ac9cbfb0199f8e87cdcceac2dfe3ab3a8982 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:58:19 +0200 Subject: [PATCH 26/28] ilverify --- ...ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl | 12 ++++++------ ..._FSharp.Compiler.Service_Debug_netstandard2.0.bsl | 12 ++++++------ ...verify_FSharp.Compiler.Service_Release_net9.0.bsl | 12 ++++++------ ...Sharp.Compiler.Service_Release_netstandard2.0.bsl | 12 ++++++------ 4 files changed, 24 insertions(+), 24 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 a7afde62fee..69842b9e059 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl @@ -21,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-815::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-805::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-519::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-519::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-519::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-519::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-519::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-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]: : 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 14f46fb9d5c..6e41547cd11 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl @@ -28,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-815::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-805::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-519::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-519::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-519::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-519::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-519::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-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]: : .$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 23c81a0d0ff..4e7b5396676 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-859::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-849::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-540::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-540::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-540::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-540::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-540::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-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]: : 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 b16e109ec20..431d4e5512a 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl @@ -28,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-859::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-849::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-540::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-540::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-540::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-540::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-540::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-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]: : .$Symbols+fullName@2495-3::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000030][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. From 523fea32461b8b59cfe1cfab2ed5f9d4c300a453 Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:12:15 +0200 Subject: [PATCH 27/28] return --- src/Compiler/Utilities/Caches.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 3bddd43599b..8e82726fe0c 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -248,7 +248,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC evictionQueue.Remove(node) evictionQueue.AddLast(node) - do! processNext () + return! processNext () } processNext ()) From 9c70966c3fd4d7034f287ff34e67fb8dd169550e Mon Sep 17 00:00:00 2001 From: Jakub Majocha <1760221+majocha@users.noreply.github.com> Date: Tue, 29 Apr 2025 21:15:45 +0200 Subject: [PATCH 28/28] restore cts --- src/Compiler/Utilities/Caches.fs | 67 +++++++++++++++++--------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/src/Compiler/Utilities/Caches.fs b/src/Compiler/Utilities/Caches.fs index 8e82726fe0c..04829d4d1f3 100644 --- a/src/Compiler/Utilities/Caches.fs +++ b/src/Compiler/Utilities/Caches.fs @@ -174,7 +174,7 @@ module Cache = let applyOverride (capacity: int) = match Environment.GetEnvironmentVariable(overrideVariable) with - | NonNull _ when capacity > 1024 -> 1024 + | NonNull _ when capacity > 4096 -> 4096 | _ -> capacity [] @@ -214,46 +214,49 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC let evicted = Event<_>() - let evictionProcessor = - new MailboxProcessor>(fun mb -> - let rec processNext () = - async { - match! mb.Receive() with - | EvictionQueueMessage.Add entity -> + let cts = new CancellationTokenSource() - assert entity.Node.IsSome + let evictionProcessor = + MailboxProcessor.Start( + (fun mb -> + let rec processNext () = + async { + match! mb.Receive() with + | EvictionQueueMessage.Add entity -> - evictionQueue.AddLast(entity.Node.Value) + assert entity.Node.IsSome - // Evict one immediately if necessary. - if evictionQueue.Count > capacity then - let first = nonNull evictionQueue.First + evictionQueue.AddLast(entity.Node.Value) - match store.TryRemove(first.Value.Key) with - | true, removed -> - evictionQueue.Remove(first) - pool.Reclaim(removed) - evictions.Add 1L - evicted.Trigger() - | _ -> evictionFails.Add 1L + // Evict one immediately if necessary. + while evictionQueue.Count > capacity do + let first = nonNull evictionQueue.First - | EvictionQueueMessage.Update entity -> - entity.AccessCount <- entity.AccessCount + 1L + match store.TryRemove(first.Value.Key) with + | true, removed -> + evictionQueue.Remove(first) + pool.Reclaim(removed) + evictions.Add 1L + evicted.Trigger() + | _ -> evictionFails.Add 1L - assert entity.Node.IsSome + | EvictionQueueMessage.Update entity -> + entity.AccessCount <- entity.AccessCount + 1L - let node = entity.Node.Value - assert (node.List = evictionQueue) - // Just move this node to the end of the list. - evictionQueue.Remove(node) - evictionQueue.AddLast(node) + assert entity.Node.IsSome - return! processNext () - } + let node = entity.Node.Value + assert (node.List = evictionQueue) + // Just move this node to the end of the list. + evictionQueue.Remove(node) + evictionQueue.AddLast(node) - processNext ()) + return! processNext () + } - do evictionProcessor.Start() + processNext ()), + cts.Token + ) member val Evicted = evicted.Publish @@ -283,6 +286,8 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC interface IDisposable with member this.Dispose() = + cts.Cancel() + cts.Dispose() evictionProcessor.Dispose() store.Clear()