From a76519648c3020f3d492f5b55dd5fdf3fccd738b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 3 Aug 2026 02:33:07 +0200 Subject: [PATCH 1/4] Perf: add CancellableTask.whenAllThrottled to cap parallel typechecks in Find All References (#20127) --- .../FSharp.Editor/Common/CancellableTasks.fs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs index 5056ddee461..e067c7f6408 100644 --- a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs +++ b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs @@ -1098,6 +1098,28 @@ module CancellableTasks = return! Task.WhenAll (tasks) } + /// Runs the given tasks concurrently, but caps the number of tasks that are running at + /// the same time to at most maxDegreeOfParallelism, to avoid launching an unbounded number + /// of parallel typechecks (e.g. one per document/project) at once. + let inline whenAllThrottled maxDegreeOfParallelism (tasks: CancellableTask<'a> seq) = + cancellableTask { + let! ct = getCancellationToken () + use semaphore = new SemaphoreSlim(maxDegreeOfParallelism: int) + + let runThrottled (task: CancellableTask<'a>) = + backgroundTask { + do! semaphore.WaitAsync(ct) + + try + return! start ct task + finally + semaphore.Release() |> ignore + } + + let tasks = seq { for task in tasks do yield runThrottled task } + return! Task.WhenAll (tasks) + } + let inline whenAllTasks (tasks: CancellableTask seq) = cancellableTask { let! ct = getCancellationToken () From bde113713bdb7deb02b2ac933516a3f666fc78a7 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 3 Aug 2026 02:34:25 +0200 Subject: [PATCH 2/4] Perf: throttle parallel typechecks in Find All References (#20127) --- .../src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index ef0929a1211..2406f3a6e32 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -1,4 +1,4 @@ -[] +[] module internal Microsoft.VisualStudio.FSharp.Editor.WorkspaceExtensions open System @@ -703,7 +703,8 @@ type Project with documents |> Seq.map (fun doc -> doc.FindFSharpReferencesAsync(symbol, projectSnapshot, (fun range -> onFound doc range), userOpName)) - |> CancellableTask.whenAll + // Throttle to avoid launching a typecheck per document in the project all at once. + |> CancellableTask.whenAllThrottled (max 1 Environment.ProcessorCount) else for doc in documents do do! doc.FindFSharpReferencesAsync(symbol, projectSnapshot, (onFound doc), userOpName) From 7ac8f30a5cce13893c44227fb038dc5a366ac6df Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 12 Aug 2026 15:20:45 +0200 Subject: [PATCH 3/4] Perf: Add release notes (#20127) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index cffa42edc9c..f23a2a2ce9c 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -5,6 +5,7 @@ ### Fixed +* Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Find All References for external DLL symbols now only searches projects that reference the specific assembly. ([Issue #10227](https://github.com/dotnet/fsharp/issues/10227), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) From 7268a33fd55f29b3d8d4610ef8aa4661baf65be0 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Wed, 12 Aug 2026 16:06:20 +0200 Subject: [PATCH 4/4] Perf: Refactor `whenAllThrottled` for clearer resource handling Refactored the `whenAllThrottled` function to use an array of background tasks, ensuring each acquires/releases the semaphore properly. Explicitly disposes the semaphore after all tasks complete using a continuation on `Task.WhenAll`. Simplified the XML doc comment. This replaces the previous sequence expression and `use` binding with more robust disposal logic. --- .../FSharp.Editor/Common/CancellableTasks.fs | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs index e067c7f6408..7520395a084 100644 --- a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs +++ b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs @@ -1098,26 +1098,36 @@ module CancellableTasks = return! Task.WhenAll (tasks) } - /// Runs the given tasks concurrently, but caps the number of tasks that are running at - /// the same time to at most maxDegreeOfParallelism, to avoid launching an unbounded number - /// of parallel typechecks (e.g. one per document/project) at once. + /// Runs the given tasks concurrently, but caps concurrent work to maxDegreeOfParallelism. let inline whenAllThrottled maxDegreeOfParallelism (tasks: CancellableTask<'a> seq) = cancellableTask { let! ct = getCancellationToken () - use semaphore = new SemaphoreSlim(maxDegreeOfParallelism: int) - - let runThrottled (task: CancellableTask<'a>) = - backgroundTask { - do! semaphore.WaitAsync(ct) - - try - return! start ct task - finally - semaphore.Release() |> ignore - } + let semaphore = new SemaphoreSlim(maxDegreeOfParallelism: int) + + let started = + [| + for task in tasks do + backgroundTask { + do! semaphore.WaitAsync(ct) + + try + return! start ct task + finally + semaphore.Release() |> ignore + } + |] + + let allTask = Task.WhenAll started + + allTask.ContinueWith( + (fun (_: Task<'a[]>) -> semaphore.Dispose()), + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default + ) + |> ignore - let tasks = seq { for task in tasks do yield runThrottled task } - return! Task.WhenAll (tasks) + return! allTask } let inline whenAllTasks (tasks: CancellableTask seq) =