Skip to content

EnC: Partial solution updates #78744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,9 @@ await TryGetMatchingSourceTextAsync(log, sourceText, sourceFilePath, currentDocu
}
}

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

lock (_guard)
{
Expand Down
143 changes: 99 additions & 44 deletions src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ internal sealed class DebuggingSession : IDisposable
/// read lock is acquired before every operation that may access a baseline module/symbol reader
/// and write lock when the baseline readers are being disposed.
/// </summary>
private readonly ReaderWriterLockSlim _baselineAccessLock = new();
private readonly ReaderWriterLockSlim _baselineContentAccessLock = new();
private bool _isDisposed;

internal EditSession EditSession { get; private set; }
Expand Down Expand Up @@ -168,7 +168,7 @@ public void Dispose()
_cancellationSource.Dispose();

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

lock (_projectEmitBaselinesGuard)
{
Expand All @@ -179,8 +179,8 @@ public void Dispose()
}
}

_baselineAccessLock.ExitWriteLock();
_baselineAccessLock.Dispose();
_baselineContentAccessLock.ExitWriteLock();
_baselineContentAccessLock.Dispose();

if (Interlocked.Exchange(ref _pendingUpdate, null) != null)
{
Expand Down Expand Up @@ -312,7 +312,7 @@ internal ImmutableList<ProjectBaseline> GetOrCreateEmitBaselines(
ArrayBuilder<Diagnostic> diagnostics,
out ReaderWriterLockSlim baselineAccessLock)
{
baselineAccessLock = _baselineAccessLock;
baselineAccessLock = _baselineContentAccessLock;

ImmutableList<ProjectBaseline>? existingBaselines;
lock (_projectEmitBaselinesGuard)
Expand Down Expand Up @@ -521,53 +521,74 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
// Make sure the solution snapshot has all source-generated documents up-to-date.
solution = solution.WithUpToDateSourceGeneratorDocuments(solution.ProjectIds);

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

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

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

switch (solutionUpdate.ModuleUpdates.Status)
{
case ModuleUpdateStatus.Ready:
// We have updates to be applied. The debugger will call Commit/Discard on the solution
Contract.ThrowIfTrue(solutionUpdate.ModuleUpdates.Updates.IsEmpty && solutionUpdate.ProjectsToRebuild.IsEmpty);

// We have updates to be applied or processes to restart. The debugger will call Commit/Discard on the solution
// based on whether the updates will be applied successfully or not.
StorePendingUpdate(new PendingSolutionUpdate(
solution,
solutionUpdate.ProjectsToStale,
solutionUpdate.ProjectBaselines,
solutionUpdate.ModuleUpdates.Updates,
solutionUpdate.NonRemappableRegions));

if (allowPartialUpdates)
{
StorePendingUpdate(new PendingSolutionUpdate(
solution,
solutionUpdate.ProjectsToStale,
solutionUpdate.ProjectsToRebuild,
solutionUpdate.ProjectBaselines,
solutionUpdate.ModuleUpdates.Updates,
solutionUpdate.NonRemappableRegions));
}
else if (solutionUpdate.ProjectsToRebuild.IsEmpty)
{
// no rude edits

StorePendingUpdate(new PendingSolutionUpdate(
solution,
solutionUpdate.ProjectsToStale,
// if partial updates are not allowed we don't treat rebuild as part of solution update:
projectsToRebuild: [],
solutionUpdate.ProjectBaselines,
solutionUpdate.ModuleUpdates.Updates,
solutionUpdate.NonRemappableRegions));
}

break;

case ModuleUpdateStatus.None:
Contract.ThrowIfFalse(solutionUpdate.ModuleUpdates.Updates.IsEmpty);
Contract.ThrowIfFalse(solutionUpdate.NonRemappableRegions.IsEmpty);

// Insignificant changes should not cause rebuilds/restarts:
Contract.ThrowIfFalse(solutionUpdate.ProjectsToRestart.IsEmpty);
Contract.ThrowIfFalse(solutionUpdate.ProjectsToRebuild.IsEmpty);

// No significant changes have been made.
// Commit the solution to apply any insignificant changes that do not generate updates.
LastCommittedSolution.CommitChanges(solution, projectsToStale: solutionUpdate.ProjectsToStale, projectsToUnstale: []);
break;
}

EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart(
solution,
solutionUpdate.ModuleUpdates,
solutionUpdate.Diagnostics,
runningProjects,
out var projectsToRestart,
out var projectsToRebuild);

// Note that we may return empty deltas if all updates have been deferred.
// The debugger will still call commit or discard on the update batch.
return new EmitSolutionUpdateResults()
{
Solution = solution,
ModuleUpdates = solutionUpdate.ModuleUpdates,
// If partial updates are disabled the debugger does not expect module updates when rude edits are reported:
ModuleUpdates = allowPartialUpdates || solutionUpdate.ProjectsToRebuild.IsEmpty
? solutionUpdate.ModuleUpdates
: new ModuleUpdates(solutionUpdate.ModuleUpdates.Status, []),
Diagnostics = solutionUpdate.Diagnostics,
SyntaxError = solutionUpdate.SyntaxError,
ProjectsToRestart = projectsToRestart,
ProjectsToRebuild = projectsToRebuild
ProjectsToRestart = solutionUpdate.ProjectsToRestart,
ProjectsToRebuild = solutionUpdate.ProjectsToRebuild
};
}

Expand All @@ -576,6 +597,7 @@ public void CommitSolutionUpdate()
ThrowIfDisposed();

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

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

LastCommittedSolution.CommitChanges(pendingSolutionUpdate.Solution, projectsToStale: pendingSolutionUpdate.ProjectsToStale, projectsToUnstale: []);
var solution = pendingSolutionUpdate.Solution;

// Once the project is rebuilt all its dependencies are going to be up-to-date.
var dependencyGraph = solution.GetProjectDependencyGraph();
foreach (var projectId in pendingSolutionUpdate.ProjectsToRebuild)
{
projectsToRebuildTransitive.Add(projectId);
projectsToRebuildTransitive.AddRange(dependencyGraph.GetProjectsThatThisProjectTransitivelyDependsOn(projectId));
}

// Unstale all projects that will be up-to-date after rebuild.
LastCommittedSolution.CommitChanges(solution, projectsToStale: pendingSolutionUpdate.ProjectsToStale, projectsToUnstale: projectsToRebuildTransitive);

foreach (var projectId in projectsToRebuildTransitive)
{
_editSessionTelemetry.LogUpdatedBaseline(solution.GetRequiredProject(projectId).State.ProjectInfo.Attributes.TelemetryId);
}
}

// update baselines:

// Wait for all operations on baseline content to finish before we dispose the readers.
_baselineContentAccessLock.EnterWriteLock();

lock (_projectEmitBaselinesGuard)
{
foreach (var updatedBaseline in pendingUpdate.ProjectBaselines)
{
_projectBaselines[updatedBaseline.ProjectId] = [.. _projectBaselines[updatedBaseline.ProjectId].Select(existingBaseline => existingBaseline.ModuleId == updatedBaseline.ModuleId ? updatedBaseline : existingBaseline)];
}

// Discard any open baseline readers for projects that need to be rebuilt,
// so that the build can overwrite the underlying files.
DiscardProjectBaselinesNoLock(projectsToRebuildTransitive);
}

_baselineContentAccessLock.ExitWriteLock();

_editSessionTelemetry.LogCommitted();

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

private void DiscardProjectBaselinesNoLock(IEnumerable<ProjectId> projects)
{
foreach (var projectId in projects)
{
if (_projectBaselines.TryGetValue(projectId, out var projectBaselines))
{
// remove all versions of modules associated with the project:
_projectBaselines.Remove(projectId);

foreach (var projectBaseline in projectBaselines)
{
var (metadata, pdb) = _initialBaselineModuleReaders[projectBaseline.ModuleId];
metadata.Dispose();
pdb.Dispose();

_initialBaselineModuleReaders.Remove(projectBaseline.ModuleId);
}
}
}
}

// TODO: remove once the debugger implements https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2459003
public void UpdateBaselines(Solution solution, ImmutableArray<ProjectId> rebuiltProjects)
{
ThrowIfDisposed();
Expand All @@ -625,30 +695,15 @@ public void UpdateBaselines(Solution solution, ImmutableArray<ProjectId> rebuilt
LastCommittedSolution.CommitChanges(solution, projectsToStale: [], projectsToUnstale: rebuiltProjects);

// Wait for all operations on baseline to finish before we dispose the readers.
_baselineAccessLock.EnterWriteLock();

_baselineContentAccessLock.EnterWriteLock();

lock (_projectEmitBaselinesGuard)
{
foreach (var projectId in rebuiltProjects)
{
if (_projectBaselines.TryGetValue(projectId, out var projectBaselines))
{
// remove all versions of modules associated with the project:
_projectBaselines.Remove(projectId);

foreach (var projectBaseline in projectBaselines)
{
var (metadata, pdb) = _initialBaselineModuleReaders[projectBaseline.ModuleId];
metadata.Dispose();
pdb.Dispose();

_initialBaselineModuleReaders.Remove(projectBaseline.ModuleId);
}
}
}
DiscardProjectBaselinesNoLock(rebuiltProjects);
}

_baselineAccessLock.ExitWriteLock();
_baselineContentAccessLock.ExitWriteLock();

foreach (var projectId in rebuiltProjects)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Linq;
using System.Diagnostics;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
using Microsoft.CodeAnalysis.Diagnostics;

Expand Down
Loading
Loading