Type subsumption cache causes sustained ~150% CPU in IDE with generative type providers (hash code not memoized)
- Dominant language
- F#
- Stars
- 4.3k
- Forks
- 876
- Avg merge
- 4d 22h
- Merged PRs (30d)
- 144
Description
## Description
The type subsumption cache introduced in PR #18875 and partially fixed in PR #18926 causes sustained high CPU usage (~150%) in IDE/FSAC when a project uses generative type providers (e.g. custom HTML type provider with ~15 template instantiations).
PR #18926 memoized `TypeStructure` creation via a `ConditionalWeakTable`, but the `GetHashCode` of `TTypeCacheKey` still calls `GenericHashArbArray` on the underlying `ImmutableArray` on every cache lookup. Since `ImmutableArray` is a struct wrapping a `T[]`, F# structural hashing delegates to the underlying array, making each hash call O(n) over the token array. In IDE mode with repeated type checks, this dominates CPU.
A 10-second `dotnet-trace` CPU sample on the FSAC process (166% CPU) shows:
| Samples | Function |
|---------|----------|
| 2414 | `Thread.PollGC` |
| 1170 | `RuntimeTypeHandle.InternalAllocNoChecks` |
| 820 | `GenericEqualityArbArray` |
| 696 | `TTypeCacheKey.GetHashCode` → `GenericHashArbArray` |
| 96 | `ConcurrentDictionary.GrowTable` |
| 12 | `Cache.rebuildStore` |
## Repro steps
1. Create an F# solution targeting `net10.0` (LangVersion 10) with 5+ projects
2. Add a generative type provider (e.g. one producing ~15 provided types with a few methods each)
3. Reference the type provider from 2+ projects in the solution
4. Open the solution in VS Code with Ionide (FSAC)
5. Observe sustained ~150% CPU on the `dotnet fsautocomplete.dll` process that does not settle down
A minimal repro with 30 template instantiations in a single project did **not** reproduce the issue. The combination of multiple projects, generative type provider types, and other type providers (e.g. FSharp.Data JsonProvider) in the same solution appears necessary to trigger it.
## Expected behavior
CPU usage should settle to near-zero after initial type checking completes. The subsumption cache should improve performance, not degrade it.
## Actual behavior
CPU stays at ~150% indefinitely. The CPU is spent almost entirely in `TTypeCacheKey.GetHashCode` → `GenericHashArbArray`, hashing `ImmutableArray` arrays on every cache lookup. The cache appears to continuously grow and rebuild, each rebuild re-hashing all existing entries at O(n) per entry.
Diagnostic logging in the type provider confirmed:
- FCS re-creates provider instances every ~10 seconds (observed 12 instances in ~60 seconds)
- Even with static caches returning identical `ProvidedTypeDefinition` objects (all cache hits), CPU remains high
- The provider itself does negligible work — the CPU is entirely in FCS internal processing
## Known workarounds
Set `9.0` in `Directory.Build.props` to disable the `UseTypeSubsumptionCache` language feature. This eliminates the CPU issue entirely. Requires replacing any F# 10-only syntax (e.g. `let! x: T =` → `let! (x: T) =`).
## Related information
- macOS (Darwin 25.3.0, Apple Silicon)
- .NET SDK 10.0.103
- Ionide 7.31.1, FSAC 0.83.0, FSharp.Compiler.Service 43.10.101-servicing
- Related issues: #18925, PR #18926 (partial fix), #19169 (build regression)
- The suggested fix: cache the hash code inside `TypeStructure` (or `TTypeCacheKey`) so `GetHashCode` is O(1) after first computation, not O(n) on every call
Contributor guide
Assessment
This issue has not been assessed yet.