Skip to content

Commit

Permalink
Do not dispose baseline PDB reader right after emit. (#38608)
Browse files Browse the repository at this point in the history
The PDB reader might be needed later on for mapping local signature of an active method that is first edited in a subsequent generation.
  • Loading branch information
tmat authored Sep 10, 2019
1 parent fffbbb3 commit 0e8c4a5
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 160 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
Expand All @@ -30,11 +31,11 @@ public sealed class EditAndContinueWorkspaceServiceTests : TestBase
{
private readonly EditAndContinueDiagnosticUpdateSource _diagnosticUpdateSource;
private readonly Mock<IDiagnosticAnalyzerService> _mockDiagnosticService;
private readonly Mock<IActiveStatementProvider> _mockActiveStatementProvider;
private readonly MockDebuggeeModuleMetadataProvider _mockDebugeeModuleMetadataProvider;
private readonly Mock<IActiveStatementTrackingService> _mockActiveStatementTrackingService;
private readonly MockCompilationOutputsProviderService _mockCompilationOutputsService;

private Mock<IActiveStatementProvider> _mockActiveStatementProvider;
private readonly List<Guid> _modulesPreparedForUpdate;
private readonly List<DiagnosticsUpdatedArgs> _emitDiagnosticsUpdated;
private int _emitDiagnosticsClearedCount;
Expand Down Expand Up @@ -889,7 +890,7 @@ public async Task BreakMode_ValidSignificantChange_EmitSuccessful_UpdateDeferred
{
var dir = Temp.CreateDirectory();

var sourceV1 = "class C1 { void M() { System.Console.WriteLine(1); } }";
var sourceV1 = "class C1 { void M1() { int a = 1; System.Console.WriteLine(a); } void M2() { System.Console.WriteLine(1); } }";
var compilationV1 = CSharpTestBase.CreateCompilationWithMscorlib40(sourceV1, options: TestOptions.DebugDll, assemblyName: "lib");

var pdbStream = new MemoryStream();
Expand All @@ -901,91 +902,122 @@ public async Task BreakMode_ValidSignificantChange_EmitSuccessful_UpdateDeferred
var pdbFile = dir.CreateFile("lib.pdb").WriteAllBytes(pdbStream.ToArray());
var moduleId = moduleMetadata.GetModuleVersionId();

using (var workspace = TestWorkspace.CreateCSharp(sourceV1))
{
var project = workspace.CurrentSolution.Projects.Single();
_mockCompilationOutputsService.Outputs.Add(project.Id, new CompilationOutputFiles(moduleFile.Path, pdbFile.Path));
using var workspace = TestWorkspace.CreateCSharp(sourceV1);

// module not loaded
_mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => null;
var project = workspace.CurrentSolution.Projects.Single();
var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();

var service = CreateEditAndContinueService(workspace);

service.StartDebuggingSession();

service.StartEditSession();
var editSession = service.Test_GetEditSession();

// change the source (valid edit):
var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single();
workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M() { System.Console.WriteLine(2); } }", Encoding.UTF8));
var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();

// validate solution update status and emit:
var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus);
_mockCompilationOutputsService.Outputs.Add(project.Id, new CompilationOutputFiles(moduleFile.Path, pdbFile.Path));

var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit);

// delta to apply:
var delta = deltas.Single();
Assert.Empty(delta.ActiveStatementsInUpdatedMethods);
Assert.NotEmpty(delta.IL.Value);
Assert.NotEmpty(delta.Metadata.Bytes);
Assert.NotEmpty(delta.Pdb.Stream);
Assert.Equal(0x06000001, delta.Pdb.UpdatedMethods.Single());
Assert.Equal(moduleId, delta.Mvid);
Assert.Empty(delta.NonRemappableRegions);
Assert.Empty(delta.LineEdits);
// set up an active statement in the first method, so that we can test preservaton of local signature.
_mockActiveStatementProvider = new Mock<IActiveStatementProvider>(MockBehavior.Strict);
_mockActiveStatementProvider.Setup(p => p.GetActiveStatementsAsync(It.IsAny<CancellationToken>())).
Returns(Task.FromResult(ImmutableArray.Create(new ActiveStatementDebugInfo(
new ActiveInstructionId(moduleId, methodToken: 0x06000001, methodVersion: 1, ilOffset: 0),
documentNameOpt: document1.Name,
linePositionSpan: new LinePositionSpan(new LinePosition(0, 15), new LinePosition(0, 16)),
threadIds: ImmutableArray.Create(Guid.NewGuid()),
ActiveStatementFlags.IsLeafFrame))));

// module not loaded
_mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => null;

var service = CreateEditAndContinueService(workspace);

service.StartDebuggingSession();

service.StartEditSession();
var editSession = service.Test_GetEditSession();

// change the source (valid edit):
workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M1() { int a = 1; System.Console.WriteLine(a); } void M2() { System.Console.WriteLine(2); } }", Encoding.UTF8));
var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();

// validate solution update status and emit:
var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus);

var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit);

// delta to apply:
var delta = deltas.Single();
Assert.Empty(delta.ActiveStatementsInUpdatedMethods);
Assert.NotEmpty(delta.IL.Value);
Assert.NotEmpty(delta.Metadata.Bytes);
Assert.NotEmpty(delta.Pdb.Stream);
Assert.Equal(0x06000002, delta.Pdb.UpdatedMethods.Single());
Assert.Equal(moduleId, delta.Mvid);
Assert.Empty(delta.NonRemappableRegions);
Assert.Empty(delta.LineEdits);

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

var readers = pendingUpdate.ModuleReaders;
Assert.Equal(2, readers.Length);
Assert.NotNull(readers[0]);
Assert.NotNull(readers[1]);

Assert.Equal(project.Id, baselineProjectId);
Assert.Equal(moduleId, newBaseline.OriginalMetadata.GetModuleVersionId());

if (commitUpdate)
{
service.CommitSolutionUpdate();
Assert.Null(service.Test_GetPendingSolutionUpdate());

// the update should be stored on the service:
var pendingUpdate = service.Test_GetPendingSolutionUpdate();
var (baselineProjectId, newBaseline) = pendingUpdate.EmitBaselines.Single();
// no change in non-remappable regions since we didn't have any active statements:
Assert.Empty(editSession.DebuggingSession.NonRemappableRegions);

var moduleReader = pendingUpdate.ModuleReaders.Single();
Assert.NotNull(moduleReader);
// deferred module readers tracked:
var baselineReaders = editSession.DebuggingSession.GetBaselineModuleReaders();
Assert.Equal(2, baselineReaders.Length);
Assert.Same(readers[0], baselineReaders[0]);
Assert.Same(readers[1], baselineReaders[1]);

Assert.Equal(project.Id, baselineProjectId);
Assert.Equal(moduleId, newBaseline.OriginalMetadata.GetModuleVersionId());
// verify that baseline is added:
Assert.Same(newBaseline, editSession.DebuggingSession.Test_GetProjectEmitBaseline(project.Id));

if (commitUpdate)
{
service.CommitSolutionUpdate();
Assert.Null(service.Test_GetPendingSolutionUpdate());
// solution update status after committing an update:
var commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.None, commitedUpdateSolutionStatus);

// no change in non-remappable regions since we didn't have any active statements:
Assert.Empty(editSession.DebuggingSession.NonRemappableRegions);
service.EndEditSession();

// deferred module readers tracked:
Assert.Same(moduleReader, editSession.DebuggingSession.GetBaselineModuleReaders().Single());
// make another update:
service.StartEditSession();

// verify that baseline is added:
Assert.Same(newBaseline, editSession.DebuggingSession.Test_GetProjectEmitBaseline(project.Id));
// Update M1 - this method has an active statement, so we will attempt to preserve the local signature.
// Since the method hasn't been edited before we'll read the baseline PDB to get the signature token.
// This validates that the Portable PDB reader can be used (and is not disposed) for a second generation edit.
var document3 = workspace.CurrentSolution.Projects.Single().Documents.Single();
workspace.ChangeDocument(document3.Id, SourceText.From("class C1 { void M1() { int a = 3; System.Console.WriteLine(a); } void M2() { System.Console.WriteLine(2); } }", Encoding.UTF8));

// solution update status after committing an update:
var commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.None, commitedUpdateSolutionStatus);
(solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit);

service.EndEditSession();
service.EndDebuggingSession();
service.EndEditSession();
service.EndDebuggingSession();

// open module readers should be disposed when the debugging session ends:
Assert.Throws<ObjectDisposedException>(() => ((MetadataReaderProvider)moduleReader).GetMetadataReader());
}
else
{
service.DiscardSolutionUpdate();
Assert.Null(service.Test_GetPendingSolutionUpdate());
// open module readers should be disposed when the debugging session ends:
Assert.Throws<ObjectDisposedException>(() => ((MetadataReaderProvider)readers.First(r => r is MetadataReaderProvider)).GetMetadataReader());
Assert.Throws<ObjectDisposedException>(() => ((DebugInformationReaderProvider)readers.First(r => r is DebugInformationReaderProvider)).CreateEditAndContinueMethodDebugInfoReader());
}
else
{
service.DiscardSolutionUpdate();
Assert.Null(service.Test_GetPendingSolutionUpdate());

// no open module readers since we didn't defer any module update:
Assert.Empty(editSession.DebuggingSession.GetBaselineModuleReaders());
// no open module readers since we didn't defer any module update:
Assert.Empty(editSession.DebuggingSession.GetBaselineModuleReaders());

Assert.Throws<ObjectDisposedException>(() => ((MetadataReaderProvider)moduleReader).GetMetadataReader());
Assert.Throws<ObjectDisposedException>(() => ((MetadataReaderProvider)readers.First(r => r is MetadataReaderProvider)).GetMetadataReader());
Assert.Throws<ObjectDisposedException>(() => ((DebugInformationReaderProvider)readers.First(r => r is DebugInformationReaderProvider)).CreateEditAndContinueMethodDebugInfoReader());

service.EndEditSession();
service.EndDebuggingSession();
}
service.EndEditSession();
service.EndDebuggingSession();
}
}

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

var moduleReader = pendingUpdate.ModuleReaders.Single();
Assert.NotNull(moduleReader);
var readers = pendingUpdate.ModuleReaders;
Assert.Equal(2, readers.Length);
Assert.NotNull(readers[0]);
Assert.NotNull(readers[1]);

Assert.Equal(moduleIdA, newBaselineA1.OriginalMetadata.GetModuleVersionId());
Assert.Equal(moduleIdB, newBaselineB1.OriginalMetadata.GetModuleVersionId());
Expand All @@ -1079,7 +1113,10 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule()
Assert.Empty(editSession.DebuggingSession.NonRemappableRegions);

// deferred module readers tracked:
Assert.Same(moduleReader, editSession.DebuggingSession.GetBaselineModuleReaders().Single());
var baselineReaders = editSession.DebuggingSession.GetBaselineModuleReaders();
Assert.Equal(2, baselineReaders.Length);
Assert.Same(readers[0], baselineReaders[0]);
Assert.Same(readers[1], baselineReaders[1]);

// verify that baseline is added for both modules:
Assert.Same(newBaselineA1, editSession.DebuggingSession.Test_GetProjectEmitBaseline(projectA.Id));
Expand Down Expand Up @@ -1133,7 +1170,10 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule()
Assert.Empty(editSession.DebuggingSession.NonRemappableRegions);

// module readers tracked:
Assert.Same(moduleReader, editSession.DebuggingSession.GetBaselineModuleReaders().Single());
baselineReaders = editSession.DebuggingSession.GetBaselineModuleReaders();
Assert.Equal(2, baselineReaders.Length);
Assert.Same(readers[0], baselineReaders[0]);
Assert.Same(readers[1], baselineReaders[1]);

// verify that baseline is updated for both modules:
Assert.Same(newBaselineA2, editSession.DebuggingSession.Test_GetProjectEmitBaseline(projectA.Id));
Expand All @@ -1148,7 +1188,8 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule()
service.EndDebuggingSession();

// open deferred module readers should be dispose when the debugging session ends:
Assert.Throws<ObjectDisposedException>(() => ((MetadataReaderProvider)moduleReader).GetMetadataReader());
Assert.Throws<ObjectDisposedException>(() => ((MetadataReaderProvider)readers.First(r => r is MetadataReaderProvider)).GetMetadataReader());
Assert.Throws<ObjectDisposedException>(() => ((DebugInformationReaderProvider)readers.First(r => r is DebugInformationReaderProvider)).CreateEditAndContinueMethodDebugInfoReader());
}
}

Expand Down
Loading

0 comments on commit 0e8c4a5

Please sign in to comment.