Skip to content

Commit

Permalink
Implement alg that determines which projects need to be restarted/reb…
Browse files Browse the repository at this point in the history
…uilt due to rude edits (#73598)
  • Loading branch information
tmat committed May 30, 2024
1 parent b714c00 commit 49e109d
Show file tree
Hide file tree
Showing 15 changed files with 587 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ public async ValueTask<ManagedHotReloadUpdates> GetUpdatesAsync(CancellationToke

UpdateApplyChangesDiagnostics(diagnosticData);

var diagnostics = await EmitSolutionUpdateResults.GetHotReloadDiagnosticsAsync(solution, diagnosticData, rudeEdits, syntaxError, moduleUpdates.Status, cancellationToken).ConfigureAwait(false);
var diagnostics = await EmitSolutionUpdateResults.GetAllDiagnosticsAsync(solution, diagnosticData, rudeEdits, syntaxError, moduleUpdates.Status, cancellationToken).ConfigureAwait(false);
return new ManagedHotReloadUpdates(moduleUpdates.Updates.FromContract(), diagnostics.FromContract());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,11 @@ public async Task Test(bool commitChanges)

var localService = localWorkspace.GetService<EditAndContinueLanguageService>();

var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);

DocumentId documentId;
await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
.AddProject(projectId, "proj", "proj", LanguageNames.CSharp)
.AddTestProject("proj", out var projectId).Solution
.AddMetadataReferences(projectId, TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40))
.AddDocument(documentId, "test.cs", SourceText.From("class C { }", Encoding.UTF8), filePath: "test.cs"));
.AddDocument(documentId = DocumentId.CreateNewId(projectId), "test.cs", SourceText.From("class C { }", Encoding.UTF8), filePath: "test.cs"));

var solution = localWorkspace.CurrentSolution;
var project = solution.GetRequiredProject(projectId);
Expand Down Expand Up @@ -179,12 +177,13 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
AssertEx.Equal(
[
$"Error ENC1001: test.cs(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "error 1")}",
$"Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}"
$"Error ENC1001: proj.csproj(0, 0, 0, 0): {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}"
], sessionState.ApplyChangesDiagnostics.Select(Inspect));

AssertEx.Equal(
[
$"Error ENC1001: test.cs(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "error 1")}",
$"Error ENC1001: proj.csproj(0, 0, 0, 0): {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}",
$"Error ENC1001: test.cs(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "syntax error 3")}",
$"RestartRequired ENC0033: test.cs(0, 2, 0, 3): {string.Format(FeaturesResources.Deleting_0_requires_restarting_the_application, "x")}"
], updates.Diagnostics.Select(Inspect));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue;
internal readonly struct ManagedHotReloadUpdate(
Guid module,
string moduleName,
ProjectId projectId,
ImmutableArray<byte> ilDelta,
ImmutableArray<byte> metadataDelta,
ImmutableArray<byte> pdbDelta,
Expand All @@ -28,6 +29,9 @@ internal readonly struct ManagedHotReloadUpdate(
[DataMember(Name = "moduleName")]
public string ModuleName { get; } = moduleName;

[DataMember(Name = "projectId")]
public ProjectId ProjectId { get; } = projectId;

[DataMember(Name = "ilDelta")]
public ImmutableArray<byte> ILDelta { get; } = ilDelta;

Expand Down
7 changes: 7 additions & 0 deletions src/Features/Core/Portable/EditAndContinue/EditSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,12 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(Solution solution
var hasEmitErrors = false;
foreach (var newProject in solution.Projects)
{
if (newProject.FilePath == null)
{
log.Write("Skipping project '{0}' without a file path", newProject.Id);
continue;
}

var oldProject = oldSolution.GetProject(newProject.Id);
if (oldProject == null)
{
Expand Down Expand Up @@ -1049,6 +1055,7 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
var delta = new ManagedHotReloadUpdate(
mvid,
newCompilation.AssemblyName ?? newProject.Name, // used for display in debugger diagnostics
newProject.Id,
ilStream.ToImmutableArray(),
metadataStream.ToImmutableArray(),
pdbStream.ToImmutableArray(),
Expand Down
161 changes: 142 additions & 19 deletions src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
Expand Down Expand Up @@ -67,10 +71,143 @@ public Data Dehydrate(Solution solution)
return DiagnosticData.Create(SyntaxError, solution.GetRequiredDocument(SyntaxError.Location.SourceTree));
}

private IEnumerable<Project> GetProjectsContainingBlockingRudeEdits(Solution solution)
=> RudeEdits
.Where(static e => e.Diagnostics.Any(static d => d.Kind.IsBlocking()))
.Select(static e => e.DocumentId.ProjectId)
.Distinct()
.OrderBy(static id => id)
.Select(solution.GetRequiredProject);

/// <summary>
/// Returns projects that need to be rebuilt and/or restarted due to blocking rude edits in order to apply changes.
/// </summary>
/// <param name="isRunningProject">Identifies projects that have been launched.</param>
/// <param name="projectsToRestart">Running projects that have to be restarted.</param>
/// <param name="projectsToRebuild">Projects whose source have been updated and need to be rebuilt.</param>
public void GetProjectsToRebuildAndRestart(
Solution solution,
Func<Project, bool> isRunningProject,
ISet<Project> projectsToRestart,
ISet<Project> projectsToRebuild)
{
var graph = solution.GetProjectDependencyGraph();

// First, find all running projects that transitively depend on projects with rude edits.
// These will need to be rebuilt and restarted. In order to rebuilt these projects
// all their transitive references must either be free of source changes or be rebuilt as well.
// This may add more running projects to the set of projects we need to restart.
// We need to repeat this process until we find a fixed point.

using var _1 = ArrayBuilder<Project>.GetInstance(out var traversalStack);

projectsToRestart.Clear();
projectsToRebuild.Clear();

foreach (var projectWithRudeEdit in GetProjectsContainingBlockingRudeEdits(solution))
{
if (AddImpactedRunningProjects(projectsToRestart, projectWithRudeEdit))
{
projectsToRebuild.Add(projectWithRudeEdit);
}
}

// At this point the restart set contains all running projects directly affected by rude edits.
// Next, find projects that were successfully updated and affect running projects.

if (ModuleUpdates.Updates.IsEmpty || projectsToRestart.IsEmpty())
{
return;
}

// The set of updated projects is usually much smaller then the number of all projects in the solution.
// We iterate over this set updating the reset set until no new project is added to the reset set.
// Once a project is determined to affect a running process, all running processes that
// reference this project are added to the reset set. The project is then removed from updated
// project set as it can't contribute any more running projects to the reset set.
// If an updated project does not affect reset set in a given iteration, it stays in the set
// because it may affect reset set later on, after another running project is added to it.

using var _2 = PooledHashSet<Project>.GetInstance(out var updatedProjects);
using var _3 = ArrayBuilder<Project>.GetInstance(out var updatedProjectsToRemove);
foreach (var update in ModuleUpdates.Updates)
{
updatedProjects.Add(solution.GetRequiredProject(update.ProjectId));
}

using var _4 = ArrayBuilder<Project>.GetInstance(out var impactedProjects);

while (true)
{
Debug.Assert(updatedProjectsToRemove.Count == 0);

foreach (var updatedProject in updatedProjects)
{
if (AddImpactedRunningProjects(impactedProjects, updatedProject) &&
impactedProjects.Any(projectsToRestart.Contains))
{
projectsToRestart.AddRange(impactedProjects);
updatedProjectsToRemove.Add(updatedProject);
projectsToRebuild.Add(updatedProject);
}

impactedProjects.Clear();
}

if (updatedProjectsToRemove is [])
{
// none of the remaining updated projects affect restart set:
break;
}

updatedProjects.RemoveAll(updatedProjectsToRemove);
updatedProjectsToRemove.Clear();
}

return;

bool AddImpactedRunningProjects(ICollection<Project> impactedProjects, Project initialProject)
{
Debug.Assert(traversalStack.Count == 0);
traversalStack.Push(initialProject);

var added = false;

while (traversalStack.Count > 0)
{
var project = traversalStack.Pop();
if (isRunningProject(project))
{
impactedProjects.Add(project);
added = true;
}

foreach (var referencingProjectId in graph.GetProjectsThatDirectlyDependOnThisProject(project.Id))
{
traversalStack.Push(solution.GetRequiredProject(referencingProjectId));
}
}

return added;
}
}

public async Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsAsync(Solution solution, CancellationToken cancellationToken)
{
using var _ = ArrayBuilder<Diagnostic>.GetInstance(out var diagnostics);

// add semantic and lowering diagnostics reported during delta emit:
foreach (var (_, projectEmitDiagnostics) in Diagnostics)
{
diagnostics.AddRange(projectEmitDiagnostics);
}

// add syntax error:
if (SyntaxError != null)
{
diagnostics.Add(SyntaxError);
}

// add rude edits:
foreach (var (documentId, documentRudeEdits) in RudeEdits)
{
Expand All @@ -84,16 +221,10 @@ public async Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsAsync(Solution so
}
}

// add emit diagnostics:
foreach (var (_, projectEmitDiagnostics) in Diagnostics)
{
diagnostics.AddRange(projectEmitDiagnostics);
}

return diagnostics.ToImmutableAndClear();
}

internal static async ValueTask<ImmutableArray<ManagedHotReloadDiagnostic>> GetHotReloadDiagnosticsAsync(
internal static async ValueTask<ImmutableArray<ManagedHotReloadDiagnostic>> GetAllDiagnosticsAsync(
Solution solution,
ImmutableArray<DiagnosticData> diagnosticData,
ImmutableArray<(DocumentId DocumentId, ImmutableArray<RudeEditDiagnostic> Diagnostics)> rudeEdits,
Expand All @@ -103,23 +234,15 @@ internal static async ValueTask<ImmutableArray<ManagedHotReloadDiagnostic>> GetH
{
using var _ = ArrayBuilder<ManagedHotReloadDiagnostic>.GetInstance(out var builder);

// Add the first compiler emit error. Do not report warnings - they do not block applying the edit.
// It's unnecessary to report more then one error since all the diagnostics are already reported in the Error List
// and this is just messaging to the agent.
// Add semantic and lowering diagnostics reported during delta emit:

foreach (var data in diagnosticData)
{
if (data.Severity != DiagnosticSeverity.Error)
{
continue;
}

builder.Add(data.ToHotReloadDiagnostic(updateStatus));

// only report first error
break;
}

// Add syntax error:

if (syntaxError != null)
{
Debug.Assert(syntaxError.DataLocation != null);
Expand Down
Loading

0 comments on commit 49e109d

Please sign in to comment.