Skip to content

P3: FSharpProjectOptionsReactor processes requests strictly FIFO — no priority for the active document #20122

Description

@xperiandri

Summary

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:

let loop (agent: MailboxProcessor<FSharpProjectOptionsMessage>) =
    async {
        while true do
            match! agent.Receive() with
            | FSharpProjectOptionsMessage.TryGetOptionsByDocument(document, reply, ct, userOpName) -> ...
            | FSharpProjectOptionsMessage.TryGetOptionsByProject(project, reply, ct) -> ...
            | FSharpProjectOptionsMessage.ClearOptions(projectId) -> ...
            ...
    }

let reactor = new FSharpProjectOptionsReactor(checker)

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:

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status
    New

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions