Skip to content

Make SourceTextData thread-safe with ConcurrentDictionary - #20113

Open
xperiandri wants to merge 5 commits into
dotnet:mainfrom
xperiandri:copilot/fix-source-text-data-thread-safety
Open

Make SourceTextData thread-safe with ConcurrentDictionary#20113
xperiandri wants to merge 5 commits into
dotnet:mainfrom
xperiandri:copilot/fix-source-text-data-thread-safety

Conversation

@xperiandri

Copy link
Copy Markdown
Contributor

SourceTextData is shared across concurrent editor operations (classification/tagging and symbol lookup), but its ResizeArray backing store is not thread-safe.

Changes

  • Replace ResizeArray<SourceLineData option> with ConcurrentDictionary<int, SourceLineData>.
  • Keep SourceTextData.[i] contract as SourceLineData option using TryGetValue in getter.
  • Setter stores Some v and removes on None with TryRemove.
  • ClearFrom(n) now removes consecutive cached entries using TryRemove and stops on first missing index.
  • Add a short note in code explaining concurrent access requirement.

This is the same fix prepared in my fork branch copilot/fix-source-text-data-thread-safety.

@xperiandri

Copy link
Copy Markdown
Contributor Author

Fixes #20112

@xperiandri xperiandri changed the title Make SourceTextData thread-safe with ConcurrentDictionary Make SourceTextData thread-safe with ConcurrentDictionary Aug 2, 2026
@github-actions github-actions Bot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label Aug 2, 2026
i <- i + 1
while cont do
let removed, _ = data.TryRemove(i)
if removed then i <- i + 1 else cont <- false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking observation: ConcurrentDictionary makes each individual entry access atomic, which fixes the memory-safety bug. However, the multi-line read-modify-write sequence in getFromRefreshedTokenCache (read [i] -> scanSourceLine -> set [i], chaining lexState, then ClearFrom(endLine+1)) is still not atomic as a whole. Two concurrent scans over overlapping line ranges on the same SourceTextData can interleave and briefly cache an inconsistent lex-state chain. This is a strict improvement over the old code (which could corrupt the list), and it self-heals on the next read via the IsValid/LexStateAtStartOfLine checks, so it is almost certainly acceptable. Worth noting only that the guarantee is container-level, not sequence-level, so the must be thread-safe comment does not imply a coherent snapshot across lines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — updated the comment above SourceTextData to clarify the guarantee is container-level (per-entry atomicity via ConcurrentDictionary), not a coherent snapshot across a range of lines. Concurrent overlapping scans can still interleave, but the cache self-heals on the next read via the IsValid/LexStateAtStartOfLine checks, as you noted. No functional change needed beyond the comment wording.

@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Aug 3, 2026
@T-Gro
T-Gro self-requested a review August 3, 2026 19:22
Comment thread vsintegration/src/FSharp.Editor/LanguageService/Tokenizer.fs Outdated
@xperiandri
xperiandri force-pushed the copilot/fix-source-text-data-thread-safety branch from 8bee41a to bb0043a Compare August 17, 2026 01:28
@github-actions

github-actions Bot commented Aug 17, 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 copilot/fix-source-text-data-thread-safety branch from bb0043a to 84f9fe8 Compare August 17, 2026 01:48
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added ⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure labels Aug 17, 2026
@xperiandri

Copy link
Copy Markdown
Contributor Author

Pushed another round of improvements building on the thread-safety fix:

FSharpLineTokenizer.ScanToken allocation-free voption conversion (commit 7b724e470)

  • Changed the public signature from ScanToken: lexState -> FSharpTokenInfo option * FSharpTokenizerLexState to ScanToken: lexState -> struct (FSharpTokenInfo voption * FSharpTokenizerLexState) in ServiceLexing.fsi/ServiceLexing.fs.
  • This removes a per-token option heap allocation on the hot tokenization path (every single token scanned during classification, brace matching, and IntelliSense previously boxed a Some/None).
  • Updated every in-tree caller to match:
    • FSharpChecker.TokenizeLine (service.fs) — rewritten with a small scanNext () helper feeding the existing [| while ... do yield ... |] loop, avoiding the earlier fst/snd mutable-tuple indirection while staying close to the original shape.
    • editor Tokenizer.fs — pattern-matches on voption directly.
    • deprecated FSharp.LanguageService Colorize.fs/Intellisense.fsScanTokenWithDetails/ScanTokenAndProvideInfoAboutIt and the recursive token-scanning helpers now match on ValueSome/ValueNone.
  • Incidental: normalized trailing whitespace in Intellisense.fs that formatting picked up along the way (no behavior change).
  • Added/updated TokenizerTests.fs coverage for the new ScanToken shape.
  • Release notes entries added under ### Improved and ### Changed in docs/release-notes/.FSharp.Compiler.Service/11.0.100.md.

All affected projects (FSharp.Compiler.Service, FSharp.LanguageService) build cleanly with these changes.

xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 17, 2026
@xperiandri
xperiandri requested a review from T-Gro August 17, 2026 02:10
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 17, 2026
@xperiandri
xperiandri force-pushed the copilot/fix-source-text-data-thread-safety branch from 4ff5e30 to c20c864 Compare August 17, 2026 02:11
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 17, 2026
@xperiandri
xperiandri force-pushed the copilot/fix-source-text-data-thread-safety branch from c20c864 to 287c161 Compare August 17, 2026 11:33

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One note on public API compatibility.

type FSharpLineTokenizer =
/// Scan one token from the line
member ScanToken: lexState: FSharpTokenizerLexState -> FSharpTokenInfo option * FSharpTokenizerLexState
member ScanToken: lexState: FSharpTokenizerLexState -> struct (FSharpTokenInfo voption * FSharpTokenizerLexState)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖🕵️ Changing ScanToken to return struct (... voption * ...) breaks source and binary compatibility for every existing caller of this public member.

member ScanToken: lexState -> struct (FSharpTokenInfo voption * FSharpTokenizerLexState)

Keep the existing member and add a separate allocation-free member for hot paths.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is our strategy on allocation-free code?
Ideally, all the code must return value tuples, unless a component of a tuple is a huge struct.
Would you like all such changes to be additions instead of replacements?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@T-Gro could you direct me what to do with this?

xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 20, 2026
@xperiandri
xperiandri force-pushed the copilot/fix-source-text-data-thread-safety branch from 287c161 to 4b3e2af Compare August 20, 2026 14:39
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Aug 21, 2026
@xperiandri
xperiandri force-pushed the copilot/fix-source-text-data-thread-safety branch from 4b3e2af to b5b7fcb Compare August 21, 2026 14:37
@xperiandri
xperiandri requested a review from T-Gro August 21, 2026 14:38
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Sep 3, 2026
@xperiandri
xperiandri force-pushed the copilot/fix-source-text-data-thread-safety branch from b5b7fcb to 4c499e3 Compare September 3, 2026 17:29
Comment thread vsintegration/src/FSharp.Editor/LanguageService/Tokenizer.fs
Copilot AI and others added 4 commits September 9, 2026 17:26
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
- Reuse already-materialized line contents in SourceTextData cache
  lookups instead of re-stringifying the line via IsValid
- processToken now takes the token as a parameter instead of reading
  a mutable voption repeatedly; scanAndColorNextToken returns whether
  a token was scanned, removing the tokenInfoOption mutable
- tokensUnderCursor uses List.filter instead of a list comprehension
- Hoist forbiddenSymbolNameChars to a module-level private array so
  it is not recreated on every isValidNameForSymbol call
…ct return

Change ScanToken's signature from 'FSharpTokenInfo option * FSharpTokenizerLexState' to 'struct (FSharpTokenInfo voption * FSharpTokenizerLexState)', eliminating a per-token option allocation on the hot tokenization path. Update all in-tree callers: FSharpChecker.TokenizeLine, the editor Tokenizer, and the deprecated FSharp.LanguageService colorizer/scanner (Colorize.fs, Intellisense.fs). Also clean up incidental trailing whitespace in Intellisense.fs picked up by formatting.
@xperiandri
xperiandri force-pushed the copilot/fix-source-text-data-thread-safety branch from 4c499e3 to 3d32352 Compare September 9, 2026 15:57
@github-actions

This comment has been minimized.

… caught

Address review comment: add a concurrent test against one shared
SourceTextData, covering overlapping reads and invalidation.

The test alternates classifySpans/getSymbolAtPosition calls across many
threads on one document, interleaving two text versions of it (one with
an unclosed block comment) so cache invalidation is exercised alongside
concurrent reads.

It reliably reproduced a real bug in getFromRefreshedTokenCache: after
walking back to find the nearest valid cache entry at scanStartLine, the
resume lex state was read from scanStartLine - 1 - one entry lower than
the one just validated, and never checked itself. Under concurrent scans
of different text on the same document, that neighboring entry can be
stale, so classification would carry over a wrong lex state (typically
"still inside a comment"), turning code after that point into one giant
comment span, or occasionally throwing on a missing entry (silently
swallowed by Assert.Exception, dropping the classification for that
call). Fixed by reading LexStateAtStartOfLine off scanStartLine itself,
the entry the preceding loop already confirmed valid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Bootstrap, Affects-Build-Infra
Affects-Bootstrap: Compiler service changes are part of the bootstrap chain.
Affects-Build-Infra: Compiler source and project build inputs are modified.

Generated by PR Tooling Safety Check · gpt56 2.6M ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure AI-reviewed PR reviewed by AI review council 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.

3 participants