P2: Use stale semantic classification for open-but-inactive documents instead of recomputing on every cache miss
- Dominant language
- F#
- Stars
- 4.3k
- Forks
- 876
- Avg merge
- 4d 22h
- Merged PRs (30d)
- 144
Description
## Summary
`FSharpClassificationService.AddSemanticClassificationsAsync` (`vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs`) treats every **open** document the same way, regardless of whether it is the active tab or a background/inactive tab:
```fsharp
member _.AddSemanticClassificationsAsync
(document: Document, textSpan: TextSpan, result: List, cancellationToken: CancellationToken)
=
...
let isOpenDocument = document.Project.Solution.Workspace.IsDocumentOpen document.Id
...
else // isOpenDocument
match! openedDocumentsSemanticClassificationCache.TryGetValueAsync document with
| ValueSome classificationDataLookup -> ...
| ValueNone ->
...
let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync(nameof (IFSharpClassificationService))
...
```
There is only a single binary split: "open" vs "not open" (`unopenedDocumentsSemanticClassificationCache` has a 5-minute lifetime, `openedDocumentsSemanticClassificationCache` only 2 minutes). Every open document — including background tabs that are not currently being viewed — goes through the short-lived (2 min) cache and, on a miss, triggers a full `GetFSharpParseAndCheckResultsAsync` typecheck to (re)compute semantic classification, even though the user isn't looking at it.
Also notable: Roslyn's `IFSharpClassificationService` already exposes an extension point specifically designed for this scenario:
```fsharp
member _.AdjustStaleClassification(_: SourceText, classifiedSpan: ClassifiedSpan) : ClassifiedSpan = classifiedSpan
```
This is currently a no-op passthrough, meaning we're not taking advantage of Roslyn's "stale classification while re-computing" pipeline at all.
## Problem
When a solution has many open tabs (a common scenario — VS keeps many documents open even when only one is visible), each background tab still has its semantic classification recomputed on a short (2-minute) cadence via a full FCS typecheck, contributing to background CPU churn even though:
- The user cannot see the (possibly stale) classification for inactive tabs anyway.
- Classification correctness for a non-visible tab is not time-sensitive — it only needs to be fresh again by the time the tab becomes active/visible.
This is the same class of problem already addressed for `UnusedOpensDiagnosticAnalyzer`, `UnusedDeclarationsAnalyzer`, `SimplifyNameDiagnosticAnalyzer`, and `FSharpInlayHintsService` via `ActiveDocumentDetection` (see the background-activity-minimization effort) — those now skip/gate expensive work for non-active documents. Semantic classification has not yet received the same treatment.
## Proposed solution
1. **Distinguish active vs. inactive-but-open documents.** Extend the existing `isOpenDocument` check in `AddSemanticClassificationsAsync` with the `ActiveDocumentDetection` helper (`vsintegration/src/FSharp.Editor/Diagnostics/ActiveDocumentDetection.fs`) used elsewhere in the codebase, so we have three states instead of two: *active*, *open-but-inactive*, *closed*.
2. **Serve stale/cached classification for inactive-but-open documents.** For documents that are open but not active:
- Prefer whatever is already in `openedDocumentsSemanticClassificationCache` (even if past its current 2-minute TTL) rather than forcing a fresh typecheck — i.e., treat the cache as "good enough" data for a tab the user isn't looking at.
- Only fall back to a real `GetFSharpParseAndCheckResultsAsync` computation if there is no cached data at all yet (first time the tab is classified).
- When the document becomes active (transitions from inactive to active), force a fresh computation on the next classification pass so the user sees up-to-date results as soon as they switch to the tab.
3. **Wire up `AdjustStaleClassification`.** Use the currently no-op `AdjustStaleClassification` member as the natural place to return the last-known classification when a fresh classification pass is skipped/deferred for a non-active document, aligning with how Roslyn's own classification pipeline expects stale data to be surfaced while a real computation is pending or skipped.
4. **Cache lifetime tuning.** With inactive tabs no longer forcing recomputation on the existing 2-minute cadence, consider extending `openedDocumentsSemanticClassificationCache`'s effective lifetime for inactive documents (or splitting it into an "active" cache with the current short TTL and a longer-lived "background/open" cache), since inactive tabs no longer need to be kept warm at the same frequency as the active document.
5. **No behavior change for the active document.** The active document should continue to get fully up-to-date semantic classification exactly as today — this change only relaxes freshness requirements for open-but-not-visible tabs.
## Impact
Reduces background FCS typecheck churn proportional to the number of open-but-inactive tabs in a solution, without changing the classification experience for the document the user is actually viewing. Classification for an inactive tab may be momentarily stale immediately after switching to it, but this is consistent with Roslyn's own `AdjustStaleClassification` design intent (show something reasonable immediately, refresh shortly after).
## Related
- Continuation of the background-activity-minimization effort tracked in `docs/ide/background-activity-minimization-plan.md`.
- Same active-document-only pattern already applied to `UnusedOpensDiagnosticAnalyzer`, `UnusedDeclarationsAnalyzer`, `SimplifyNameDiagnosticAnalyzer`, and `FSharpInlayHintsService`.
Contributor guide
Research direction
Start in vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs at AddSemanticClassificationsAsync and the AdjustStaleClassification member, then compare ActiveDocumentDetection.fs and the related analyzer and inlay-hint usage. Review the cache behavior and background-activity-minimization-plan.md; done means inactive open documents can use stale data without repeated typechecks while the active document remains fully up to date.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- fsharp
- Domain
- developer-experience, performance
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100