You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
FSharpProjectOptionsReactor (vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs) serializes all project-options requests through a single MailboxProcessor<FSharpProjectOptionsMessage> and processes them with a plain FIFO loop:
Every consumer of project options — the active editor tab computing diagnostics/classification/completion, and background services (solution crawler passes, Find All References, unused-opens/unused-declarations analyzers, etc.) — posts into the same mailbox and is served in strict arrival order.
Problem
When background work enqueues a burst of TryGetOptionsByDocument / TryGetOptionsByProject messages (e.g. a crawler pass over many documents, or Find All References across a large project), a request coming from the active document (the one the user is currently typing in) can be queued behind dozens of background requests. Since each request may trigger a real F# Compiler Service computation (tryComputeOptions, tryComputeOptionsBySingleScriptOrFile), this can noticeably delay diagnostics/IntelliSense responsiveness for the file the user is actively looking at, even though the reactor itself isn't overloaded in absolute terms — it's simply working through older, lower-priority requests first.
This mirrors the general theme of the background-activity-minimization effort: background work should not be allowed to starve foreground/interactive work.
Proposed solution
Introduce a priority queue in front of (or instead of) the plain FIFO MailboxProcessor, so that requests associated with the active document are dequeued ahead of background requests:
Two-tier queue. Replace the single MailboxProcessor receive loop with two channels/queues — a small-capacity "foreground" queue and a "background" queue (e.g. System.Threading.Channels.Channel<'T> with UnboundedChannel for background and a bounded/unbounded high-priority channel for foreground), or a single MailboxProcessor combined with an internal PriorityQueue<FSharpProjectOptionsMessage, int> that the loop drains with priority ordering (using agent.TryScan/Scan is not ideal for this since it re-scans the whole mailbox on every call; a dedicated processing loop backed by System.Threading.Channels is a cleaner fit for prioritized draining).
Priority classification at post time. When a message is posted (TryGetOptionsByDocument, TryGetOptionsByProject), classify it using the existing ActiveDocumentDetection helper (see Internal error in FSI: FS0192: binding null type in envBindTypeRef #9-related work in vsintegration/src/FSharp.Editor/Diagnostics/ActiveDocumentDetection.fs) — if the request's document/project matches the active document, enqueue into the foreground queue; otherwise the background queue.
Draining order. The processing loop should always prefer to drain the foreground queue when it is non-empty, falling back to the background queue only when the foreground queue is empty, so active-document requests are never blocked behind an arbitrarily long backlog of background requests. To avoid starving background work entirely under sustained foreground activity, consider a simple weighted/round-robin fallback (e.g., service at most N foreground messages before checking background once) if needed in practice.
Cancellation-awareness. Preserve existing behavior where messages already carrying a canceled CancellationToken are replied to immediately with ValueNone without doing any work, for both queues.
No change to computation semantics.tryComputeOptions/tryComputeOptionsBySingleScriptOrFile and the existing caches (cache, lastSuccessfulCompilations, emitCache) are unaffected — this is purely a scheduling/ordering change on top of the existing reactor, not a change to what gets computed.
Alternative considered
A lighter-weight alternative would be to keep the single MailboxProcessor but call agent.Scan at the head of the loop to look for a foreground message first before falling back to agent.Receive(). This avoids introducing System.Threading.Channels but has less predictable performance characteristics under a large mailbox backlog (each Scan call walks the mailbox), and is likely a reasonable first iteration if the full priority queue is judged too invasive for a first pass.
Impact
Low risk (isolated to FSharpProjectOptionsReactor's message loop), improves perceived editor responsiveness for the active document during heavy background project-options activity (crawler passes, Find All References, etc.), without changing correctness or caching behavior.
Related
Continuation of the background-activity-minimization effort tracked in docs/ide/background-activity-minimization-plan.md.
Summary
FSharpProjectOptionsReactor(vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs) serializes all project-options requests through a singleMailboxProcessor<FSharpProjectOptionsMessage>and processes them with a plain FIFO loop:Every consumer of project options — the active editor tab computing diagnostics/classification/completion, and background services (solution crawler passes, Find All References, unused-opens/unused-declarations analyzers, etc.) — posts into the same mailbox and is served in strict arrival order.
Problem
When background work enqueues a burst of
TryGetOptionsByDocument/TryGetOptionsByProjectmessages (e.g. a crawler pass over many documents, orFind All Referencesacross a large project), a request coming from the active document (the one the user is currently typing in) can be queued behind dozens of background requests. Since each request may trigger a real F# Compiler Service computation (tryComputeOptions,tryComputeOptionsBySingleScriptOrFile), this can noticeably delay diagnostics/IntelliSense responsiveness for the file the user is actively looking at, even though the reactor itself isn't overloaded in absolute terms — it's simply working through older, lower-priority requests first.This mirrors the general theme of the background-activity-minimization effort: background work should not be allowed to starve foreground/interactive work.
Proposed solution
Introduce a priority queue in front of (or instead of) the plain FIFO
MailboxProcessor, so that requests associated with the active document are dequeued ahead of background requests:MailboxProcessorreceive loop with two channels/queues — a small-capacity "foreground" queue and a "background" queue (e.g.System.Threading.Channels.Channel<'T>withUnboundedChannelfor background and a bounded/unbounded high-priority channel for foreground), or a singleMailboxProcessorcombined with an internalPriorityQueue<FSharpProjectOptionsMessage, int>that the loop drains with priority ordering (usingagent.TryScan/Scanis not ideal for this since it re-scans the whole mailbox on every call; a dedicated processing loop backed bySystem.Threading.Channelsis a cleaner fit for prioritized draining).TryGetOptionsByDocument,TryGetOptionsByProject), classify it using the existingActiveDocumentDetectionhelper (see Internal error in FSI: FS0192: binding null type in envBindTypeRef #9-related work invsintegration/src/FSharp.Editor/Diagnostics/ActiveDocumentDetection.fs) — if the request's document/project matches the active document, enqueue into the foreground queue; otherwise the background queue.CancellationTokenare replied to immediately withValueNonewithout doing any work, for both queues.tryComputeOptions/tryComputeOptionsBySingleScriptOrFileand the existing caches (cache,lastSuccessfulCompilations,emitCache) are unaffected — this is purely a scheduling/ordering change on top of the existing reactor, not a change to what gets computed.Alternative considered
A lighter-weight alternative would be to keep the single
MailboxProcessorbut callagent.Scanat the head of the loop to look for a foreground message first before falling back toagent.Receive(). This avoids introducingSystem.Threading.Channelsbut has less predictable performance characteristics under a large mailbox backlog (eachScancall walks the mailbox), and is likely a reasonable first iteration if the full priority queue is judged too invasive for a first pass.Impact
Low risk (isolated to
FSharpProjectOptionsReactor's message loop), improves perceived editor responsiveness for the active document during heavy background project-options activity (crawler passes, Find All References, etc.), without changing correctness or caching behavior.Related
docs/ide/background-activity-minimization-plan.md.