From 05d8e75698f5c1524e187d0182ae3d069ecb2a9d Mon Sep 17 00:00:00 2001
From: Onur Gumus <onur@outlook.com.tr>
Date: Sat, 28 Feb 2026 09:38:56 +0100
Subject: [PATCH 1/2] Cache hash code in TypeStructure to fix O(n) GetHashCode
 regression

TypeStructure.GetHashCode was calling GenericHashArbArray on the
underlying TypeToken[] on every cache lookup, making each hash O(n).
In IDE mode with generative type providers this caused sustained ~150%
CPU as the type subsumption cache continuously rehashed all entries.

Pre-compute the hash when creating TypeStructure and store it in the
DU case. Add [<CustomEquality; NoComparison>] with a fast-path Equals
that checks the cached hash before comparing arrays.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 .../Checking/OverloadResolutionCache.fs       | 10 +++---
 src/Compiler/Utilities/TypeHashing.fs         | 34 ++++++++++++++++---
 2 files changed, 35 insertions(+), 9 deletions(-)

diff --git a/src/Compiler/Checking/OverloadResolutionCache.fs b/src/Compiler/Checking/OverloadResolutionCache.fs
index cbf6b1aca03..995b1e5e211 100644
--- a/src/Compiler/Checking/OverloadResolutionCache.fs
+++ b/src/Compiler/Checking/OverloadResolutionCache.fs
@@ -83,12 +83,12 @@ let tryGetTypeStructureForOverloadCache (g: TcGlobals) (ty: TType) : TypeStructu
     let ty = stripTyEqns g ty
 
     match tryGetTypeStructureOfStrippedType ty with
-    | ValueSome(Stable tokens) -> ValueSome(Stable tokens)
-    | ValueSome(Unstable tokens) ->
+    | ValueSome(Stable(h, tokens)) -> ValueSome(Stable(h, tokens))
+    | ValueSome(Unstable(h, tokens)) ->
         if hasUnsolvedTokens tokens then
             ValueNone
         else
-            ValueSome(Stable tokens)
+            ValueSome(Stable(h, tokens))
     | ValueSome PossiblyInfinite -> ValueNone
     | ValueNone -> ValueNone
 
@@ -162,8 +162,8 @@ let tryComputeOverloadCacheKey
                     | Some retTy ->
                         match tryGetTypeStructureForOverloadCache g retTy with
                         | ValueSome ts -> ValueSome ts
-                        | ValueNone -> if anyHasOutArgs then ValueNone else ValueSome(Stable [||])
-                    | None -> ValueSome(Stable [||])
+                        | ValueNone -> if anyHasOutArgs then ValueNone else ValueSome(Stable(0, [||]))
+                    | None -> ValueSome(Stable(0, [||]))
 
                 match retTyStructure with
                 | ValueNone -> ValueNone
diff --git a/src/Compiler/Utilities/TypeHashing.fs b/src/Compiler/Utilities/TypeHashing.fs
index 7723e8b04df..ff4bab0a403 100644
--- a/src/Compiler/Utilities/TypeHashing.fs
+++ b/src/Compiler/Utilities/TypeHashing.fs
@@ -407,12 +407,35 @@ module StructuralUtilities =
         | Unsolved of int
         | Rigid of int
 
+    let private hashTokenArray (tokens: TypeToken[]) =
+        let mutable acc = 0
+        for t in tokens do
+            acc <- combineHash acc (hash t)
+        acc
+
+    [<CustomEquality; NoComparison>]
     type TypeStructure =
-        | Stable of TypeToken[]
+        | Stable of hash: int * tokens: TypeToken[]
         // Unstable means that the type structure of a given TType may change because of constraint solving or Trace.Undo.
-        | Unstable of TypeToken[]
+        | Unstable of hash: int * tokens: TypeToken[]
         | PossiblyInfinite
 
+        override this.GetHashCode() =
+            match this with
+            | Stable(h, _) -> h
+            | Unstable(h, _) -> h ||| 0x40000000
+            | PossiblyInfinite -> 0
+
+        override this.Equals(obj) =
+            match obj with
+            | :? TypeStructure as other ->
+                match this, other with
+                | Stable(h1, a), Stable(h2, b) -> h1 = h2 && a = b
+                | Unstable(h1, a), Unstable(h2, b) -> h1 = h2 && a = b
+                | PossiblyInfinite, PossiblyInfinite -> true
+                | _ -> false
+            | _ -> false
+
     type private GenerationContext() =
         member val TyparMap = System.Collections.Generic.Dictionary<Stamp, int>(4)
         member val Tokens = ResizeArray<TypeToken>(MaxTokenCount)
@@ -566,8 +589,11 @@ module StructuralUtilities =
 
         // If the sequence got too long, just drop it, we could be dealing with an infinite type.
         if out.Count >= MaxTokenCount then PossiblyInfinite
-        elif not ctx.Stable then Unstable(out.ToArray())
-        else Stable(out.ToArray())
+        else
+            let tokens = out.ToArray()
+            let h = hashTokenArray tokens
+            if not ctx.Stable then Unstable(h, tokens)
+            else Stable(h, tokens)
 
     // Speed up repeated calls by memoizing results for types that yield a stable structure.
     let private getTypeStructureOfStrippedType =

From f95aa0dac747cd4b28ee4aee966723767456ace0 Mon Sep 17 00:00:00 2001
From: Tomas Grosup <tomasgrosup@microsoft.com>
Date: Mon, 13 Apr 2026 13:25:06 +0200
Subject: [PATCH 2/2] Apply fantomas formatting and add release notes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../.FSharp.Compiler.Service/11.0.100.md             |  9 +++++++++
 src/Compiler/Checking/OverloadResolutionCache.fs     |  6 +++++-
 src/Compiler/Utilities/TypeHashing.fs                | 12 +++++++++---
 3 files changed, 23 insertions(+), 4 deletions(-)
 create mode 100644 docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
new file mode 100644
index 00000000000..589cb3bcb3e
--- /dev/null
+++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
@@ -0,0 +1,9 @@
+### Fixed
+
+* Fix O(n) `TypeStructure.GetHashCode` performance regression causing sustained high CPU in IDE mode with generative type providers. ([Issue #18925](https://github.com/dotnet/fsharp/issues/18925), [PR #19369](https://github.com/dotnet/fsharp/pull/19369))
+
+### Added
+
+### Changed
+
+### Breaking Changes
diff --git a/src/Compiler/Checking/OverloadResolutionCache.fs b/src/Compiler/Checking/OverloadResolutionCache.fs
index 995b1e5e211..aae0a99bc2f 100644
--- a/src/Compiler/Checking/OverloadResolutionCache.fs
+++ b/src/Compiler/Checking/OverloadResolutionCache.fs
@@ -162,7 +162,11 @@ let tryComputeOverloadCacheKey
                     | Some retTy ->
                         match tryGetTypeStructureForOverloadCache g retTy with
                         | ValueSome ts -> ValueSome ts
-                        | ValueNone -> if anyHasOutArgs then ValueNone else ValueSome(Stable(0, [||]))
+                        | ValueNone ->
+                            if anyHasOutArgs then
+                                ValueNone
+                            else
+                                ValueSome(Stable(0, [||]))
                     | None -> ValueSome(Stable(0, [||]))
 
                 match retTyStructure with
diff --git a/src/Compiler/Utilities/TypeHashing.fs b/src/Compiler/Utilities/TypeHashing.fs
index ff4bab0a403..0e7b3ea7638 100644
--- a/src/Compiler/Utilities/TypeHashing.fs
+++ b/src/Compiler/Utilities/TypeHashing.fs
@@ -409,8 +409,10 @@ module StructuralUtilities =
 
     let private hashTokenArray (tokens: TypeToken[]) =
         let mutable acc = 0
+
         for t in tokens do
             acc <- combineHash acc (hash t)
+
         acc
 
     [<CustomEquality; NoComparison>]
@@ -588,12 +590,16 @@ module StructuralUtilities =
         let out = ctx.Tokens
 
         // If the sequence got too long, just drop it, we could be dealing with an infinite type.
-        if out.Count >= MaxTokenCount then PossiblyInfinite
+        if out.Count >= MaxTokenCount then
+            PossiblyInfinite
         else
             let tokens = out.ToArray()
             let h = hashTokenArray tokens
-            if not ctx.Stable then Unstable(h, tokens)
-            else Stable(h, tokens)
+
+            if not ctx.Stable then
+                Unstable(h, tokens)
+            else
+                Stable(h, tokens)
 
     // Speed up repeated calls by memoizing results for types that yield a stable structure.
     let private getTypeStructureOfStrippedType =
