Skip to content

Commit 9ba4090

Browse files
committed
Implement support for partial solution updates
1 parent 12a238a commit 9ba4090

File tree

14 files changed

+316
-285
lines changed

14 files changed

+316
-285
lines changed

src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -521,36 +521,44 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
521521
// Make sure the solution snapshot has all source-generated documents up-to-date.
522522
solution = solution.WithUpToDateSourceGeneratorDocuments(solution.ProjectIds);
523523

524-
var solutionUpdate = await EditSession.EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, updateId, cancellationToken).ConfigureAwait(false);
524+
var solutionUpdate = await EditSession.EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, updateId, runningProjects, cancellationToken).ConfigureAwait(false);
525525

526526
var allowPartialUpdates = runningProjects.Any(p => p.Value.AllowPartialUpdate);
527527

528-
EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart(
529-
solution,
530-
solutionUpdate.ModuleUpdates,
531-
solutionUpdate.Diagnostics,
532-
runningProjects,
533-
out var projectsToRestart,
534-
out var projectsToRebuild);
535-
536528
solutionUpdate.Log(SessionLog, updateId);
537529
_lastModuleUpdatesLog = solutionUpdate.ModuleUpdates.Updates;
538530

539531
switch (solutionUpdate.ModuleUpdates.Status)
540532
{
541-
case ModuleUpdateStatus.RestartRequired when allowPartialUpdates:
542533
case ModuleUpdateStatus.Ready:
543-
Contract.ThrowIfTrue(solutionUpdate.ModuleUpdates.Updates.IsEmpty && projectsToRebuild.IsEmpty);
534+
Contract.ThrowIfTrue(solutionUpdate.ModuleUpdates.Updates.IsEmpty && solutionUpdate.ProjectsToRebuild.IsEmpty);
544535

545536
// We have updates to be applied or processes to restart. The debugger will call Commit/Discard on the solution
546537
// based on whether the updates will be applied successfully or not.
547-
StorePendingUpdate(new PendingSolutionUpdate(
548-
solution,
549-
solutionUpdate.ProjectsToStale,
550-
allowPartialUpdates ? projectsToRebuild : [],
551-
solutionUpdate.ProjectBaselines,
552-
solutionUpdate.ModuleUpdates.Updates,
553-
solutionUpdate.NonRemappableRegions));
538+
539+
if (allowPartialUpdates)
540+
{
541+
StorePendingUpdate(new PendingSolutionUpdate(
542+
solution,
543+
solutionUpdate.ProjectsToStale,
544+
solutionUpdate.ProjectsToRebuild,
545+
solutionUpdate.ProjectBaselines,
546+
solutionUpdate.ModuleUpdates.Updates,
547+
solutionUpdate.NonRemappableRegions));
548+
}
549+
else if (solutionUpdate.ProjectsToRebuild.IsEmpty)
550+
{
551+
// no rude edits
552+
553+
StorePendingUpdate(new PendingSolutionUpdate(
554+
solution,
555+
solutionUpdate.ProjectsToStale,
556+
// if partial updates are not allowed we don't treat rebuild as part of solution update:
557+
projectsToRebuild: [],
558+
solutionUpdate.ProjectBaselines,
559+
solutionUpdate.ModuleUpdates.Updates,
560+
solutionUpdate.NonRemappableRegions));
561+
}
554562

555563
break;
556564

@@ -559,8 +567,8 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
559567
Contract.ThrowIfFalse(solutionUpdate.NonRemappableRegions.IsEmpty);
560568

561569
// Insignificant changes should not cause rebuilds/restarts:
562-
Contract.ThrowIfFalse(projectsToRestart.IsEmpty);
563-
Contract.ThrowIfFalse(projectsToRebuild.IsEmpty);
570+
Contract.ThrowIfFalse(solutionUpdate.ProjectsToRestart.IsEmpty);
571+
Contract.ThrowIfFalse(solutionUpdate.ProjectsToRebuild.IsEmpty);
564572

565573
// No significant changes have been made.
566574
// Commit the solution to apply any insignificant changes that do not generate updates.
@@ -573,11 +581,14 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
573581
return new EmitSolutionUpdateResults()
574582
{
575583
Solution = solution,
576-
ModuleUpdates = solutionUpdate.ModuleUpdates,
584+
// If partial updates are disabled the debugger does not expect module updates when rude edits are reported:
585+
ModuleUpdates = allowPartialUpdates || solutionUpdate.ProjectsToRebuild.IsEmpty
586+
? solutionUpdate.ModuleUpdates
587+
: new ModuleUpdates(solutionUpdate.ModuleUpdates.Status, []),
577588
Diagnostics = solutionUpdate.Diagnostics,
578589
SyntaxError = solutionUpdate.SyntaxError,
579-
ProjectsToRestart = projectsToRestart,
580-
ProjectsToRebuild = projectsToRebuild
590+
ProjectsToRestart = solutionUpdate.ProjectsToRestart,
591+
ProjectsToRebuild = solutionUpdate.ProjectsToRebuild
581592
};
582593
}
583594

src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6-
using System.Linq;
7-
using System.Diagnostics;
86
using System.Collections.Generic;
97
using System.Collections.Immutable;
8+
using System.Diagnostics;
9+
using System.Linq;
1010
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
1111
using Microsoft.CodeAnalysis.Diagnostics;
1212

src/Features/Core/Portable/EditAndContinue/EditSession.cs

Lines changed: 44 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ internal static async IAsyncEnumerable<DocumentId> GetChangedDocumentsAsync(Trac
552552
ArrayBuilder<Diagnostic> diagnostics,
553553
CancellationToken cancellationToken)
554554
{
555-
using var _2 = ArrayBuilder<(Document? oldDocument, Document? newDocument)>.GetInstance(out var documents);
555+
using var _ = ArrayBuilder<(Document? oldDocument, Document? newDocument)>.GetInstance(out var documents);
556556
var hasOutOfSyncDocument = false;
557557

558558
foreach (var newDocument in documentDifferences.ChangedOrAdded)
@@ -833,7 +833,12 @@ internal static void MergePartialEdits(
833833
addedSymbols = [.. addedSymbolsBuilder];
834834
}
835835

836-
public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(Solution solution, ActiveStatementSpanProvider solutionActiveStatementSpanProvider, UpdateId updateId, CancellationToken cancellationToken)
836+
public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(
837+
Solution solution,
838+
ActiveStatementSpanProvider solutionActiveStatementSpanProvider,
839+
UpdateId updateId,
840+
ImmutableDictionary<ProjectId, RunningProjectInfo> runningProjects,
841+
CancellationToken cancellationToken)
837842
{
838843
var projectDiagnostics = ArrayBuilder<Diagnostic>.GetInstance();
839844

@@ -875,8 +880,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
875880

876881
var oldSolution = DebuggingSession.LastCommittedSolution;
877882

878-
var blockUpdates = false;
879-
var hasEmitErrors = false;
883+
var hasPersistentErrors = false;
880884
foreach (var newProject in solution.Projects)
881885
{
882886
try
@@ -934,7 +938,6 @@ void UpdateChangedDocumentsStaleness(bool isStale)
934938
projectDiagnostics.Add(mvidReadError);
935939

936940
Telemetry.LogProjectAnalysisSummary(ProjectAnalysisSummary.ValidChanges, newProject.State.ProjectInfo.Attributes.TelemetryId, projectDiagnostics);
937-
blockUpdates = true;
938941
continue;
939942
}
940943

@@ -963,15 +966,6 @@ void UpdateChangedDocumentsStaleness(bool isStale)
963966
var (changedDocumentAnalyses, hasOutOfSyncChangedDocument) =
964967
await AnalyzeDocumentsAsync(solution, documentDifferences, solutionActiveStatementSpanProvider, projectDiagnostics, cancellationToken).ConfigureAwait(false);
965968

966-
// The diagnostic hasn't been reported by GetDocumentDiagnosticsAsync since out-of-sync documents are likely to be synchronized
967-
// before the changes are attempted to be applied. If we still have any out-of-sync documents we report warnings and ignore changes in them.
968-
// If in future the file is updated so that its content matches the PDB checksum, the document transitions to a matching state,
969-
// and we consider any further changes to it for application.
970-
if (projectDiagnostics.Any(static d => d.IsDocumentReadError()))
971-
{
972-
blockUpdates = true;
973-
}
974-
975969
if (hasOutOfSyncChangedDocument)
976970
{
977971
// The project is considered stale as long as it has at least one document that is out-of-sync.
@@ -992,6 +986,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
992986
{
993987
// only remember the first syntax error we encounter:
994988
syntaxError ??= changedDocumentAnalysis.SyntaxError;
989+
hasPersistentErrors = true;
995990

996991
Log.Write($"Changed document '{changedDocumentAnalysis.FilePath}' has syntax error: {changedDocumentAnalysis.SyntaxError}");
997992
}
@@ -1015,17 +1010,6 @@ void UpdateChangedDocumentsStaleness(bool isStale)
10151010
// an additional process that doesn't support EnC (or detaches from such process). Before we apply edits
10161011
// we need to check with the debugger.
10171012
var moduleBlockingDiagnosticId = await ReportModuleDiagnosticsAsync(mvid, oldProject, newProject, changedDocumentAnalyses, projectDiagnostics, cancellationToken).ConfigureAwait(false);
1018-
var isModuleEncBlocked = moduleBlockingDiagnosticId != null;
1019-
1020-
if (isModuleEncBlocked)
1021-
{
1022-
blockUpdates = true;
1023-
}
1024-
1025-
if (projectSummary is ProjectAnalysisSummary.SyntaxErrors or ProjectAnalysisSummary.RudeEdits)
1026-
{
1027-
blockUpdates = true;
1028-
}
10291013

10301014
// Report rude edit diagnostics - these can be blocking (errors) or non-blocking (warnings):
10311015
foreach (var analysis in changedDocumentAnalyses)
@@ -1044,7 +1028,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
10441028
}
10451029
}
10461030

1047-
if (isModuleEncBlocked || projectSummary != ProjectAnalysisSummary.ValidChanges)
1031+
if (moduleBlockingDiagnosticId != null || projectSummary != ProjectAnalysisSummary.ValidChanges)
10481032
{
10491033
Telemetry.LogProjectAnalysisSummary(projectSummary, newProject.State.ProjectInfo.Attributes.TelemetryId, projectDiagnostics);
10501034

@@ -1062,7 +1046,6 @@ void UpdateChangedDocumentsStaleness(bool isStale)
10621046
// This is consistent with reporting compilation errors - the IDE reports them for all TFMs regardless of what framework the app is running on.
10631047
Telemetry.LogProjectAnalysisSummary(projectSummary, newProject.State.ProjectInfo.Attributes.TelemetryId, projectDiagnostics);
10641048

1065-
blockUpdates = true;
10661049
await LogDocumentChangesAsync(generation: null, cancellationToken).ConfigureAwait(false);
10671050
continue;
10681051
}
@@ -1136,8 +1119,12 @@ void UpdateChangedDocumentsStaleness(bool isStale)
11361119

11371120
if (!emitResult.Success)
11381121
{
1139-
// error
1140-
blockUpdates = hasEmitErrors = true;
1122+
hasPersistentErrors = true;
1123+
1124+
// Stop emitting deltas, we will discard the updates emitted so far.
1125+
// Persistent errors need to be fixed before we attempt rebuilding the projects.
1126+
// The baseline solution snapshot will not be moved forward and next call to
1127+
// EmitSolutionUpdatesAsync will calculate changes for all updated projects again.
11411128
break;
11421129
}
11431130

@@ -1147,8 +1134,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
11471134
if (unsupportedChangesDiagnostic is not null)
11481135
{
11491136
projectDiagnostics.Add(unsupportedChangesDiagnostic);
1150-
blockUpdates = true;
1151-
break;
1137+
continue;
11521138
}
11531139

11541140
var updatedMethodTokens = emitResult.UpdatedMethods.SelectAsArray(h => MetadataTokens.GetToken(h));
@@ -1236,30 +1222,36 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
12361222
}
12371223
}
12381224

1239-
// log capabilities for edit sessions with changes or reported errors:
1240-
if (blockUpdates || deltas.Count > 0)
1225+
var diagnostics = diagnosticBuilders.SelectAsArray(entry => new ProjectDiagnostics(entry.Key, entry.Value.ToImmutableAndFree()));
1226+
1227+
Telemetry.LogRuntimeCapabilities(await Capabilities.GetValueAsync(cancellationToken).ConfigureAwait(false));
1228+
1229+
if (hasPersistentErrors)
12411230
{
1242-
Telemetry.LogRuntimeCapabilities(await Capabilities.GetValueAsync(cancellationToken).ConfigureAwait(false));
1231+
return SolutionUpdate.Empty(diagnostics, syntaxError, ModuleUpdateStatus.Blocked);
12431232
}
12441233

1245-
var diagnostics = diagnosticBuilders.SelectAsArray(entry => new ProjectDiagnostics(entry.Key, entry.Value.ToImmutableAndFree()));
1246-
1247-
var update = blockUpdates
1248-
? SolutionUpdate.Empty(
1249-
diagnostics,
1250-
syntaxError,
1251-
syntaxError != null || hasEmitErrors ? ModuleUpdateStatus.Blocked : ModuleUpdateStatus.RestartRequired)
1252-
: new SolutionUpdate(
1253-
new ModuleUpdates(
1254-
(deltas.Count > 0) ? ModuleUpdateStatus.Ready : ModuleUpdateStatus.None,
1255-
deltas.ToImmutable()),
1256-
projectsToStale.ToImmutable(),
1257-
nonRemappableRegions.ToImmutable(),
1258-
newProjectBaselines.ToImmutable(),
1259-
diagnostics,
1260-
syntaxError);
1261-
1262-
return update;
1234+
var updates = deltas.ToImmutable();
1235+
1236+
EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart(
1237+
solution,
1238+
updates,
1239+
diagnostics,
1240+
runningProjects,
1241+
out var projectsToRestart,
1242+
out var projectsToRebuild);
1243+
1244+
var moduleUpdates = new ModuleUpdates(deltas.IsEmpty && projectsToRebuild.IsEmpty ? ModuleUpdateStatus.None : ModuleUpdateStatus.Ready, updates);
1245+
1246+
return new SolutionUpdate(
1247+
moduleUpdates,
1248+
projectsToStale.ToImmutable(),
1249+
nonRemappableRegions.ToImmutable(),
1250+
newProjectBaselines.ToImmutable(),
1251+
diagnostics,
1252+
syntaxError,
1253+
projectsToRestart,
1254+
projectsToRebuild);
12631255
}
12641256
catch (Exception e) when (LogException(e) && FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
12651257
{

src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ internal ImmutableArray<ManagedHotReloadDiagnostic> GetAllDiagnostics()
7272

7373
return builder.ToImmutableAndClear();
7474
}
75+
76+
public static Data CreateFromInternalError(Solution solution, string errorMessage, ImmutableDictionary<ProjectId, RunningProjectInfo> runningProjects)
77+
{
78+
ImmutableArray<DiagnosticData> diagnostics = [];
79+
var firstProject = solution.GetProject(runningProjects.FirstOrDefault().Key) ?? solution.Projects.First();
80+
var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.CannotApplyChangesUnexpectedError);
81+
82+
var diagnostic = Diagnostic.Create(
83+
descriptor,
84+
Location.None,
85+
string.Format(descriptor.MessageFormat.ToString(), "", errorMessage));
86+
87+
return new()
88+
{
89+
ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Ready, []),
90+
Diagnostics = [DiagnosticData.Create(diagnostic, firstProject)],
91+
SyntaxError = null,
92+
ProjectsToRebuild = [.. runningProjects.Keys],
93+
ProjectsToRestart = runningProjects.Keys.ToImmutableDictionary(keySelector: static p => p, elementSelector: static p => ImmutableArray.Create(p))
94+
};
95+
}
7596
}
7697

7798
public static readonly EmitSolutionUpdateResults Empty = new()
@@ -160,7 +181,7 @@ public Data Dehydrate()
160181
/// </param>
161182
internal static void GetProjectsToRebuildAndRestart(
162183
Solution solution,
163-
ModuleUpdates moduleUpdates,
184+
ImmutableArray<ManagedHotReloadUpdate> moduleUpdates,
164185
ImmutableArray<ProjectDiagnostics> diagnostics,
165186
ImmutableDictionary<ProjectId, RunningProjectInfo> runningProjects,
166187
out ImmutableDictionary<ProjectId, ImmutableArray<ProjectId>> projectsToRestart,
@@ -173,7 +194,7 @@ internal static void GetProjectsToRebuildAndRestart(
173194
Debug.Assert(diagnostics
174195
.Where(r => r.Diagnostics.Any(static d => d.Severity == DiagnosticSeverity.Error))
175196
.Select(r => r.ProjectId)
176-
.Intersect(moduleUpdates.Updates.Select(u => u.ProjectId))
197+
.Intersect(moduleUpdates.Select(u => u.ProjectId))
177198
.IsEmpty());
178199

179200
var graph = solution.GetProjectDependencyGraph();
@@ -234,7 +255,7 @@ internal static void GetProjectsToRebuildAndRestart(
234255
// Partial solution update not supported.
235256
if (projectsToRestartBuilder.Any())
236257
{
237-
foreach (var update in moduleUpdates.Updates)
258+
foreach (var update in moduleUpdates)
238259
{
239260
AddImpactedRunningProjects(impactedRunningProjects, update.ProjectId, isBlocking: true);
240261

@@ -247,7 +268,7 @@ internal static void GetProjectsToRebuildAndRestart(
247268
}
248269
}
249270
}
250-
else if (!moduleUpdates.Updates.IsEmpty && projectsToRestartBuilder.Count > 0)
271+
else if (!moduleUpdates.IsEmpty && projectsToRestartBuilder.Count > 0)
251272
{
252273
// The set of updated projects is usually much smaller than the number of all projects in the solution.
253274
// We iterate over this set updating the reset set until no new project is added to the reset set.
@@ -261,7 +282,7 @@ internal static void GetProjectsToRebuildAndRestart(
261282
using var _7 = ArrayBuilder<ProjectId>.GetInstance(out var updatedProjectsToRemove);
262283
using var _8 = PooledHashSet<ProjectId>.GetInstance(out var projectsThatCausedRestart);
263284

264-
updatedProjects.AddRange(moduleUpdates.Updates.Select(static u => u.ProjectId));
285+
updatedProjects.AddRange(moduleUpdates.Select(static u => u.ProjectId));
265286

266287
while (true)
267288
{

src/Features/Core/Portable/EditAndContinue/ModuleUpdateStatus.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,13 @@ internal enum ModuleUpdateStatus
1515
None = 0,
1616

1717
/// <summary>
18-
/// All changes are valid, can be applied.
18+
/// Changes can be applied (project might need rebuild in presence of transient errors).
1919
/// </summary>
2020
Ready = 1,
2121

22-
/// <summary>
23-
/// Changes require restarting the application in order to be applied.
24-
/// </summary>
25-
RestartRequired = 2,
26-
2722
/// <summary>
2823
/// Some changes are errors that block rebuild of the module.
2924
/// This means that the code is in a broken state that cannot be resolved by restarting the application.
3025
/// </summary>
31-
Blocked = 3
26+
Blocked = 2
3227
}

0 commit comments

Comments
 (0)