Skip to content

refactor(CancellableTasks): Delegate to Task.sequential/parallelLimit - #20695

Open
bartelink wants to merge 5 commits into
dotnet:mainfrom
bartelink:apply-parallel-limit
Open

bartelink wants to merge 5 commits into
dotnet:mainfrom
bartelink:apply-parallel-limit

Conversation

@bartelink

@bartelink bartelink commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Follow up to #20128 as noted in #20128 (comment) - Replace bodies of CancellableTask.whenAllThrottled and sequential with Task.parallelLimit and sequential

Copilot AI balanced review requested due to automatic review settings October 3, 2026 23:09
@bartelink
bartelink requested a review from a team as a code owner October 3, 2026 23:09
@bartelink bartelink changed the title refactor(CancellableTasks): Apply Task.sequential/parallelLimit refactor(CancellableTasks): Delegate to Task.sequential/parallelLimit Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev

@bartelink,

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)`

See examples in the files, listed in the table below or in th full documentation at https://fsharp.github.io/fsharp-compiler-docs/release-notes/About.html.

If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.

Change path Release notes path Description
`src/FSharp.Core` docs/release-notes/.FSharp.Core/11.0.200.md No release notes found or release notes format is not correct

✅ Found changes and release notes in following paths:

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The sequential delegation can continue queued work after cancellation.

Review effort: Balanced
Findings: 1 Medium severity

Open (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 Task helpers.
  • 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.

Comment thread vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs Outdated
Comment thread vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs Outdated
@bartelink
bartelink force-pushed the apply-parallel-limit branch 10 times, most recently from 5ac18b9 to dfe59e9 Compare October 5, 2026 08:53

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 🕵️ AI review — verify independently.


return! allTask
}
fun ct -> Task.parallelLimit maxDegreeOfParallelism ct tasks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 🕵️ 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have pushed a revert of the inlining. Not sure if that will actually clear the issue... Happy to take guidance....

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 🕵️ 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() |> ignore

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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...

@T-Gro
T-Gro self-requested a review October 7, 2026 12:51
@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Oct 7, 2026
@bartelink
bartelink force-pushed the apply-parallel-limit branch from 6db3c40 to d1ba03f Compare October 7, 2026 20:40
@bartelink
bartelink force-pushed the apply-parallel-limit branch from d1ba03f to 7740b07 Compare October 7, 2026 20:43

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-reviewed PR reviewed by AI review council

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants