Skip to content

Commit

Permalink
EnC refactoring: ProjectBaseline, ProjectDiagnostics, PendingUpdate (#…
Browse files Browse the repository at this point in the history
…67941)

* EnC refactoring: ProjectBaseline, ProjectDiagnostics, PendingUpdate

* Fix

* FIx
  • Loading branch information
tmat authored Apr 27, 2023
1 parent 5b47c7f commit c2a3eb2
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 207 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ private static void EndDebuggingSession(DebuggingSession session, ImmutableArray
ActiveStatementSpanProvider activeStatementSpanProvider = null)
{
var result = await session.EmitSolutionUpdateAsync(solution, activeStatementSpanProvider ?? s_noActiveSpans, CancellationToken.None);
return (result.ModuleUpdates, result.GetDiagnosticData(solution));
return (result.ModuleUpdates, result.Diagnostics.ToDiagnosticData(solution));
}

internal static void SetDocumentsState(DebuggingSession session, Solution solution, CommittedSolution.DocumentState state)
Expand Down Expand Up @@ -2733,10 +2733,10 @@ void ValidateDelta(ModuleUpdate delta)

// the update should be stored on the service:
var pendingUpdate = debuggingSession.GetTestAccessor().GetPendingSolutionUpdate();
var (baselineProjectId, newBaseline) = pendingUpdate.EmitBaselines.Single();
var newBaseline = pendingUpdate.ProjectBaselines.Single();
AssertEx.Equal(updates.Updates, pendingUpdate.Deltas);
Assert.Equal(document2.Project.Id, baselineProjectId);
Assert.Equal(moduleId, newBaseline.OriginalMetadata.GetModuleVersionId());
Assert.Equal(document2.Project.Id, newBaseline.ProjectId);
Assert.Equal(moduleId, newBaseline.EmitBaseline.OriginalMetadata.GetModuleVersionId());

var readers = debuggingSession.GetTestAccessor().GetBaselineModuleReaders();
Assert.Equal(2, readers.Length);
Expand All @@ -2759,7 +2759,7 @@ void ValidateDelta(ModuleUpdate delta)
Assert.Same(readers[1], baselineReaders[1]);

// verify that baseline is added:
Assert.Same(newBaseline, debuggingSession.GetTestAccessor().GetProjectEmitBaseline(document2.Project.Id));
Assert.Same(newBaseline.EmitBaseline, debuggingSession.GetTestAccessor().GetProjectEmitBaseline(document2.Project.Id));

// solution update status after committing an update:
(updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution);
Expand Down Expand Up @@ -2868,15 +2868,15 @@ public async Task ValidSignificantChange_EmitSuccessful_UpdateDeferred(bool comm

// the update should be stored on the service:
var pendingUpdate = debuggingSession.GetTestAccessor().GetPendingSolutionUpdate();
var (baselineProjectId, newBaseline) = pendingUpdate.EmitBaselines.Single();
var newBaseline = pendingUpdate.ProjectBaselines.Single();

var readers = debuggingSession.GetTestAccessor().GetBaselineModuleReaders();
Assert.Equal(2, readers.Length);
Assert.NotNull(readers[0]);
Assert.NotNull(readers[1]);

Assert.Equal(document2.Project.Id, baselineProjectId);
Assert.Equal(moduleId, newBaseline.OriginalMetadata.GetModuleVersionId());
Assert.Equal(document2.Project.Id, newBaseline.ProjectId);
Assert.Equal(moduleId, newBaseline.EmitBaseline.OriginalMetadata.GetModuleVersionId());

if (commitUpdate)
{
Expand All @@ -2887,7 +2887,7 @@ public async Task ValidSignificantChange_EmitSuccessful_UpdateDeferred(bool comm
Assert.Empty(debuggingSession.EditSession.NonRemappableRegions);

// verify that baseline is added:
Assert.Same(newBaseline, debuggingSession.GetTestAccessor().GetProjectEmitBaseline(document2.Project.Id));
Assert.Same(newBaseline.EmitBaseline, debuggingSession.GetTestAccessor().GetProjectEmitBaseline(document2.Project.Id));

// solution update status after committing an update:
ExitBreakState(debuggingSession);
Expand Down Expand Up @@ -3375,8 +3375,8 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule()

// the update should be stored on the service:
var pendingUpdate = debuggingSession.GetTestAccessor().GetPendingSolutionUpdate();
var (_, newBaselineA1) = pendingUpdate.EmitBaselines.Single(b => b.ProjectId == projectA.Id);
var (_, newBaselineB1) = pendingUpdate.EmitBaselines.Single(b => b.ProjectId == projectB.Id);
var newBaselineA1 = pendingUpdate.ProjectBaselines.Single(b => b.ProjectId == projectA.Id).EmitBaseline;
var newBaselineB1 = pendingUpdate.ProjectBaselines.Single(b => b.ProjectId == projectB.Id).EmitBaseline;

var baselineA0 = newBaselineA1.GetInitialEmitBaseline();
var baselineB0 = newBaselineB1.GetInitialEmitBaseline();
Expand Down Expand Up @@ -3423,8 +3423,8 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule()

// the update should be stored on the service:
pendingUpdate = debuggingSession.GetTestAccessor().GetPendingSolutionUpdate();
var (_, newBaselineA2) = pendingUpdate.EmitBaselines.Single(b => b.ProjectId == projectA.Id);
var (_, newBaselineB2) = pendingUpdate.EmitBaselines.Single(b => b.ProjectId == projectB.Id);
var newBaselineA2 = pendingUpdate.ProjectBaselines.Single(b => b.ProjectId == projectA.Id).EmitBaseline;
var newBaselineB2 = pendingUpdate.ProjectBaselines.Single(b => b.ProjectId == projectB.Id).EmitBaseline;

Assert.NotSame(newBaselineA1, newBaselineA2);
Assert.NotSame(newBaselineB1, newBaselineB2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Remote.Testing;
Expand Down Expand Up @@ -225,10 +226,16 @@ void VerifyReanalyzeInvocation(ImmutableArray<DocumentId> documentIds)
var syntaxError = Diagnostic.Create(diagnosticDescriptor1, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)), new[] { "doc", "syntax error" });

var updates = new ModuleUpdates(ModuleUpdateStatus.Ready, deltas);
var diagnostics = ImmutableArray.Create((project.Id, ImmutableArray.Create(documentDiagnostic, projectDiagnostic)));
var diagnostics = ImmutableArray.Create(new ProjectDiagnostics(project.Id, ImmutableArray.Create(documentDiagnostic, projectDiagnostic)));
var documentsWithRudeEdits = ImmutableArray.Create((documentId, ImmutableArray<RudeEditDiagnostic>.Empty));

return new(updates, diagnostics, documentsWithRudeEdits, syntaxError);
return new()
{
ModuleUpdates = updates,
Diagnostics = diagnostics,
RudeEdits = documentsWithRudeEdits,
SyntaxError = syntaxError
};
};

var (updates, _, _, syntaxErrorData) = await sessionProxy.EmitSolutionUpdateAsync(localWorkspace.CurrentSolution, activeStatementSpanProvider, mockDiagnosticService, diagnosticUpdateSource, CancellationToken.None);
Expand Down
119 changes: 43 additions & 76 deletions src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ internal sealed class DebuggingSession : IDisposable
/// <remarks>
/// The baseline of each updated project is linked to its initial baseline that reads from the on-disk metadata and PDB.
/// Therefore once an initial baseline is created it needs to be kept alive till the end of the debugging session,
/// even when it's replaced in <see cref="_projectEmitBaselines"/> by a newer baseline.
/// even when it's replaced in <see cref="_projectBaselines"/> by a newer baseline.
/// </remarks>
private readonly Dictionary<ProjectId, (EmitBaseline Baseline, int Generation)> _projectEmitBaselines = new();
private readonly Dictionary<ProjectId, ProjectBaseline> _projectBaselines = new();
private readonly List<IDisposable> _initialBaselineModuleReaders = new();
private readonly object _projectEmitBaselinesGuard = new();

Expand Down Expand Up @@ -93,7 +93,7 @@ internal sealed class DebuggingSession : IDisposable
private readonly DebuggingSessionTelemetry _telemetry;
private readonly EditSessionTelemetry _editSessionTelemetry = new();

private PendingSolutionUpdate? _pendingUpdate;
private PendingUpdate? _pendingUpdate;
private Action<DebuggingSessionTelemetry.Data> _reportTelemetry;

/// <summary>
Expand Down Expand Up @@ -161,13 +161,9 @@ internal void ThrowIfDisposed()
throw new ObjectDisposedException(nameof(DebuggingSession));
}

private void StorePendingUpdate(Solution solution, SolutionUpdate update)
private void StorePendingUpdate(PendingUpdate update)
{
var previousPendingUpdate = Interlocked.Exchange(ref _pendingUpdate, new PendingSolutionUpdate(
solution,
update.EmitBaselines,
update.ModuleUpdates.Updates,
update.NonRemappableRegions));
var previousPendingUpdate = Interlocked.Exchange(ref _pendingUpdate, update);

// commit/discard was not called:
if (previousPendingUpdate != null)
Expand All @@ -176,7 +172,7 @@ private void StorePendingUpdate(Solution solution, SolutionUpdate update)
}
}

private PendingSolutionUpdate RetrievePendingUpdate()
private PendingUpdate RetrievePendingUpdate()
{
var pendingUpdate = Interlocked.Exchange(ref _pendingUpdate, null);
if (pendingUpdate == null)
Expand Down Expand Up @@ -300,7 +296,7 @@ private bool AddModulePreparedForUpdate(Guid mvid)
}
}

private bool TryGetProjectId(Guid moduleId, [NotNullWhen(true)] out ProjectId? projectId)
internal bool TryGetProjectId(Guid moduleId, [NotNullWhen(true)] out ProjectId? projectId)
{
lock (_projectModuleIdsGuard)
{
Expand All @@ -315,17 +311,15 @@ private bool TryGetProjectId(Guid moduleId, [NotNullWhen(true)] out ProjectId? p
internal bool TryGetOrCreateEmitBaseline(
Project project,
out ImmutableArray<Diagnostic> diagnostics,
[NotNullWhen(true)] out EmitBaseline? baseline,
out int baselineGeneration,
[NotNullWhen(true)] out ProjectBaseline? baseline,
[NotNullWhen(true)] out ReaderWriterLockSlim? baselineAccessLock)
{
baselineAccessLock = _baselineAccessLock;

lock (_projectEmitBaselinesGuard)
{
if (_projectEmitBaselines.TryGetValue(project.Id, out var baselineAndGeneration))
if (_projectBaselines.TryGetValue(project.Id, out baseline))
{
(baseline, baselineGeneration) = baselineAndGeneration;
diagnostics = ImmutableArray<Diagnostic>.Empty;
return true;
}
Expand All @@ -336,31 +330,26 @@ internal bool TryGetOrCreateEmitBaseline(
{
// Unable to read the DLL/PDB at this point (it might be open by another process).
// Don't cache the failure so that the user can attempt to apply changes again.
baselineGeneration = -1;
baseline = null;
return false;
}

const int initialBaselineGeneration = 0;

lock (_projectEmitBaselinesGuard)
{
if (_projectEmitBaselines.TryGetValue(project.Id, out var baselineAndGeneration))
if (_projectBaselines.TryGetValue(project.Id, out baseline))
{
metadataReaderProvider.Dispose();
debugInfoReaderProvider.Dispose();
(baseline, baselineGeneration) = baselineAndGeneration;
return true;
}

_projectEmitBaselines.Add(project.Id, (initialBaseline, initialBaselineGeneration));
baseline = new ProjectBaseline(project.Id, initialBaseline, generation: 0);

_projectBaselines.Add(project.Id, baseline);
_initialBaselineModuleReaders.Add(metadataReaderProvider);
_initialBaselineModuleReaders.Add(debugInfoReaderProvider);
}

baseline = initialBaseline;
baselineGeneration = initialBaselineGeneration;
return true;
}

Expand Down Expand Up @@ -531,83 +520,61 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(

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

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

if (solutionUpdate.ModuleUpdates.Status == ModuleUpdateStatus.Ready)
{
StorePendingUpdate(solution, solutionUpdate);
StorePendingUpdate(new PendingSolutionUpdate(
solution,
solutionUpdate.ProjectBaselines,
solutionUpdate.ModuleUpdates.Updates,
solutionUpdate.NonRemappableRegions));
}

// 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(solutionUpdate.ModuleUpdates, solutionUpdate.Diagnostics, solutionUpdate.DocumentsWithRudeEdits, solutionUpdate.SyntaxError);
}

private void LogSolutionUpdate(SolutionUpdate update, UpdateId updateId)
{
var log = EditAndContinueService.Log;

log.Write("Solution update {0}.{1} status: {2}", updateId.SessionId.Ordinal, updateId.Ordinal, update.ModuleUpdates.Status);

foreach (var moduleUpdate in update.ModuleUpdates.Updates)
return new EmitSolutionUpdateResults()
{
log.Write("Module update: capabilities=[{0}], types=[{1}], methods=[{2}]",
moduleUpdate.RequiredCapabilities,
moduleUpdate.UpdatedTypes,
moduleUpdate.UpdatedMethods);
}

if (update.Diagnostics.Length > 0)
{
var firstProjectDiagnostic = update.Diagnostics[0];

log.Write("Solution update diagnostics: #{0} [{1}: {2}, ...]",
update.Diagnostics.Length,
firstProjectDiagnostic.ProjectId,
firstProjectDiagnostic.Diagnostics[0]);
}

if (update.DocumentsWithRudeEdits.Length > 0)
{
var firstDocumentWithRudeEdits = update.DocumentsWithRudeEdits[0];

log.Write("Solution update documents with rude edits: #{0} [{1}: {2}, ...]",
update.DocumentsWithRudeEdits.Length,
firstDocumentWithRudeEdits.DocumentId,
firstDocumentWithRudeEdits.Diagnostics[0].Kind);
}

_lastModuleUpdatesLog = update.ModuleUpdates.Updates;
ModuleUpdates = solutionUpdate.ModuleUpdates,
Diagnostics = solutionUpdate.Diagnostics,
RudeEdits = solutionUpdate.DocumentsWithRudeEdits,
SyntaxError = solutionUpdate.SyntaxError,
};
}

public void CommitSolutionUpdate(out ImmutableArray<DocumentId> documentsToReanalyze)
{
ThrowIfDisposed();

ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>? newNonRemappableRegions = null;

var pendingUpdate = RetrievePendingUpdate();
if (pendingUpdate is PendingSolutionUpdate pendingSolutionUpdate)
{
// Save new non-remappable regions for the next edit session.
// If no edits were made the pending list will be empty and we need to keep the previous regions.

// Save new non-remappable regions for the next edit session.
// If no edits were made the pending list will be empty and we need to keep the previous regions.
newNonRemappableRegions = GroupToImmutableDictionary(
from moduleRegions in pendingSolutionUpdate.NonRemappableRegions
from region in moduleRegions.Regions
group region.Region by new ManagedMethodId(moduleRegions.ModuleId, region.Method));

var newNonRemappableRegions = GroupToImmutableDictionary(
from moduleRegions in pendingUpdate.NonRemappableRegions
from region in moduleRegions.Regions
group region.Region by new ManagedMethodId(moduleRegions.ModuleId, region.Method));
if (newNonRemappableRegions.IsEmpty)
newNonRemappableRegions = null;

if (newNonRemappableRegions.IsEmpty)
newNonRemappableRegions = null;
LastCommittedSolution.CommitSolution(pendingSolutionUpdate.Solution);
}

// update baselines:
lock (_projectEmitBaselinesGuard)
{
foreach (var (projectId, baseline) in pendingUpdate.EmitBaselines)
foreach (var baseline in pendingUpdate.ProjectBaselines)
{
_projectEmitBaselines[projectId] = (baseline, _projectEmitBaselines[projectId].Generation + 1);
_projectBaselines[baseline.ProjectId] = baseline;
}
}

LastCommittedSolution.CommitSolution(pendingUpdate.Solution);

_editSessionTelemetry.LogCommitted();

// Restart edit session with no active statements (switching to run mode).
Expand Down Expand Up @@ -1110,14 +1077,14 @@ public EmitBaseline GetProjectEmitBaseline(ProjectId id)
{
lock (_instance._projectEmitBaselinesGuard)
{
return _instance._projectEmitBaselines[id].Baseline;
return _instance._projectBaselines[id].EmitBaseline;
}
}

public ImmutableArray<IDisposable> GetBaselineModuleReaders()
=> _instance.GetBaselineModuleReaders();

public PendingSolutionUpdate? GetPendingSolutionUpdate()
public PendingUpdate? GetPendingSolutionUpdate()
=> _instance._pendingUpdate;

public void SetTelemetryLogger(Action<FunctionId, LogMessage> logger, Func<int> getNextId)
Expand Down
Loading

0 comments on commit c2a3eb2

Please sign in to comment.