Repository navigation
Conversation
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev Caution No release notes found for the changed paths (see table below). Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format. The following format is recommended for this repository: `* . (PR #XXXXX)`
If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.
|
4ae3982 to
91809eb
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The sequential delegation can continue queued work after cancellation.
Review effort: Balanced
Findings: 1
What changed in this PR
Refactors editor cancellable-task helpers to reuse FSharp.Core task combinators.
Changes:
- Delegates throttled parallel and sequential execution to
Taskhelpers. - Adds the follow-up PR to Visual Studio release notes.
| File | Description |
|---|---|
CancellableTasks.fs |
Replaces custom scheduling implementations. |
18.vNext.md |
Adds the follow-up PR reference. |
5ac18b9 to
dfe59e9
Compare
T-Gro
left a comment
There was a problem hiding this comment.
🤖 🕵️ AI review — verify independently.
|
|
||
| return! allTask | ||
| } | ||
| fun ct -> Task.parallelLimit maxDegreeOfParallelism ct tasks |
There was a problem hiding this comment.
🤖 🕵️ A pre-canceled request now runs the input enumerator and throws synchronously instead of returning a canceled task.
open System
open System.Threading
open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks
let work: seq<CancellableTask<int>> =
seq {
raise (InvalidOperationException "enumeration failure")
yield CancellableTask.singleton 42
}
let pending =
CancellableTask.whenAllThrottled 2 work (CancellationToken true)
// Before: pending.IsCanceled. Now: throws before pending is assigned.There was a problem hiding this comment.
have pushed a revert of the inlining. Not sure if that will actually clear the issue... Happy to take guidance....
There was a problem hiding this comment.
See below - now reinstated, but the new incoming CT Canceled guards should now prevent this possibility
|
|
||
| return! allTask | ||
| } | ||
| fun ct -> Task.parallelLimit maxDegreeOfParallelism ct tasks |
There was a problem hiding this comment.
🤖 🕵️ Completed factories retain 62.4 MiB of otherwise collectible buffers until the last task completes — 500/500 live buffers versus 1/500 before.
open System
open System.Threading
open System.Threading.Tasks
open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks
let refs = ResizeArray<WeakReference>()
let gate = TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously)
let work: seq<CancellableTask<int>> =
seq {
for i in 0 .. 499 do
let payload = Array.zeroCreate<byte> (128 * 1024)
payload[0] <- 17uy
refs.Add(WeakReference payload)
yield fun _ ->
if i = 499 then
task {
let! value = gate.Task
return value + int payload[0]
}
else
Task.FromResult(int payload[0])
}
let pending = CancellableTask.whenAllThrottled 4 work CancellationToken.None
Thread.Sleep 100
GC.Collect(2, GCCollectionMode.Forced, true, true)
let liveBuffers = refs |> Seq.filter _.IsAlive |> Seq.length
// Before: 1. Now: 500, while gate remains closed.
gate.SetResult 0
pending.GetAwaiter().GetResult() |> ignoreThere was a problem hiding this comment.
There's a Seq.toArray at the very start of Task.parallel that dictates a lot of what's possible here.
A more reasonable test of laziness/resource waste would be for the refs.Add bit to happen within the yield fun _ ->, which would e.g. trap an impl that created lots of work but froze it. As it is, each worker only runs the factory the moment it's ready to run. The original impl can have 500 tasks in flight awaiting the semaphore, which would be a far more significant drain...
parallelLimit differs from sequential in synchronously commencing the enumeration of the computations seq before any cancellation check:
match Seq.toArray computations with
| _ when ct.IsCancellationRequested -> Task.FromCanceled<'T[]> ct
It could be argued that the fix for sequential is thus to add a guard before it enumerates:
if ct.IsCancellationRequested then Task.FromCanceled<'T[]> ct else
If you're doing that, you'd probably make parallelLimit follow suit, skipping the Seq.toArray if its cancelled:
if ct.IsCancellationRequested then Task.FromCanceled<'T[]> ct else
match Seq.toArray computations with
// REMOVE: | _ when ct.IsCancellationRequested -> Task.FromCanceled<'T[]> ct
(IIRC I had it as an if guard until Fantomas started harassing me)
@T-Gro happy to take any guidance on what way to go here...
There was a problem hiding this comment.
@T-Gro I applied the above (consistent guard reacting to Canceled incoming CT with a Task.FromCanceled, guaranteeing the enumeration of the computations (and exceptions that may trigger) are avoided where there will be zero work done) and reinstated the removal of the cancellableTask CE wrapping.
Would be interested to see what review thinks now...
6db3c40 to
d1ba03f
Compare
This reverts commit dfe59e9.
d1ba03f to
7740b07
Compare

Follow up to #20128 as noted in #20128 (comment) - Replace bodies of
CancellableTask.whenAllThrottledandsequentialwithTask.parallelLimitandsequential