From 2b9cbf62b71087ee5da4741485edadaeefb45aa7 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 17 Aug 2026 02:07:24 +0200 Subject: [PATCH 1/2] Avoid cancellableTask in DocumentCache and add release notes --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + .../src/FSharp.Editor/Common/DocumentCache.fs | 23 +++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index cffa42edc9c..c7239567ba2 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -7,6 +7,7 @@ * 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)) +* Avoid using `cancellableTask` in `DocumentCache` because it cannot compile to a static state machine in the hot path; the editor cache now uses direct `CancellationToken`-aware `task` wrappers to keep the async path allocation-free. ([Issue #20268](https://github.com/dotnet/fsharp/issues/20268)) * 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)) * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421)) diff --git a/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs b/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs index 6c88bc0f912..892f7e93328 100644 --- a/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs +++ b/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs @@ -2,6 +2,8 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System open System.Runtime.Caching +open System.Threading +open System.Threading.Tasks open Microsoft.CodeAnalysis open CancellableTasks @@ -16,12 +18,8 @@ type DocumentCache<'Value when 'Value: not struct>(name: string, ?cacheItemPolic let policy = defaultArg cacheItemPolicy (CacheItemPolicy(SlidingExpiration = (TimeSpan.FromSeconds defaultSlidingExpiration))) - new(name: string, slidingExpirationSeconds: float) = - new DocumentCache<'Value>(name, CacheItemPolicy(SlidingExpiration = (TimeSpan.FromSeconds slidingExpirationSeconds))) - - member _.TryGetValueAsync(doc: Document) = - cancellableTask { - let! ct = CancellableTask.getCancellationToken () + static let tryGetCachedValueAsync (doc: Document, cache: MemoryCache, ct: CancellationToken) = + task { let! currentVersion = doc.GetTextVersionAsync ct match cache.Get(doc.Id.ToString()) with @@ -34,12 +32,19 @@ type DocumentCache<'Value when 'Value: not struct>(name: string, ?cacheItemPolic | _ -> return ValueNone } - member _.SetAsync(doc: Document, value: 'Value) = - cancellableTask { - let! ct = CancellableTask.getCancellationToken () + static let setCacheValueAsync (doc: Document, value: 'Value, cache: MemoryCache, policy: CacheItemPolicy, ct: CancellationToken) = + task { let! currentVersion = doc.GetTextVersionAsync ct do cache.Set(doc.Id.ToString(), (currentVersion, value), policy) } + new(name: string, slidingExpirationSeconds: float) = + new DocumentCache<'Value>(name, CacheItemPolicy(SlidingExpiration = (TimeSpan.FromSeconds slidingExpirationSeconds))) + + member _.TryGetValueAsync(doc: Document) : CancellableTask<'Value voption> = fun ct -> tryGetCachedValueAsync (doc, cache, ct) + + member _.SetAsync(doc: Document, value: 'Value) : CancellableTask = + fun ct -> setCacheValueAsync (doc, value, cache, policy, ct) + interface IDisposable with member _.Dispose() = cache.Dispose() From 84a158cd6131beee2ba86cf6f2683a44ea72e420 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Mon, 17 Aug 2026 13:03:28 +0200 Subject: [PATCH 2/2] DocumentCache: preserve cancellation semantics --- docs/release-notes/.VisualStudio/18.vNext.md | 2 +- .../src/FSharp.Editor/Common/DocumentCache.fs | 41 +++++++++++-------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index c7239567ba2..9e49f812bd6 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -7,7 +7,7 @@ * 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)) -* Avoid using `cancellableTask` in `DocumentCache` because it cannot compile to a static state machine in the hot path; the editor cache now uses direct `CancellationToken`-aware `task` wrappers to keep the async path allocation-free. ([Issue #20268](https://github.com/dotnet/fsharp/issues/20268)) +* Avoid using `cancellableTask` in `DocumentCache`; the editor cache now uses direct `CancellationToken`-aware `task` wrappers, avoiding the background `Task.Run` offload and a larger wrapper closure from the `cancellableTask` builder. ([Issue #20268](https://github.com/dotnet/fsharp/issues/20268)) * 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)) * Improve static compilation of state machines. ([PR #19297](https://github.com/dotnet/fsharp/pull/19297)) * Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421)) diff --git a/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs b/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs index 892f7e93328..9233d95e8f4 100644 --- a/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs +++ b/vsintegration/src/FSharp.Editor/Common/DocumentCache.fs @@ -19,29 +19,36 @@ type DocumentCache<'Value when 'Value: not struct>(name: string, ?cacheItemPolic defaultArg cacheItemPolicy (CacheItemPolicy(SlidingExpiration = (TimeSpan.FromSeconds defaultSlidingExpiration))) static let tryGetCachedValueAsync (doc: Document, cache: MemoryCache, ct: CancellationToken) = - task { - let! currentVersion = doc.GetTextVersionAsync ct - - match cache.Get(doc.Id.ToString()) with - | null -> return ValueNone - | :? (VersionStamp * 'Value) as value -> - if fst value = currentVersion then - return ValueSome(snd value) - else - return ValueNone - | _ -> return ValueNone - } + if ct.IsCancellationRequested then + Task.FromCanceled<'Value voption>(ct) + else + task { + let! currentVersion = doc.GetTextVersionAsync ct + + match cache.Get(doc.Id.ToString()) with + | null -> return ValueNone + | :? (VersionStamp * 'Value) as value -> + if fst value = currentVersion then + return ValueSome(snd value) + else + return ValueNone + | _ -> return ValueNone + } static let setCacheValueAsync (doc: Document, value: 'Value, cache: MemoryCache, policy: CacheItemPolicy, ct: CancellationToken) = - task { - let! currentVersion = doc.GetTextVersionAsync ct - do cache.Set(doc.Id.ToString(), (currentVersion, value), policy) - } + if ct.IsCancellationRequested then + Task.FromCanceled(ct) + else + task { + let! currentVersion = doc.GetTextVersionAsync ct + do cache.Set(doc.Id.ToString(), (currentVersion, value), policy) + } new(name: string, slidingExpirationSeconds: float) = new DocumentCache<'Value>(name, CacheItemPolicy(SlidingExpiration = (TimeSpan.FromSeconds slidingExpirationSeconds))) - member _.TryGetValueAsync(doc: Document) : CancellableTask<'Value voption> = fun ct -> tryGetCachedValueAsync (doc, cache, ct) + member _.TryGetValueAsync(doc: Document) : CancellableTask<'Value voption> = + fun ct -> tryGetCachedValueAsync (doc, cache, ct) member _.SetAsync(doc: Document, value: 'Value) : CancellableTask = fun ct -> setCacheValueAsync (doc, value, cache, policy, ct)