Skip to content

Share one whole-file classification between requests for a document version - #20452

Open
xperiandri wants to merge 3 commits into
dotnet:mainfrom
xperiandri:classification-shared-requests
Open

Share one whole-file classification between requests for a document version#20452
xperiandri wants to merge 3 commits into
dotnet:mainfrom
xperiandri:classification-shared-requests

Conversation

@xperiandri

Copy link
Copy Markdown
Contributor

Description

Since #20450 the semantic classification of an open document covers the whole file, so its cost is proportional to the file rather than to the viewport — on an 8000-line file one pass takes about a second and a half in Release. Two things made that pass run far more often than the text changed.

The entry expired after two seconds. It lived in a MemoryCache with a sliding expiration, so the first scroll after any pause missed it and walked the file again for a text version that had already been classified. The entry now lives in the per-document store that the last-known-good fallback already used, keyed by text version and kept for the life of the document; the separate opened-documents DocumentCache goes away.

Overlapping requests each walked the file. Roslyn's taggers above and below the viewport, or a split view, ask for the same version at once. Requests for one version now join a single in-flight computation (InFlightClassification): it starts with the first of them and is cancelled only when the last one leaves, so a superseded request never cancels the work its neighbours still wait for, and no request outlives its own token.

Both are covered by tests: one for sharing, one for the cancel-on-last-leave rule. The classification service tests that asserted on the removed cache now assert on the per-document store.

Depends on #20450 — it is the head of this branch. Only the last commit, "Share one whole-file classification between requests for a version", belongs to this pull request; please review it after #20450 merges.

No benchmark is attached: the change removes repeated executions of an existing pass rather than altering the pass itself, and the per-pass cost quoted above was measured with the tree's own FCS on a generated 8002-line file (69,330 classification items, 1483 ms per whole-file pass in Release).

Checklist

  • Test cases added
  • Performance benchmarks added in case of performance changes
  • Release notes entry updated

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md

@xperiandri
xperiandri force-pushed the classification-shared-requests branch from e6a1910 to ea8a3e7 Compare September 4, 2026 17:48
@github-actions github-actions Bot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label Sep 4, 2026
xperiandri and others added 3 commits September 5, 2026 00:10
Every entry point in WorkspaceExtensions signalled "the project has no
options yet" by raising an OperationCanceledException, which the callers
then had to tell apart from a real cancellation. They cannot, so they
treat both as "cancelled" and return an empty result. For a service
Roslyn asks repeatedly while a solution loads, that is a wrong answer,
not a missing one.

Add `Try` siblings that return ValueNone instead, and keep the raising
members as thin wrappers so the call sites that have not moved over
still get the same exception with the same message.

The four-tuple those members hand out becomes a named record, which is
also what ProjectCache stores, so a cache hit hands back the instance it
holds rather than rebuilding a tuple.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Splitting the semantic classification cache in two (dotnet#15954) left the
open-document branch reading the opened-documents cache and writing the
unopened one, so the opened cache was never populated and every request
for an open file re-ran the checker. What it wrote was also only the
requested span, keyed by document and text version, so a second request
for another span of the same version (scrolling, a split view, Roslyn
asking around the viewport) would have hit that entry and sliced nothing
out of it.

Cache the classification of the whole file instead, and slice it per
request, the way the unopened-documents branch already does.

A check that cannot complete - the project is loading or reloading, or
the check was superseded - must not answer with no classifications
either: Roslyn replaces the tags of a span with whatever comes back, so
an empty answer strips the colours the user is looking at, while a
cancellation carrying Roslyn's own token leaves them alone. Keep the
last classification per document and re-emit it for those requests, but
only while the text it was computed from is still the current one, as
its spans would otherwise land on the wrong characters.

`ifCanceledThen` replaces `ifCanceledReturn ()` for the same reason: the
checker relabels its internal cancellations with the caller's token, and
those must produce the last known colours rather than none.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The classification of an open document covers the whole file, so what
it costs is proportional to the file, not to the viewport: on an
8000-line file a single pass takes about a second and a half. Two
things made that pass run far more often than the text changed.

The entry lived in a MemoryCache with a two-second sliding expiration,
so the first scroll after any pause missed it and walked the file
again for a text version that had already been classified. The entry
now lives in the per-document store that the last-known-good fallback
already used, keyed by text version and kept for the life of the
document, and the separate opened-documents cache goes away.

Requests that overlapped - Roslyn's taggers above and below the
viewport, a split view - each walked the file on their own. Requests
for the same version now join one in-flight computation; it starts
with the first of them and is cancelled only when the last one leaves,
so a superseded request never cancels the work its neighbours are
still waiting for, and no request outlives its own token.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the classification-shared-requests branch from ea8a3e7 to 5c70d1e Compare September 4, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

1 participant