Skip to content

Commit 9403340

Browse files
Improve performance in metadata-to-project-reference conversion (#79163)
A bunch of small improvements: - Make CanConvertMetadataReferenceToProjectReference take ProjectStates rather than the IDs and doing the lookups. The ID -> State lookup is O(log n), but doing that a lot on a high traffic method like that was showing up in traces. - Rather than looping through IDs and then grabbing states in ConvertMetadataReferencesToProjectReference, we can just loop across states directly. - Avoid calling CanConvertMetadataReferenceToProjectReference if there's no references, which is common during load where we might know all the output paths up front but not the references which come in later. - Use DoesProjectTransitivelyDependOnProject when checking for P2P references, which takes advantage of the map in either direction.
2 parents 6b00b6c + b961397 commit 9403340

File tree

2 files changed

+29
-30
lines changed

2 files changed

+29
-30
lines changed

src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ static ProjectUpdateState UpdateMetadataReferences(
726726
foreach (var (path, properties) in metadataReferencesAddedInBatch)
727727
{
728728
projectUpdateState = TryCreateConvertedProjectReference_NoLock(
729-
projectId, path, properties, projectUpdateState, solutionChanges.Solution, out var projectReference);
729+
projectBeforeMutation.State, path, properties, projectUpdateState, solutionChanges.Solution, out var projectReference);
730730

731731
if (projectReference != null)
732732
{

src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Immutable;
88
using System.IO;
99
using System.Linq;
10+
using System.Runtime.CompilerServices;
1011
using System.Threading;
1112
using System.Threading.Tasks;
1213
using Microsoft.CodeAnalysis.Diagnostics;
@@ -548,14 +549,20 @@ private static ProjectUpdateState ConvertMetadataReferencesToProjectReferences_N
548549
string outputPath,
549550
ProjectUpdateState projectUpdateState)
550551
{
551-
foreach (var projectIdToRetarget in solutionChanges.Solution.ProjectIds)
552+
// PERF: call GetRequiredProjectState instead of GetRequiredProject, otherwise creating a new project
553+
// might force all Project instances to get created.
554+
var candidateProjectState = solutionChanges.Solution.GetRequiredProjectState(projectIdToReference);
555+
556+
foreach (var projectToRetarget in solutionChanges.Solution.SortedProjectStates)
552557
{
553-
if (CanConvertMetadataReferenceToProjectReference(solutionChanges.Solution, projectIdToRetarget, referencedProjectId: projectIdToReference))
558+
// PERF: If we don't even have any metadata references yet, then don't even call CanConvertMetadataReferenceToProjectReference.
559+
// This optimizes the early parts of solution load, where projects may be created with their output paths right away,
560+
// but metadata references come in later. CanConvertMetadataReferenceToProjectReference isn't terribly expensive
561+
// but when called enough times things can start to add up.
562+
if (projectToRetarget.MetadataReferences.Count > 0 &&
563+
CanConvertMetadataReferenceToProjectReference(solutionChanges.Solution, projectToRetarget, candidateProjectState))
554564
{
555-
// PERF: call GetRequiredProjectState instead of GetRequiredProject, otherwise creating a new project
556-
// might force all Project instances to get created.
557-
var projectState = solutionChanges.Solution.GetRequiredProjectState(projectIdToRetarget);
558-
foreach (var reference in projectState.MetadataReferences)
565+
foreach (var reference in projectToRetarget.MetadataReferences)
559566
{
560567
if (reference is PortableExecutableReference peReference
561568
&& string.Equals(peReference.FilePath, outputPath, StringComparison.OrdinalIgnoreCase))
@@ -564,13 +571,13 @@ private static ProjectUpdateState ConvertMetadataReferencesToProjectReferences_N
564571

565572
var projectReference = new ProjectReference(projectIdToReference, peReference.Properties.Aliases, peReference.Properties.EmbedInteropTypes);
566573
var newSolution = solutionChanges.Solution
567-
.RemoveMetadataReference(projectIdToRetarget, peReference)
568-
.AddProjectReference(projectIdToRetarget, projectReference);
574+
.RemoveMetadataReference(projectToRetarget.Id, peReference)
575+
.AddProjectReference(projectToRetarget.Id, projectReference);
569576

570-
solutionChanges.UpdateSolutionForProjectAction(projectIdToRetarget, newSolution);
577+
solutionChanges.UpdateSolutionForProjectAction(projectToRetarget.Id, newSolution);
571578

572-
projectUpdateState = GetReferenceInformation(projectIdToRetarget, projectUpdateState, out var projectInfo);
573-
projectUpdateState = projectUpdateState.WithProjectReferenceInfo(projectIdToRetarget,
579+
projectUpdateState = GetReferenceInformation(projectToRetarget.Id, projectUpdateState, out var projectInfo);
580+
projectUpdateState = projectUpdateState.WithProjectReferenceInfo(projectToRetarget.Id,
574581
projectInfo.WithConvertedProjectReference(peReference.FilePath!, projectReference));
575582

576583
// We have converted one, but you could have more than one reference with different aliases that
@@ -585,33 +592,25 @@ private static ProjectUpdateState ConvertMetadataReferencesToProjectReferences_N
585592

586593
[PerformanceSensitive("https://github.com/dotnet/roslyn/issues/31306",
587594
Constraint = "Avoid calling " + nameof(CodeAnalysis.Solution.GetProject) + " to avoid realizing all projects.")]
588-
private static bool CanConvertMetadataReferenceToProjectReference(Solution solution, ProjectId projectIdWithMetadataReference, ProjectId referencedProjectId)
595+
private static bool CanConvertMetadataReferenceToProjectReference(Solution solution, ProjectState projectWithMetadataReference, ProjectState candidateProjectToReference)
589596
{
590597
// We can never make a project reference ourselves. This isn't a meaningful scenario, but if somebody does this by accident
591598
// we do want to throw exceptions.
592-
if (projectIdWithMetadataReference == referencedProjectId)
599+
if (projectWithMetadataReference.Id == candidateProjectToReference.Id)
593600
{
594601
return false;
595602
}
596603

597-
// PERF: call GetProjectState instead of GetProject, otherwise creating a new project might force all
598-
// Project instances to get created.
599-
var projectWithMetadataReference = solution.GetProjectState(projectIdWithMetadataReference);
600-
var referencedProject = solution.GetProjectState(referencedProjectId);
601-
602-
Contract.ThrowIfNull(projectWithMetadataReference);
603-
Contract.ThrowIfNull(referencedProject);
604-
605604
// We don't want to convert a metadata reference to a project reference if the project being referenced isn't
606605
// something we can create a Compilation for. For example, if we have a C# project, and it's referencing a F#
607606
// project via a metadata reference everything would be fine if we left it a metadata reference. Converting it
608607
// to a project reference means we couldn't create a Compilation anymore in the IDE, since the C# compilation
609608
// would need to reference an F# compilation. F# projects referencing other F# projects though do expect this to
610609
// work, and so we'll always allow references through of the same language.
611-
if (projectWithMetadataReference.Language != referencedProject.Language)
610+
if (projectWithMetadataReference.Language != candidateProjectToReference.Language)
612611
{
613612
if (projectWithMetadataReference.LanguageServices.GetService<ICompilationFactoryService>() != null &&
614-
referencedProject.LanguageServices.GetService<ICompilationFactoryService>() == null)
613+
candidateProjectToReference.LanguageServices.GetService<ICompilationFactoryService>() == null)
615614
{
616615
// We're referencing something that we can't create a compilation from something that can, so keep the metadata reference
617616
return false;
@@ -621,11 +620,11 @@ private static bool CanConvertMetadataReferenceToProjectReference(Solution solut
621620
// Getting a metadata reference from a 'module' is not supported from the compilation layer. Nor is emitting a
622621
// 'metadata-only' stream for it (a 'skeleton' reference). So converting a NetModule reference to a project
623622
// reference won't actually help us out. Best to keep this as a plain metadata reference.
624-
if (referencedProject.CompilationOptions?.OutputKind == OutputKind.NetModule)
623+
if (candidateProjectToReference.CompilationOptions?.OutputKind == OutputKind.NetModule)
625624
return false;
626625

627626
// If this is going to cause a circular reference, also disallow it
628-
if (solution.GetProjectDependencyGraph().GetProjectsThatThisProjectTransitivelyDependsOn(referencedProjectId).Contains(projectIdWithMetadataReference))
627+
if (solution.GetProjectDependencyGraph().DoesProjectTransitivelyDependOnProject(candidateProjectToReference.Id, projectWithMetadataReference.Id))
629628
{
630629
return false;
631630
}
@@ -696,7 +695,7 @@ private static ProjectUpdateState ConvertProjectReferencesToMetadataReferences_N
696695
/// during a workspace update (which will attempt to apply the update multiple times).
697696
/// </summary>
698697
public static ProjectUpdateState TryCreateConvertedProjectReference_NoLock(
699-
ProjectId referencingProject,
698+
ProjectState referencingProjectState,
700699
string path,
701700
MetadataReferenceProperties properties,
702701
ProjectUpdateState projectUpdateState,
@@ -707,15 +706,15 @@ public static ProjectUpdateState TryCreateConvertedProjectReference_NoLock(
707706
{
708707
var projectIdToReference = ids.First();
709708

710-
if (CanConvertMetadataReferenceToProjectReference(currentSolution, referencingProject, projectIdToReference))
709+
if (CanConvertMetadataReferenceToProjectReference(currentSolution, referencingProjectState, currentSolution.GetRequiredProjectState(projectIdToReference)))
711710
{
712711
projectReference = new ProjectReference(
713712
projectIdToReference,
714713
aliases: properties.Aliases,
715714
embedInteropTypes: properties.EmbedInteropTypes);
716715

717-
projectUpdateState = GetReferenceInformation(referencingProject, projectUpdateState, out var projectReferenceInfo);
718-
projectUpdateState = projectUpdateState.WithProjectReferenceInfo(referencingProject, projectReferenceInfo.WithConvertedProjectReference(path, projectReference));
716+
projectUpdateState = GetReferenceInformation(referencingProjectState.Id, projectUpdateState, out var projectReferenceInfo);
717+
projectUpdateState = projectUpdateState.WithProjectReferenceInfo(referencingProjectState.Id, projectReferenceInfo.WithConvertedProjectReference(path, projectReference));
719718
return projectUpdateState;
720719
}
721720
else

0 commit comments

Comments
 (0)