Skip to content

Commit a821574

Browse files
authored
EnC: Partial solution updates (#78744)
1 parent 70435cc commit a821574

17 files changed

+437
-405
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,9 +458,9 @@ await TryGetMatchingSourceTextAsync(log, sourceText, sourceFilePath, currentDocu
458458
}
459459
}
460460

461-
public void CommitChanges(Solution solution, ImmutableArray<ProjectId> projectsToStale, ImmutableArray<ProjectId> projectsToUnstale)
461+
public void CommitChanges(Solution solution, ImmutableArray<ProjectId> projectsToStale, IReadOnlyCollection<ProjectId> projectsToUnstale)
462462
{
463-
Contract.ThrowIfFalse(projectsToStale is [] || projectsToUnstale is []);
463+
Debug.Assert(projectsToStale.Intersect(projectsToUnstale).IsEmpty());
464464

465465
lock (_guard)
466466
{

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

Lines changed: 99 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ internal sealed class DebuggingSession : IDisposable
8282
/// read lock is acquired before every operation that may access a baseline module/symbol reader
8383
/// and write lock when the baseline readers are being disposed.
8484
/// </summary>
85-
private readonly ReaderWriterLockSlim _baselineAccessLock = new();
85+
private readonly ReaderWriterLockSlim _baselineContentAccessLock = new();
8686
private bool _isDisposed;
8787

8888
internal EditSession EditSession { get; private set; }
@@ -168,7 +168,7 @@ public void Dispose()
168168
_cancellationSource.Dispose();
169169

170170
// Wait for all operations on baseline to finish before we dispose the readers.
171-
_baselineAccessLock.EnterWriteLock();
171+
_baselineContentAccessLock.EnterWriteLock();
172172

173173
lock (_projectEmitBaselinesGuard)
174174
{
@@ -179,8 +179,8 @@ public void Dispose()
179179
}
180180
}
181181

182-
_baselineAccessLock.ExitWriteLock();
183-
_baselineAccessLock.Dispose();
182+
_baselineContentAccessLock.ExitWriteLock();
183+
_baselineContentAccessLock.Dispose();
184184

185185
if (Interlocked.Exchange(ref _pendingUpdate, null) != null)
186186
{
@@ -312,7 +312,7 @@ internal ImmutableList<ProjectBaseline> GetOrCreateEmitBaselines(
312312
ArrayBuilder<Diagnostic> diagnostics,
313313
out ReaderWriterLockSlim baselineAccessLock)
314314
{
315-
baselineAccessLock = _baselineAccessLock;
315+
baselineAccessLock = _baselineContentAccessLock;
316316

317317
ImmutableList<ProjectBaseline>? existingBaselines;
318318
lock (_projectEmitBaselinesGuard)
@@ -521,53 +521,74 @@ 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);
525+
526+
var allowPartialUpdates = runningProjects.Any(p => p.Value.AllowPartialUpdate);
525527

526528
solutionUpdate.Log(SessionLog, updateId);
527529
_lastModuleUpdatesLog = solutionUpdate.ModuleUpdates.Updates;
528530

529531
switch (solutionUpdate.ModuleUpdates.Status)
530532
{
531533
case ModuleUpdateStatus.Ready:
532-
// We have updates to be applied. The debugger will call Commit/Discard on the solution
534+
Contract.ThrowIfTrue(solutionUpdate.ModuleUpdates.Updates.IsEmpty && solutionUpdate.ProjectsToRebuild.IsEmpty);
535+
536+
// We have updates to be applied or processes to restart. The debugger will call Commit/Discard on the solution
533537
// based on whether the updates will be applied successfully or not.
534-
StorePendingUpdate(new PendingSolutionUpdate(
535-
solution,
536-
solutionUpdate.ProjectsToStale,
537-
solutionUpdate.ProjectBaselines,
538-
solutionUpdate.ModuleUpdates.Updates,
539-
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+
}
540562

541563
break;
542564

543565
case ModuleUpdateStatus.None:
544566
Contract.ThrowIfFalse(solutionUpdate.ModuleUpdates.Updates.IsEmpty);
545567
Contract.ThrowIfFalse(solutionUpdate.NonRemappableRegions.IsEmpty);
546568

569+
// Insignificant changes should not cause rebuilds/restarts:
570+
Contract.ThrowIfFalse(solutionUpdate.ProjectsToRestart.IsEmpty);
571+
Contract.ThrowIfFalse(solutionUpdate.ProjectsToRebuild.IsEmpty);
572+
547573
// No significant changes have been made.
548574
// Commit the solution to apply any insignificant changes that do not generate updates.
549575
LastCommittedSolution.CommitChanges(solution, projectsToStale: solutionUpdate.ProjectsToStale, projectsToUnstale: []);
550576
break;
551577
}
552578

553-
EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart(
554-
solution,
555-
solutionUpdate.ModuleUpdates,
556-
solutionUpdate.Diagnostics,
557-
runningProjects,
558-
out var projectsToRestart,
559-
out var projectsToRebuild);
560-
561579
// Note that we may return empty deltas if all updates have been deferred.
562580
// The debugger will still call commit or discard on the update batch.
563581
return new EmitSolutionUpdateResults()
564582
{
565583
Solution = solution,
566-
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, []),
567588
Diagnostics = solutionUpdate.Diagnostics,
568589
SyntaxError = solutionUpdate.SyntaxError,
569-
ProjectsToRestart = projectsToRestart,
570-
ProjectsToRebuild = projectsToRebuild
590+
ProjectsToRestart = solutionUpdate.ProjectsToRestart,
591+
ProjectsToRebuild = solutionUpdate.ProjectsToRebuild
571592
};
572593
}
573594

@@ -576,6 +597,7 @@ public void CommitSolutionUpdate()
576597
ThrowIfDisposed();
577598

578599
ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>? newNonRemappableRegions = null;
600+
using var _ = PooledHashSet<ProjectId>.GetInstance(out var projectsToRebuildTransitive);
579601

580602
var pendingUpdate = RetrievePendingUpdate();
581603
if (pendingUpdate is PendingSolutionUpdate pendingSolutionUpdate)
@@ -591,18 +613,44 @@ from region in moduleRegions.Regions
591613
if (newNonRemappableRegions.IsEmpty)
592614
newNonRemappableRegions = null;
593615

594-
LastCommittedSolution.CommitChanges(pendingSolutionUpdate.Solution, projectsToStale: pendingSolutionUpdate.ProjectsToStale, projectsToUnstale: []);
616+
var solution = pendingSolutionUpdate.Solution;
617+
618+
// Once the project is rebuilt all its dependencies are going to be up-to-date.
619+
var dependencyGraph = solution.GetProjectDependencyGraph();
620+
foreach (var projectId in pendingSolutionUpdate.ProjectsToRebuild)
621+
{
622+
projectsToRebuildTransitive.Add(projectId);
623+
projectsToRebuildTransitive.AddRange(dependencyGraph.GetProjectsThatThisProjectTransitivelyDependsOn(projectId));
624+
}
625+
626+
// Unstale all projects that will be up-to-date after rebuild.
627+
LastCommittedSolution.CommitChanges(solution, projectsToStale: pendingSolutionUpdate.ProjectsToStale, projectsToUnstale: projectsToRebuildTransitive);
628+
629+
foreach (var projectId in projectsToRebuildTransitive)
630+
{
631+
_editSessionTelemetry.LogUpdatedBaseline(solution.GetRequiredProject(projectId).State.ProjectInfo.Attributes.TelemetryId);
632+
}
595633
}
596634

597635
// update baselines:
636+
637+
// Wait for all operations on baseline content to finish before we dispose the readers.
638+
_baselineContentAccessLock.EnterWriteLock();
639+
598640
lock (_projectEmitBaselinesGuard)
599641
{
600642
foreach (var updatedBaseline in pendingUpdate.ProjectBaselines)
601643
{
602644
_projectBaselines[updatedBaseline.ProjectId] = [.. _projectBaselines[updatedBaseline.ProjectId].Select(existingBaseline => existingBaseline.ModuleId == updatedBaseline.ModuleId ? updatedBaseline : existingBaseline)];
603645
}
646+
647+
// Discard any open baseline readers for projects that need to be rebuilt,
648+
// so that the build can overwrite the underlying files.
649+
DiscardProjectBaselinesNoLock(projectsToRebuildTransitive);
604650
}
605651

652+
_baselineContentAccessLock.ExitWriteLock();
653+
606654
_editSessionTelemetry.LogCommitted();
607655

608656
// Restart edit session with no active statements (switching to run mode).
@@ -615,6 +663,28 @@ public void DiscardSolutionUpdate()
615663
_ = RetrievePendingUpdate();
616664
}
617665

666+
private void DiscardProjectBaselinesNoLock(IEnumerable<ProjectId> projects)
667+
{
668+
foreach (var projectId in projects)
669+
{
670+
if (_projectBaselines.TryGetValue(projectId, out var projectBaselines))
671+
{
672+
// remove all versions of modules associated with the project:
673+
_projectBaselines.Remove(projectId);
674+
675+
foreach (var projectBaseline in projectBaselines)
676+
{
677+
var (metadata, pdb) = _initialBaselineModuleReaders[projectBaseline.ModuleId];
678+
metadata.Dispose();
679+
pdb.Dispose();
680+
681+
_initialBaselineModuleReaders.Remove(projectBaseline.ModuleId);
682+
}
683+
}
684+
}
685+
}
686+
687+
// TODO: remove once the debugger implements https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2459003
618688
public void UpdateBaselines(Solution solution, ImmutableArray<ProjectId> rebuiltProjects)
619689
{
620690
ThrowIfDisposed();
@@ -625,30 +695,15 @@ public void UpdateBaselines(Solution solution, ImmutableArray<ProjectId> rebuilt
625695
LastCommittedSolution.CommitChanges(solution, projectsToStale: [], projectsToUnstale: rebuiltProjects);
626696

627697
// Wait for all operations on baseline to finish before we dispose the readers.
628-
_baselineAccessLock.EnterWriteLock();
698+
699+
_baselineContentAccessLock.EnterWriteLock();
629700

630701
lock (_projectEmitBaselinesGuard)
631702
{
632-
foreach (var projectId in rebuiltProjects)
633-
{
634-
if (_projectBaselines.TryGetValue(projectId, out var projectBaselines))
635-
{
636-
// remove all versions of modules associated with the project:
637-
_projectBaselines.Remove(projectId);
638-
639-
foreach (var projectBaseline in projectBaselines)
640-
{
641-
var (metadata, pdb) = _initialBaselineModuleReaders[projectBaseline.ModuleId];
642-
metadata.Dispose();
643-
pdb.Dispose();
644-
645-
_initialBaselineModuleReaders.Remove(projectBaseline.ModuleId);
646-
}
647-
}
648-
}
703+
DiscardProjectBaselinesNoLock(rebuiltProjects);
649704
}
650705

651-
_baselineAccessLock.ExitWriteLock();
706+
_baselineContentAccessLock.ExitWriteLock();
652707

653708
foreach (var projectId in rebuiltProjects)
654709
{

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

0 commit comments

Comments
 (0)