Summary
FSharp.Editor now watches on-disk -r: reference assemblies via IVsFileChangeEx (see FileChangeWatcher.fs, FileChangeWatcherHub, and its consumers in FSharpProjectOptionsManager.fs and WorkspaceExtensions.fs) and invalidates caches reactively instead of polling File.GetLastWriteTimeUtc on every comparison.
However, when a watched reference assembly changes on disk, the current behavior is a coarse, whole-project invalidation:
FSharpProjectOptionsReactor (FSharpProjectOptionsManager.fs): cache.TryRemove(projectId) drops the entire cached (Project * FSharpParsingOptions * FSharpProjectOptions) tuple for the project, forcing a full recomputation of project options from scratch on next request.
- Transparent-compiler snapshot path (
WorkspaceExtensions.fs): latestSnapshots.TryRemove(project.Id) drops the entire cached FSharpProjectSnapshot, forcing FSharpProjectSnapshot.FromOptions to rebuild the whole snapshot (all file snapshots, not just the changed reference) on next request.
This is architecturally different from how Roslyn handles the equivalent scenario.
How Roslyn does it
Roslyn's VisualStudioMetadataReferenceManager / FileChangeWatcher infrastructure (src/VisualStudio/Core/Def/ProjectSystem/VisualStudioMetadataReferenceManager.cs and related FileWatchedReferenceFactory<T> types) watches individual metadata reference files (PE references) and, on change, invalidates only the cached PortableExecutableReference/MetadataReference snapshot for that specific file (keyed by path in a ReferenceCountedDisposable / weak-reference cache). Roslyn's immutable Solution/Project/Compilation snapshots are structured so that a single reference can be swapped:
Project.WithMetadataReferences(...) (and the underlying Compilation.ReplaceReference / Compilation.WithReferences) only replaces the one MetadataReference whose backing file changed; the rest of the Compilation (syntax trees, other references, symbol caches for unrelated assemblies) is structurally shared and reused.
- Because
Compilation is designed around incremental, reference-level substitution as a first-class operation, invalidating one reference does not force Roslyn to re-parse/re-bind the whole project — only re-resolve/re-bind against the one updated reference.
Why we don't do the same today
FSharpProjectOptions and FSharpProjectSnapshot are immutable value-like structures without a supported "replace this one reference and keep everything else structurally shared" API. FSharpChecker/FCS's incremental checking model is keyed off the whole FSharpProjectOptions/ProjectSnapshot identity (OtherOptions array, ReferencedProjects, etc.), not per-reference substitution. Introducing point-in-place reference updates would require:
- A stable, addressable identity for each metadata/reference-assembly entry inside
FSharpProjectOptions/ProjectSnapshot (today they're just -r: strings in OtherOptions, or FSharpReferencedProject values without independent cache keys).
- Incremental-checking support in FCS's
IncrementalBuilder/TransparentCompiler for substituting a single reference without invalidating the whole project's type-checking state (bound signature data, checked file results, etc. that depend on the reference set as a whole).
- Care around cross-project referencing (
AreFSharpInMemoryCrossProjectReferencesEnabled) where downstream projects' checked state also depends on the changed reference.
This is a change to the core compiler/FCS incremental-checking model, not something that can be done from the editor layer alone (vsintegration/src/FSharp.Editor).
Expected performance win
For solutions with many/large projects referencing a handful of frequently-rebuilt assemblies (e.g. multi-project solutions where a shared library is rebuilt in a loop during active development, or NuGet package caches refreshed on disk), whole-project invalidation means:
- Every keystroke-triggered background check after an external rebuild re-parses and re-type-checks the entire project (all source files), instead of just re-resolving symbols against the one updated assembly.
- For large projects (hundreds of files), this can mean seconds of unnecessary recomputation on every reference-assembly rebuild, compared to Roslyn's near-instantaneous reference swap for C#/VB projects of comparable size.
- This especially affects "F5 workflows" where a referenced class library is rebuilt frequently while iterating on a consuming F# project open in the IDE.
Proposed follow-up
Track this as a separate, larger effort (likely spanning FSharp.Compiler.Service's IncrementalBuilder/TransparentCompiler and the FSharpProjectSnapshot/ProjectSnapshot model) to support per-reference invalidation/substitution, so the editor-layer watcher introduced in FileChangeWatcher.fs can eventually trigger a targeted reference swap instead of a full project options/snapshot rebuild.
Related
vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs
vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs
vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs
src/Compiler/Service/FSharpProjectSnapshot.fs
Summary
FSharp.Editornow watches on-disk-r:reference assemblies viaIVsFileChangeEx(seeFileChangeWatcher.fs,FileChangeWatcherHub, and its consumers inFSharpProjectOptionsManager.fsandWorkspaceExtensions.fs) and invalidates caches reactively instead of pollingFile.GetLastWriteTimeUtcon every comparison.However, when a watched reference assembly changes on disk, the current behavior is a coarse, whole-project invalidation:
FSharpProjectOptionsReactor(FSharpProjectOptionsManager.fs):cache.TryRemove(projectId)drops the entire cached(Project * FSharpParsingOptions * FSharpProjectOptions)tuple for the project, forcing a full recomputation of project options from scratch on next request.WorkspaceExtensions.fs):latestSnapshots.TryRemove(project.Id)drops the entire cachedFSharpProjectSnapshot, forcingFSharpProjectSnapshot.FromOptionsto rebuild the whole snapshot (all file snapshots, not just the changed reference) on next request.This is architecturally different from how Roslyn handles the equivalent scenario.
How Roslyn does it
Roslyn's
VisualStudioMetadataReferenceManager/FileChangeWatcherinfrastructure (src/VisualStudio/Core/Def/ProjectSystem/VisualStudioMetadataReferenceManager.csand relatedFileWatchedReferenceFactory<T>types) watches individual metadata reference files (PE references) and, on change, invalidates only the cachedPortableExecutableReference/MetadataReferencesnapshot for that specific file (keyed by path in aReferenceCountedDisposable/ weak-reference cache). Roslyn's immutableSolution/Project/Compilationsnapshots are structured so that a single reference can be swapped:Project.WithMetadataReferences(...)(and the underlyingCompilation.ReplaceReference/Compilation.WithReferences) only replaces the oneMetadataReferencewhose backing file changed; the rest of theCompilation(syntax trees, other references, symbol caches for unrelated assemblies) is structurally shared and reused.Compilationis designed around incremental, reference-level substitution as a first-class operation, invalidating one reference does not force Roslyn to re-parse/re-bind the whole project — only re-resolve/re-bind against the one updated reference.Why we don't do the same today
FSharpProjectOptionsandFSharpProjectSnapshotare immutable value-like structures without a supported "replace this one reference and keep everything else structurally shared" API.FSharpChecker/FCS's incremental checking model is keyed off the wholeFSharpProjectOptions/ProjectSnapshotidentity (OtherOptionsarray,ReferencedProjects, etc.), not per-reference substitution. Introducing point-in-place reference updates would require:FSharpProjectOptions/ProjectSnapshot(today they're just-r:strings inOtherOptions, orFSharpReferencedProjectvalues without independent cache keys).IncrementalBuilder/TransparentCompilerfor substituting a single reference without invalidating the whole project's type-checking state (bound signature data, checked file results, etc. that depend on the reference set as a whole).AreFSharpInMemoryCrossProjectReferencesEnabled) where downstream projects' checked state also depends on the changed reference.This is a change to the core compiler/FCS incremental-checking model, not something that can be done from the editor layer alone (
vsintegration/src/FSharp.Editor).Expected performance win
For solutions with many/large projects referencing a handful of frequently-rebuilt assemblies (e.g. multi-project solutions where a shared library is rebuilt in a loop during active development, or NuGet package caches refreshed on disk), whole-project invalidation means:
Proposed follow-up
Track this as a separate, larger effort (likely spanning
FSharp.Compiler.Service'sIncrementalBuilder/TransparentCompilerand theFSharpProjectSnapshot/ProjectSnapshotmodel) to support per-reference invalidation/substitution, so the editor-layer watcher introduced inFileChangeWatcher.fscan eventually trigger a targeted reference swap instead of a full project options/snapshot rebuild.Related
vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fsvsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fsvsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fssrc/Compiler/Service/FSharpProjectSnapshot.fs