Skip to content

F# VS editor: reduce background CPU load by gating expensive analyzers to the active document only #20114

Description

@xperiandri

Is your feature request related to a problem? Please describe.

CPU profiling of devenv.exe (Visual Studio Insiders + F# extension) shows ThreadPoolWorkQueue.Dispatch at 72 % total / 59 % self, driven by a continuous stream of small tasks from FSharp.Compiler.CheckDeclarations, FSharpProjectOptionsReactor, and async/cancellableTask state-machine overhead.

The root cause is not a single hot function but the volume of background tasks scheduled regardless of whether an F# document is actually active/visible. Roslyn's default BackgroundAnalysisScope for C# is VisibleFilesAndOpenFilesWithPreviouslyReportedDiagnostics, but F# analyzers currently run a full typecheck for every open tab on every crawler pass.


Describe the solution you'd like

Already implemented in this PR

A shared ActiveDocumentDetection module was extracted that uses SVsShellMonitorSelection / IVsMonitorSelection / IVsWindowFrame to determine whether a Roslyn Document is the currently focused editor tab. The module is now applied to the four heaviest analyzers/services:

Component File Fix applied
UnusedOpensDiagnosticAnalyzer Diagnostics/UnusedOpensDiagnosticAnalyzer.fs active-document gate (was already in place, refactored to shared module)
UnusedDeclarationsAnalyzer Diagnostics/UnusedDeclarationsAnalyzer.fs active-document gate added
SimplifyNameDiagnosticAnalyzer Diagnostics/SimplifyNameDiagnosticAnalyzer.fs active-document gate added
FSharpInlayHintsService Hints/FSharpInlayHintsService.fs active-document gate added

The shared helper (ActiveDocumentDetection.isActiveDocument) falls back to true (= don't suppress) when the active document cannot be determined, so analysis is never silently lost.

Remaining work (tracked in docs/ide/background-activity-minimization-plan.md)

# Source Estimated impact Risk
4 ClassificationService.AddSemanticClassificationsAsync — cache miss triggers full typecheck for every open tab Medium Low-medium
5 createPEReference / Compilation.Emit — re-emits metadata for every C# referenced project on each workspace event High Medium
6 hasDependentVersionChanged — synchronous .Result blocks the reactor MailboxProcessor thread Medium Low
7 FSharpProjectOptionsReactor — FIFO queue starves active-document requests Medium Medium
8 Script updateProjectOptions fires on every caret move Low-medium Low
9 DocumentDiagnosticAnalyzer semantic path — no text-version short-circuit Medium Low
10 SymbolHelpers.findReferencedSymbolsAsync — typechecks all documents in solution Low (on-demand) Low

Full root-cause descriptions and proposed fixes are in docs/ide/background-activity-minimization-plan.md.


Describe alternatives you've considered

  • Lowering BackgroundAnalysisScope globally via IGlobalOptionService — blocked by internal Roslyn API; proposed as a future Roslyn API extension in docs/ide/api-designs/Active-Document-Background-Analysis.md.
  • Using IDocumentTrackingService from Roslyn — not exposed via ExternalAccess; same future-API path.
  • SemaphoreSlim throttling alone — already in place for SimplifyName but does not eliminate unnecessary work, just caps it.

Additional context

  • Profile was collected on Visual Studio 18.9 Insiders (devenv.exe) with the /rootsuffix RoslynDev hive.
  • The immediate symptom is sluggish UI and continuous CPU consumption even when the user is not typing or switching tabs.
  • The fix is fully backward-compatible: non-active documents receive fresh diagnostics/hints as soon as the user switches to them.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status
    New

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions