Search each file of a multi-targeted F# project once in Find All References and Rename - #20464
Open
xperiandri wants to merge 9 commits into
Open
Search each file of a multi-targeted F# project once in Find All References and Rename#20464xperiandri wants to merge 9 commits into
xperiandri wants to merge 9 commits into
Conversation
…r tests Test helpers so far put every synthetic file into one Roslyn project. CreateMultiProjectSolution creates one project per synthetic project with project references, the way VS wires project-to-project references; CreateMultiTargetSolution creates one project per target instance sharing the project path and the document paths, the way VS loads a multi-targeted project. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The IFSharpFindUsagesContext stub of FindReferencesTests moves to RoslynTestHelpers.CreateFindUsagesContext so other test files can collect the definitions and references a search reports. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the solution Find All References and Rename searched every target-framework instance of a multi-targeted F# project in full, and built the snapshots of all projects in scope before the first search started. Instances of one project file are now grouped: the instance of the current document (or one in its dependency closure) is searched in full, the others only for files compiled solely there and for files with conditional compilation directives, whose sources can differ between instances. Searches start as soon as a project's snapshot is ready, snapshots are built only for the transparent compiler, and one SemaphoreSlim bounds the concurrent file checks across the whole search instead of per project. With EnableFastFindReferencesAndRename off every instance is still searched in full; the concurrency bound then replaces the previous per-project sequential loop. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One project loaded as two instances (one without FOO and without the fourth file, one with both): every file is reported once, and Rename gets one document per file, owned by an instance that compiles it. The fixture lives in its own file: the modules of one file share a static initializer, so a second fixture module makes the first module's values observable before they are assigned when xunit runs the classes in parallel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
3 tasks
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…editor Three things the search did on every request: it walked the solution for Find Implementations, which reports no uses at all and threw the result away; it started a task per reference found, each fetching the document's text again; and it swallowed the cancellation Roslyn raises when the user closes the window or starts another search, so the search ran on. The callback now takes the uses of a document at once, so its text is read once and the reports go out in order, and cancellation propagates. The documents of a project go through a fixed set of workers instead of a task per document parked on the throttle, and the throttle itself is one budget for the whole editor, one core smaller than the machine, so the thread drawing the results keeps one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Searching each file of a multi-targeted project once left two holes, and a solution that uses conditional compilation widely fell through both: rename and Find All References reported the same use once per target framework. A secondary instance re-searched a file whenever it held any conditional directive, so an inactive `#if DEBUG` was enough to search it again under every target framework. Only the defines the two instances disagree on can make a shared file parse differently, so the directives are now read for the idents they test and the file is re-searched only when one of those defines is in that difference. Instances whose defines match skip their shared files outright, which also spares the checker the project builds those searches would force. The uses themselves were never deduplicated on the F# side, so a file two projects compile - the instances of one project file, or two project files sharing a source file - reported its every use twice over. A range carries its file, so the first project to report one keeps it. The test project gained a use beside the `#if FOO` block and a file guarded by a define both instances share: its only conditional use used to sit inside the disabled branch, which is why the duplicates went unnoticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Grouping the target-framework instances of a project file sorted each group to read its head, though what follows the head is never ordered: every secondary instance is searched the same way. One pass for the best-ranked instance says that outright, and `Array.minBy` keeps the first of equal rank, which is what the stable sort put at the head. Sorting the whole solution once and letting the grouping keep that order reads better still, but it measures worse - 86 instances over 26 project files, on net472: 14.5 us and 16.1 KB for the pass per group against 17.2 us and 17.4 KB for the single sort, and 15.0 us and 16.7 KB for the list-and-sort this replaces. Neither number matters next to the project checks the grouping schedules; the sort is dropped because it is not paid for. `start` becomes `startSearching`, with the reason it is not awaited written down, and the callback that collects the uses becomes a function rather than a lambda bound to a name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Find All References and Rename on a symbol of a multi-targeted F# project did every piece of work once per target framework. Measured in the debugger on a solution with 135 project instances: a scope of two projects (the two instances of one
.fsproj) grew to 86 instances of 26 project files after adding dependents, 1246 documents in all; the snapshots of all 86 were built one after another before the first search started; every file was type checked once per instance, and FCS never shares those checks between instances; Roslyn's window hides the duplicate results, so the user only sees the time. On top of that the "skip the files before the declaration" optimisation only applied in the instance owning the firstDocumentIdof the declaration, becauseProject.FindFSharpReferencesAsynccompareddocument.Project = thisby reference.Change (
SymbolHelpers.fs,WorkspaceExtensions.fs):getSymbolUsesInProjectskeeps F# projects only (which also stopsgetFSharpOptionsForProjectfrom being asked about a C# project in the external-symbol branch) and, withEnableFastFindReferencesAndRenameon (the default), groups the instances of one project file. The instance searched in full is the one of the current document, else one in its dependency closure, else the first; the other instances only search files compiled solely there and files whose conditional compilation tests a define the two instances disagree on. Only that difference can make a shared file parse differently, so the#ifexpressions (ParsedInput.Trivia.ConditionalDirectives) are read for the idents they test rather than merely counted: an inactive#if DEBUGno longer forces a second search, and instances whose defines match skip their shared files outright, which also spares the checker the project builds those searches would force. Uses are reported once per range across the whole search, so a file two projects compile - the instances of one project file, or two project files sharing a source file - no longer reports each use twice.Document.FindFSharpReferencesAsynctakes anFSharpProjectSnapshot voptionand picks the FCS overload from it); oneSemaphoreSlim(ProcessorCount)bounds the concurrent file checks across the whole search instead of per project.Project.FindFSharpReferencesAsyncresolves the declaration document inside its own instance, so the compile-order skip works in every instance.findSymbolUses(CurrentDocument/SignatureAndImplementationinside the| scope ->fallback) are gone; the outer match is exhaustive.Behaviour change with the option off: every instance is still searched in full, but the concurrency bound now applies across the solution where the code used to run one project's documents sequentially and projects without a limit. Residual risk with the option on: an instance can resolve an overload differently without any
#if(an extension member shadowed by a newer BCL intrinsic); the ranking covers the polyfill case within the current project's world, and turning the option off restores the full search.Rename needs no change:
getSymbolUsesInSolutionnow yields oneDocumentIdper file plus the instance-specific ones, and Roslyn merges the edits of linked documents.Tests:
MultiTargetFindReferencesTestsloads one project as two instances (one withoutFOOand without the fourth file, one with both) through the newRoslynTestHelpers.CreateMultiTargetSolutionand, starting from either instance, checks that Find All References reports the signature, the plain use, both uses in the#if FOOfile, the use in the instance-only file and the use guarded by a define both instances share exactly once each, and that Rename gets every use once, each from an instance that compiles its file. The first commit (test helpers) is shared with #20462; the second moves theIFSharpFindUsagesContextstub intoRoslynTestHelpers.No timings are claimed: on the solution above the search scope goes from 86 instances to 26, plus the conditional and instance-only files of the other 60.
Checklist
Test cases added
Performance benchmarks added in case of performance changes
Release notes entry updated:
🤖 Generated with Claude Code