Avoid cancellableTask CE in DocumentCache to preserve static-state-machine optimization - #20273
Conversation
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
Warning No PR link found in some release notes, please consider adding it.
|
cancellableTask CE in DocumentCache to preserve static-state-machine optimization
T-Gro
left a comment
There was a problem hiding this comment.
Thanks for this — the direction is reasonable and the diff is small and readable. A few notes, mostly about the rationale rather than the mechanics.
1. The justification is imprecise — the CE does statically compile
cancellableTask.Run is built on __stateMachine / __useResumableCode, so the resumable body is compiled to a static state machine today. What cancellableTask { … } additionally does is:
- allocate the
AfterCodeclosure (fun ct -> …) that captures the struct state machine, and - because
cancellableTask = CancellableTaskBuilder(true)(the background builder), inject aTask.Runoffload when not already on the thread pool.
Your replacement fun ct -> tryGetCachedValueAsync (doc, cache, ct) still allocates a closure (it captures doc and cache), and the inner task { … } still boxes its state machine on the first suspension. So the path is not "allocation-free" — it's "one smaller closure, no background Task.Run, no chance of a dynamic fallback."
Suggest tightening the release note wording accordingly, e.g. "avoids the background Task.Run offload and a larger wrapper closure from the cancellableTask builder" rather than "allocation-free." The precise wording matters here because the note is the durable explanation for why this pattern exists.
2. Behavioral change: background offload is dropped (Low — looks intentional/safe)
cancellableTask is the background builder, so the old helpers were offloaded via Task.Run when invoked from a sync context; the new task { … } runs inline on the caller until the first real await. Both current callers (ClassificationService.AddSemanticClassificationsAsync and HintService.getHintsForDocument) already wrap the work in a background cancellableTask, so the inner offload was redundant and removing it is fine — arguably a small win. Worth a one-line note that this is intentional.
3. Behavioral change: eager cancellation check is lost (Low)
The CE Run entry checks ct.IsCancellationRequested and returns Task.FromCanceled<_>(ct) before running any code. The manual helpers skip that and call doc.GetTextVersionAsync ct directly. Roslyn's GetTextVersionAsync honors the token, so this is practically equivalent, but you no longer guarantee a canceled Task on an already-canceled token. If you want to preserve exact semantics, add a guard:
static let tryGetCachedValueAsync (doc: Document, cache: MemoryCache, ct: CancellationToken) =
if ct.IsCancellationRequested then Task.FromCanceled<_ voption>(ct)
else task { … }Otherwise fine to leave as-is.
4. Nits / positives
- Making the helpers
static letso they don't capturethisis a nice touch. 👍 - An alternative that keeps CE ergonomics while dropping the background offload is
foregroundCancellableTask { … }— no need to change, just noting the CE didn't have to be abandoned to get the perf shape you want. - Please confirm CI is green: the secondary
new(name, slidingExpirationSeconds)constructor now sits after thestatic letbindings. That ordering is valid F#, but worth a glance.
Net: I'd approve after the release-note wording is adjusted (item 1) — the code change itself is sound.
377beb1 to
dbf142a
Compare
dbf142a to
84a158c
Compare
Summary
Fixes #20268 by avoiding the
cancellableTaskcomputation expression inDocumentCache, where it cannot compile to a static state machine in the hot path. The cache helpers now use directCancellationToken-awaretaskwrappers instead of a closure-basedcancellableTaskwrapper.Changes
TryGetValueAsyncandSetAsyncinvsintegration/src/FSharp.Editor/Common/DocumentCache.fsto explicitfun ct -> task { ... }functions.CancellableTask<'T>API shape while removing the non-static resumable wrapper from the performance-sensitive code path.docs/release-notes/.VisualStudio/18.vNext.mdfor issueDocumentCache.fscannot usecancellableTaskCE because it does not compile to a static state machine #20268.