Skip to content

Commit

Permalink
Merge pull request #72957 from CyrusNajmabadi/bulkAddRemove
Browse files Browse the repository at this point in the history
  • Loading branch information
CyrusNajmabadi authored Apr 9, 2024
2 parents 3397730 + 8c0eb5e commit a620334
Show file tree
Hide file tree
Showing 9 changed files with 540 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.ProjectSystem;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
Expand Down Expand Up @@ -133,8 +134,9 @@ await ApplyChangeToWorkspaceAsync(w =>
analyzerReferences: w.CurrentSolution.AnalyzerReferences).WithTelemetryId(SolutionTelemetryId);
var newSolution = w.CreateSolution(solutionInfo);
foreach (var project in solutionInfo.Projects)
newSolution = newSolution.AddProject(project);
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projectInfos);
projectInfos.AddRange(solutionInfo.Projects);
newSolution = newSolution.AddProjects(projectInfos);
return newSolution;
}
Expand Down
29 changes: 21 additions & 8 deletions src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
Expand Down Expand Up @@ -344,17 +345,29 @@ public Project AddProject(string name, string assemblyName, string language)
public Solution AddProject(ProjectId projectId, string name, string assemblyName, string language)
=> this.AddProject(ProjectInfo.Create(projectId, VersionStamp.Create(), name, assemblyName, language));

/// <summary>
/// Create a new solution instance that includes a project with the specified project information.
/// </summary>
/// <inheritdoc cref="SolutionCompilationState.AddProjects"/>
public Solution AddProject(ProjectInfo projectInfo)
=> WithCompilationState(_compilationState.AddProject(projectInfo));
{
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(1, out var projectInfos);
projectInfos.Add(projectInfo);
return AddProjects(projectInfos);
}

/// <summary>
/// Create a new solution instance without the project specified.
/// </summary>
/// <inheritdoc cref="SolutionCompilationState.AddProjects"/>
internal Solution AddProjects(ArrayBuilder<ProjectInfo> projectInfos)
=> WithCompilationState(_compilationState.AddProjects(projectInfos));

/// <inheritdoc cref="SolutionCompilationState.RemoveProjects"/>
public Solution RemoveProject(ProjectId projectId)
=> WithCompilationState(_compilationState.RemoveProject(projectId));
{
using var _ = ArrayBuilder<ProjectId>.GetInstance(1, out var projectIds);
projectIds.Add(projectId);
return RemoveProjects(projectIds);
}

/// <inheritdoc cref="SolutionCompilationState.RemoveProjects"/>
internal Solution RemoveProjects(ArrayBuilder<ProjectId> projectIds)
=> WithCompilationState(_compilationState.RemoveProjects(projectIds));

/// <summary>
/// Creates a new solution instance with the project specified updated to have the new
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,42 +316,83 @@ private ImmutableSegmentedDictionary<ProjectId, ICompilationTracker> CreateCompi

public SourceGeneratorExecutionVersionMap SourceGeneratorExecutionVersionMap => _sourceGeneratorExecutionVersionMap;

/// <inheritdoc cref="SolutionState.AddProject(ProjectInfo)"/>
public SolutionCompilationState AddProject(ProjectInfo projectInfo)
/// <inheritdoc cref="SolutionState.AddProjects(ArrayBuilder{ProjectInfo})"/>
public SolutionCompilationState AddProjects(ArrayBuilder<ProjectInfo> projectInfos)
{
var newSolutionState = this.SolutionState.AddProject(projectInfo);
var newTrackerMap = CreateCompilationTrackerMap(projectInfo.Id, newSolutionState.GetProjectDependencyGraph(), static (_, _) => { }, /* unused */ 0, skipEmptyCallback: true);
if (projectInfos.Count == 0)
return this;

var newSolutionState = this.SolutionState.AddProjects(projectInfos);

var sourceGeneratorExecutionVersionMap = _sourceGeneratorExecutionVersionMap;
if (RemoteSupportedLanguages.IsSupported(projectInfo.Language))
// When adding a project, we might add a project that an *existing* project now has a reference to. That's
// because we allow existing projects to have 'dangling' project references. As such, we have to ensure we do
// not reuse compilation trackers for any of those projects.
using var _ = PooledHashSet<ProjectId>.GetInstance(out var dependentProjects);
var newDependencyGraph = newSolutionState.GetProjectDependencyGraph();
foreach (var projectInfo in projectInfos)
dependentProjects.AddRange(newDependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(projectInfo.Id));

var newTrackerMap = CreateCompilationTrackerMap(
static (projectId, dependentProjects) => !dependentProjects.Contains(projectId),
dependentProjects,
// We don't need to do anything here. Compilation trackers are created on demand. So we'll just keep the
// tracker map as-is, and have the trackers for these new projects be created when needed.
modifyNewTrackerInfo: static (_, _) => { }, argModifyNewTrackerInfo: default(VoidResult),
skipEmptyCallback: true);

var versionMapBuilder = _sourceGeneratorExecutionVersionMap.Map.ToBuilder();
foreach (var projectInfo in projectInfos)
{
var versionMapBuilder = _sourceGeneratorExecutionVersionMap.Map.ToBuilder();
versionMapBuilder.Add(projectInfo.Id, new());
sourceGeneratorExecutionVersionMap = new(versionMapBuilder.ToImmutable());
if (RemoteSupportedLanguages.IsSupported(projectInfo.Language))
versionMapBuilder.Add(projectInfo.Id, new());
}

var sourceGeneratorExecutionVersionMap = new SourceGeneratorExecutionVersionMap(versionMapBuilder.ToImmutable());
return Branch(
newSolutionState,
projectIdToTrackerMap: newTrackerMap,
sourceGeneratorExecutionVersionMap: sourceGeneratorExecutionVersionMap);
}

/// <inheritdoc cref="SolutionState.RemoveProject(ProjectId)"/>
public SolutionCompilationState RemoveProject(ProjectId projectId)
/// <inheritdoc cref="SolutionState.RemoveProjects"/>
public SolutionCompilationState RemoveProjects(ArrayBuilder<ProjectId> projectIds)
{
var newSolutionState = this.SolutionState.RemoveProject(projectId);
if (projectIds.Count == 0)
return this;

// Now go and remove the projects from teh solution-state itself.
var newSolutionState = this.SolutionState.RemoveProjects(projectIds);

var originalDependencyGraph = this.SolutionState.GetProjectDependencyGraph();
using var _ = PooledHashSet<ProjectId>.GetInstance(out var dependentProjects);

// Determine the set of projects that depend on the projects being removed.
foreach (var projectId in projectIds)
{
foreach (var dependentProject in originalDependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(projectId))
dependentProjects.Add(dependentProject);
}

// Now for each compilation tracker.
// 1. remove the compilation tracker if we're removing the project.
// 2. fork teh compilation tracker if it depended on a removed project.
// 3. do nothing for the rest.
var newTrackerMap = CreateCompilationTrackerMap(
projectId,
newSolutionState.GetProjectDependencyGraph(),
static (trackerMap, projectId) =>
// Can reuse the compilation tracker for a project, unless it is some project that had a dependency on one
// of the projects removed.
static (projectId, dependentProjects) => !dependentProjects.Contains(projectId),
dependentProjects,
static (trackerMap, projectIds) =>
{
trackerMap.Remove(projectId);
foreach (var projectId in projectIds)
trackerMap.Remove(projectId);
},
projectId,
projectIds,
skipEmptyCallback: true);

var versionMapBuilder = _sourceGeneratorExecutionVersionMap.Map.ToBuilder();
versionMapBuilder.Remove(projectId);
foreach (var projectId in projectIds)
versionMapBuilder.Remove(projectId);

return this.Branch(
newSolutionState,
Expand Down
158 changes: 94 additions & 64 deletions src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
Expand Down Expand Up @@ -288,98 +289,127 @@ public ProjectState GetRequiredProjectState(ProjectId projectId)
return result;
}

private SolutionState AddProject(ProjectState projectState)
/// <summary>
/// Create a new solution instance that includes projects with the specified project information.
/// </summary>
public SolutionState AddProjects(ArrayBuilder<ProjectInfo> projectInfos)
{
var projectId = projectState.Id;
Contract.ThrowIfTrue(projectInfos.HasDuplicates(static p => p.Id), "Duplicate ProjectId provided");

// changed project list so, increment version.
var newSolutionAttributes = _solutionAttributes.With(version: Version.GetNewerVersion());
if (projectInfos.Count == 0)
return this;

var newProjectIds = ProjectIds.ToImmutableArray().Add(projectId);
var newStateMap = _projectIdToProjectStateMap.Add(projectId, projectState);
using var _ = ArrayBuilder<ProjectState>.GetInstance(projectInfos.Count, out var projectStates);
foreach (var projectInfo in projectInfos)
projectStates.Add(CreateProjectState(projectInfo));

var newDependencyGraph = _dependencyGraph
.WithAdditionalProject(projectId)
.WithAdditionalProjectReferences(projectId, projectState.ProjectReferences);
return AddProjects(projectStates);

// It's possible that another project already in newStateMap has a reference to this project that we're adding, since we allow
// dangling references like that. If so, we'll need to link those in too.
foreach (var newState in newStateMap)
ProjectState CreateProjectState(ProjectInfo projectInfo)
{
foreach (var projectReference in newState.Value.ProjectReferences)
{
if (projectReference.ProjectId == projectId)
{
newDependencyGraph = newDependencyGraph.WithAdditionalProjectReferences(
newState.Key, [projectReference]);
if (projectInfo == null)
throw new ArgumentNullException(nameof(projectInfo));

break;
}
}
}
var projectId = projectInfo.Id;

return Branch(
solutionAttributes: newSolutionAttributes,
projectIds: newProjectIds,
idToProjectStateMap: newStateMap,
dependencyGraph: newDependencyGraph);
}
var language = projectInfo.Language;
if (language == null)
throw new ArgumentNullException(nameof(language));

/// <summary>
/// Create a new solution instance that includes a project with the specified project information.
/// </summary>
public SolutionState AddProject(ProjectInfo projectInfo)
{
if (projectInfo == null)
{
throw new ArgumentNullException(nameof(projectInfo));
}
var displayName = projectInfo.Name;
if (displayName == null)
throw new ArgumentNullException(nameof(displayName));

var projectId = projectInfo.Id;
CheckNotContainsProject(projectId);

var language = projectInfo.Language;
if (language == null)
{
throw new ArgumentNullException(nameof(language));
var languageServices = Services.GetLanguageServices(language);
if (languageServices == null)
throw new ArgumentException(string.Format(WorkspacesResources.The_language_0_is_not_supported, language));

var newProject = new ProjectState(languageServices, projectInfo);
return newProject;
}

var displayName = projectInfo.Name;
if (displayName == null)
SolutionState AddProjects(ArrayBuilder<ProjectState> projectStates)
{
throw new ArgumentNullException(nameof(displayName));
}
// changed project list so, increment version.
var newSolutionAttributes = _solutionAttributes.With(version: Version.GetNewerVersion());

CheckNotContainsProject(projectId);
using var _1 = ArrayBuilder<ProjectId>.GetInstance(ProjectIds.Count + projectStates.Count, out var newProjectIdsBuilder);
using var _2 = PooledHashSet<ProjectId>.GetInstance(out var addedProjectIds);
var newStateMapBuilder = _projectIdToProjectStateMap.ToBuilder();

var languageServices = Services.GetLanguageServices(language);
if (languageServices == null)
{
throw new ArgumentException(string.Format(WorkspacesResources.The_language_0_is_not_supported, language));
}
newProjectIdsBuilder.AddRange(ProjectIds);

foreach (var projectState in projectStates)
{
addedProjectIds.Add(projectState.Id);
newProjectIdsBuilder.Add(projectState.Id);
newStateMapBuilder.Add(projectState.Id, projectState);
}

var newProject = new ProjectState(languageServices, projectInfo);
var newProjectIds = newProjectIdsBuilder.ToBoxedImmutableArray();
var newStateMap = newStateMapBuilder.ToImmutable();

return this.AddProject(newProject);
// TODO: it would be nice to update these graphs without so much forking.
var newDependencyGraph = _dependencyGraph;
foreach (var projectState in projectStates)
{
var projectId = projectState.Id;
newDependencyGraph = newDependencyGraph
.WithAdditionalProject(projectId)
.WithAdditionalProjectReferences(projectId, projectState.ProjectReferences);
}

// It's possible that another project already in newStateMap has a reference to this project that we're adding,
// since we allow dangling references like that. If so, we'll need to link those in too.
foreach (var (projectId, newState) in newStateMap)
{
foreach (var projectReference in newState.ProjectReferences)
{
if (addedProjectIds.Contains(projectReference.ProjectId))
newDependencyGraph = newDependencyGraph.WithAdditionalProjectReferences(projectId, [projectReference]);
}
}

return Branch(
solutionAttributes: newSolutionAttributes,
projectIds: newProjectIds,
idToProjectStateMap: newStateMap,
dependencyGraph: newDependencyGraph);
}
}

/// <summary>
/// Create a new solution instance without the project specified.
/// Create a new solution instance without the projects specified.
/// </summary>
public SolutionState RemoveProject(ProjectId projectId)
public SolutionState RemoveProjects(ArrayBuilder<ProjectId> projectIds)
{
if (projectId == null)
{
throw new ArgumentNullException(nameof(projectId));
}
Contract.ThrowIfTrue(projectIds.HasDuplicates(), "Duplicate ProjectId provided");

CheckContainsProject(projectId);
if (projectIds.Count == 0)
return this;

foreach (var projectId in projectIds)
CheckContainsProject(projectId);

// changed project list so, increment version.
var newSolutionAttributes = _solutionAttributes.With(version: this.Version.GetNewerVersion());

var newProjectIds = ProjectIds.ToImmutableArray().Remove(projectId);
var newStateMap = _projectIdToProjectStateMap.Remove(projectId);
var newDependencyGraph = _dependencyGraph.WithProjectRemoved(projectId);
using var _ = PooledHashSet<ProjectId>.GetInstance(out var projectIdsSet);
projectIdsSet.AddRange(projectIds);

var newProjectIds = ProjectIds.Where(p => !projectIdsSet.Contains(p)).ToBoxedImmutableArray();

var newStateMapBuilder = _projectIdToProjectStateMap.ToBuilder();
foreach (var projectId in projectIds)
newStateMapBuilder.Remove(projectId);
var newStateMap = newStateMapBuilder.ToImmutable();

// Note: it would be nice to not cause N forks of the dependency graph here.
var newDependencyGraph = _dependencyGraph;
foreach (var projectId in projectIds)
newDependencyGraph = newDependencyGraph.WithProjectRemoved(projectId);

return this.Branch(
solutionAttributes: newSolutionAttributes,
Expand Down
Loading

0 comments on commit a620334

Please sign in to comment.