Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/release-notes/.VisualStudio/18.vNext.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
* Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421))
* Fix doubled F# diagnostics in tooltips. ([Issue #16360](https://github.com/dotnet/fsharp/issues/16360))
* Fix `NotSupportedException` in the memory-mapped-file optimization when copying `ReadOnlyMemory` into `MemoryMappedFileViewStream`. ([Issue #20263](https://github.com/dotnet/fsharp/issues/20263))
* Find All References and Rename search each file of a multi-targeted F# project once instead of once per target framework, revisiting a file in another instance only when it is compiled there alone or under conditional compilation; searches start as each project's snapshot is ready and type checks are bounded across the solution. ([PR #20464](https://github.com/dotnet/fsharp/pull/20464))
* Find Implementations no longer searches the whole solution for uses it then discards; it reports the declarations alone.
* Find All References reports the uses of a document in one go instead of starting a task per reference, stops when the request is cancelled, and searches the documents of a project through a fixed set of workers under one solution-wide budget that leaves a core to the editor.

### Changed

Expand Down
25 changes: 25 additions & 0 deletions vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,31 @@ module CancellableTasks =
return! allTask
}

/// Runs the work over the items with at most maxDegreeOfParallelism of them in flight. A worker
/// takes the next item when it frees up, so cancellation cancels the workers rather than a
/// pending task per item.
let forEachThrottled maxDegreeOfParallelism (work: 'T -> CancellableTask<unit>) (items: 'T seq) =
cancellableTask {
let! ct = getCancellationToken ()
let items = Seq.toArray items
let mutable next = -1

let worker () =
backgroundTask {
let mutable index = Interlocked.Increment &next

while index < items.Length do
ct.ThrowIfCancellationRequested()
do! work items[index] ct
index <- Interlocked.Increment &next
}

let workers =
Array.init (min (max 1 maxDegreeOfParallelism) items.Length) (fun _ -> worker ())

do! (Task.WhenAll workers :> Task)
}

let inline whenAllTasks (tasks: CancellableTask seq) =
cancellableTask {
let! ct = getCancellationToken ()
Expand Down
161 changes: 113 additions & 48 deletions vsintegration/src/FSharp.Editor/LanguageService/SymbolHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ open System.Collections.Concurrent
open System.Collections.Generic
open System.Collections.Immutable
open System.IO
open System.Threading
open System.Threading.Tasks

open Microsoft.CodeAnalysis
Expand Down Expand Up @@ -70,34 +71,103 @@ module internal SymbolHelpers =
return symbolUses
}

let getSymbolUsesInProjects (symbol: FSharpSymbol, projects: Project list, onFound: Document -> range -> CancellableTask<unit>) =
match projects with
/// Ranks the target-framework instances of a project file: the one the current document lives in,
/// then those in its dependency closure, whose references resolve the symbol the same way.
let private rankInstances (currentProject: Project) =
let graph = currentProject.Solution.GetProjectDependencyGraph()

let related =
HashSet
[
yield! graph.GetProjectsThatThisProjectTransitivelyDependsOn currentProject.Id
yield! graph.GetProjectsThatTransitivelyDependOnThisProject currentProject.Id
]

fun (project: Project) ->
if project.Id = currentProject.Id then 0
elif related.Contains project.Id then 1
else 2

/// One search per project file: the best-ranked target-framework instance is searched in full,
/// the others only where their sources can differ from it.
let private groupInstances (currentProject: Project) (projects: Project list) =
let rank = rankInstances currentProject

projects
|> List.groupBy _.FilePath
|> List.map (fun (_, instances) ->
let ordered = List.sortBy rank instances
struct (ordered.Head, ordered.Tail))

/// One core is left to the thread that has to stay responsive, and every search in the editor
/// shares what remains.
let searchThrottle = new SemaphoreSlim(max 1 (Environment.ProcessorCount - 1))

let getSymbolUsesInProjects
(symbol: FSharpSymbol, currentProject: Project, projects: Project list, onFound: Document -> range seq -> CancellableTask<unit>)
=
match projects |> List.filter _.IsFSharp with
| [] -> CancellableTask.singleton ()
| firstProject :: _ ->
| firstProject :: _ as projects ->
let isFastFindReferencesEnabled = firstProject.IsFastFindReferencesEnabled

// TODO: this needs to use already boxed boolean instead of boxing it every time.
let props =
[| nameof isFastFindReferencesEnabled, isFastFindReferencesEnabled :> obj |]

let groups =
if isFastFindReferencesEnabled then
groupInstances currentProject projects
else
[ for project in projects -> struct (project, []) ]

cancellableTask {
// TODO: this needs to be a single event with a duration
TelemetryReporter.ReportSingleEvent(TelemetryEvents.GetSymbolUsesInProjectsStarted, props)

let! ct = CancellableTask.getCancellationToken ()

// A file that several projects compile - the target-framework instances of one project
// file, or two project files sharing a source file - is searched in each of them and
// reports the same range every time. The range carries its file, so the first project
// to report a use keeps it and the rest are dropped.
let reported = ConcurrentDictionary<range, unit>()

let onFound document ranges =
let fresh =
ranges |> Seq.filter (fun range -> reported.TryAdd(range, ())) |> Seq.toArray

if fresh.Length = 0 then
CancellableTask.singleton ()
else
onFound document fresh
// Mutated by the checker while a snapshot is built, so snapshots are built one at a time.
let snapshotAccumulator = Dictionary()
let searches = ResizeArray<Task>()

let snapshotFor (project: Project) =
if project.UseTransparentCompiler then
project.GetFSharpProjectSnapshot snapshotAccumulator
|> CancellableTask.map ValueSome
else
CancellableTask.singleton ValueNone

let! projects =
projects
|> Seq.map (fun project ->
project.GetFSharpProjectSnapshot(snapshotAccumulator)
|> CancellableTask.map (fun s -> project, s))
|> CancellableTask.sequential
let start (project: Project) snapshot searchedInstance =
searches.Add(
project.FindFSharpReferencesAsync
(symbol, snapshot, searchedInstance, searchThrottle, onFound, "getSymbolUsesInProjects")
ct
)

do!
projects
|> Seq.map (fun (project, snapshot) ->
project.FindFSharpReferencesAsync(symbol, snapshot, onFound, "getSymbolUsesInProjects"))
|> CancellableTask.whenAll
for struct (primary, secondaries) in groups do
let! snapshot = snapshotFor primary
start primary snapshot ValueNone

for secondary in secondaries do
let! snapshot = snapshotFor secondary
start secondary snapshot (ValueSome primary)

do! Task.WhenAll searches

TelemetryReporter.ReportSingleEvent(TelemetryEvents.GetSymbolUsesInProjectsFinished, props)
}
Expand All @@ -106,7 +176,7 @@ module internal SymbolHelpers =
(symbolUse: FSharpSymbolUse)
(currentDocument: Document)
(checkFileResults: FSharpCheckFileResults)
(onFound: Document -> range -> CancellableTask<unit>)
(onFound: Document -> range seq -> CancellableTask<unit>)
=
cancellableTask {
match symbolUse.GetSymbolScope currentDocument with
Expand All @@ -115,10 +185,7 @@ module internal SymbolHelpers =
let symbolUses =
checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol, relatedSymbolKinds = RelatedSymbolUseKind.All)

do!
symbolUses
|> Seq.map (fun symbolUse -> onFound currentDocument symbolUse.Range)
|> CancellableTask.whenAll
do! onFound currentDocument (symbolUses |> Seq.map _.Range)

| Some SymbolScope.SignatureAndImplementation ->
let otherFile = getOtherFile currentDocument.FilePath
Expand All @@ -132,52 +199,50 @@ module internal SymbolHelpers =
}
| ValueNone -> CancellableTask.singleton []

let symbolUses =
(checkFileResults, currentDocument) :: otherFileCheckResults
|> Seq.collect (fun (checkFileResults, doc) ->
for checkFileResults, doc in (checkFileResults, currentDocument) :: otherFileCheckResults do
let symbolUses =
checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol, relatedSymbolKinds = RelatedSymbolUseKind.All)
|> Seq.map (fun symbolUse -> (doc, symbolUse.Range)))

do! symbolUses |> Seq.map ((<||) onFound) |> CancellableTask.whenAll
do! onFound doc (symbolUses |> Seq.map _.Range)

| scope ->
| Some(SymbolScope.Projects(scopeProjects, isLocalForProject)) ->
let projectsToCheck =
match scope with
| Some(SymbolScope.CurrentDocument)
| Some(SymbolScope.SignatureAndImplementation) ->
// For current document or signature/implementation, just search current project
[ currentDocument.Project ]
| Some(SymbolScope.Projects(scopeProjects, false)) ->
if isLocalForProject then
scopeProjects
else
[
for scopeProject in scopeProjects do
yield scopeProject
yield! scopeProject.GetDependentProjects()
]
|> List.distinct
| Some(SymbolScope.Projects(scopeProjects, true)) -> scopeProjects
// The symbol is declared in .NET framework, an external assembly or in a C# project within the solution.
// Optimization: Only search projects that reference the specific assembly
| None ->
match symbolUse.Symbol.Assembly.FileName with
| Some assemblyPath ->
let referencingProjects =
ProjectFiltering.getProjectsReferencingAssembly assemblyPath currentDocument.Project.Solution

if List.isEmpty referencingProjects then
Seq.toList currentDocument.Project.Solution.Projects
else
referencingProjects
| None -> Seq.toList currentDocument.Project.Solution.Projects

do! getSymbolUsesInProjects (symbolUse.Symbol, projectsToCheck, onFound)

do! getSymbolUsesInProjects (symbolUse.Symbol, currentDocument.Project, projectsToCheck, onFound)

// The symbol is declared in .NET framework, an external assembly or in a C# project within the solution.
// Optimization: Only search projects that reference the specific assembly
| None ->
let projectsToCheck =
match symbolUse.Symbol.Assembly.FileName with
| Some assemblyPath ->
match ProjectFiltering.getProjectsReferencingAssembly assemblyPath currentDocument.Project.Solution with
| [] -> Seq.toList currentDocument.Project.Solution.Projects
| referencingProjects -> referencingProjects
| None -> Seq.toList currentDocument.Project.Solution.Projects

do! getSymbolUsesInProjects (symbolUse.Symbol, currentDocument.Project, projectsToCheck, onFound)
}

let getSymbolUses (symbolUse: FSharpSymbolUse) (currentDocument: Document) (checkFileResults: FSharpCheckFileResults) =
cancellableTask {
let symbolUses = ConcurrentBag()

let onFound =
fun document range -> cancellableTask { symbolUses.Add(document, range) }
fun document (ranges: range seq) ->
cancellableTask {
for range in ranges do
symbolUses.Add(document, range)
}

do! findSymbolUses symbolUse currentDocument checkFileResults onFound

Expand Down
Loading
Loading