Skip to content
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

Hot Reload watch service #51967

Merged
merged 4 commits into from
Mar 22, 2021
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public Validator(

var mockCompilationOutputsProvider = new Func<Project, CompilationOutputs>(_ => new MockCompilationOutputs(Guid.NewGuid()));

var debuggingSession = new DebuggingSession(solution, mockCompilationOutputsProvider);
var debuggingSession = new DebuggingSession(solution, mockCompilationOutputsProvider, SpecializedCollections.EmptyEnumerable<KeyValuePair<DocumentId, CommittedSolution.DocumentState>>());

if (initialState != CommittedSolution.DocumentState.None)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,13 @@ void VerifyReanalyzeInvocation(ImmutableArray<DocumentId> documentIds)
// StartDebuggingSession

var called = false;
mockEncService.StartDebuggingSessionImpl = solution =>
mockEncService.StartDebuggingSessionImpl = (solution, captureMatchingDocuments) =>
{
Assert.Equal("proj", solution.Projects.Single().Name);
called = true;
};

await proxy.StartDebuggingSessionAsync(localWorkspace.CurrentSolution, CancellationToken.None).ConfigureAwait(false);
await proxy.StartDebuggingSessionAsync(localWorkspace.CurrentSolution, captureMatchingDocuments: false, CancellationToken.None).ConfigureAwait(false);
Assert.True(called);

// StartEditSession
Expand Down Expand Up @@ -226,11 +226,10 @@ await proxy.StartEditSessionAsync(

var syntaxTree = project.Documents.Single().GetSyntaxTreeSynchronously(CancellationToken.None)!;

var documentDiagnostic = DiagnosticData.Create(Diagnostic.Create(diagnosticDescriptor1, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)), new[] { "doc", "some error" }), document);
var projectDiagnostic = DiagnosticData.Create(Diagnostic.Create(diagnosticDescriptor1, Location.None, new[] { "proj", "some error" }), project);
var solutionDiagnostic = DiagnosticData.Create(Diagnostic.Create(diagnosticDescriptor1, Location.None, new[] { "sol", "some error" }), solution.Options);
var documentDiagnostic = Diagnostic.Create(diagnosticDescriptor1, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)), new[] { "doc", "some error" });
var projectDiagnostic = Diagnostic.Create(diagnosticDescriptor1, Location.None, new[] { "proj", "some error" });

return (new(ManagedModuleUpdateStatus.Ready, deltas), ImmutableArray.Create(documentDiagnostic, projectDiagnostic, solutionDiagnostic));
return new(new(ManagedModuleUpdateStatus.Ready, deltas), ImmutableArray.Create((project.Id, ImmutableArray.Create(documentDiagnostic, projectDiagnostic))));
};

var updates = await proxy.EmitSolutionUpdateAsync(localWorkspace.CurrentSolution, solutionActiveStatementSpanProvider, diagnosticUpdateSource, CancellationToken.None).ConfigureAwait(false);
Expand All @@ -242,8 +241,7 @@ await proxy.StartEditSessionAsync(
AssertEx.Equal(new[]
{
$"[{project.Id}] Error ENC1001: test.cs(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "some error")}",
$"[{project.Id}] Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, "proj", "some error")}",
$"[] Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, "sol", "some error")}",
$"[{project.Id}] Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, "proj", "some error")}"
},
emitDiagnosticsUpdated.Select(update =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ internal class MockEditAndContinueWorkspaceService : IEditAndContinueWorkspaceSe
public Func<Solution, SolutionActiveStatementSpanProvider, ManagedInstructionId, LinePositionSpan?>? GetCurrentActiveStatementPositionImpl;

public Func<Document, DocumentActiveStatementSpanProvider, ImmutableArray<(LinePositionSpan, ActiveStatementFlags)>>? GetAdjustedActiveStatementSpansImpl;
public Action<Solution>? StartDebuggingSessionImpl;
public Action<Solution, bool>? StartDebuggingSessionImpl;
public StartEditSession? StartEditSessionImpl;
public EndSession? EndDebuggingSessionImpl;
public EndSession? EndEditSessionImpl;
public Func<Solution, SolutionActiveStatementSpanProvider, string?, bool>? HasChangesImpl;
public Func<Solution, SolutionActiveStatementSpanProvider, (ManagedModuleUpdates, ImmutableArray<DiagnosticData>)>? EmitSolutionUpdateImpl;
public Func<Solution, SolutionActiveStatementSpanProvider, EmitSolutionUpdateResults>? EmitSolutionUpdateImpl;
public Func<Solution, ManagedInstructionId, bool?>? IsActiveStatementInExceptionRegionImpl;
public Action<Document>? OnSourceFileUpdatedImpl;
public Action? CommitSolutionUpdateImpl;
Expand All @@ -39,8 +39,7 @@ public void CommitSolutionUpdate()
public void DiscardSolutionUpdate()
=> DiscardSolutionUpdateImpl?.Invoke();

public ValueTask<(ManagedModuleUpdates Updates, ImmutableArray<DiagnosticData> Diagnostics)>
EmitSolutionUpdateAsync(Solution solution, SolutionActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken)
public ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(Solution solution, SolutionActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken)
=> new((EmitSolutionUpdateImpl ?? throw new NotImplementedException()).Invoke(solution, activeStatementSpanProvider));

public void EndDebuggingSession(out ImmutableArray<DocumentId> documentsToReanalyze)
Expand Down Expand Up @@ -76,8 +75,11 @@ public ValueTask<bool> HasChangesAsync(Solution solution, SolutionActiveStatemen
public void OnSourceFileUpdated(Document document)
=> OnSourceFileUpdatedImpl?.Invoke(document);

public void StartDebuggingSession(Solution solution)
=> StartDebuggingSessionImpl?.Invoke(solution);
public ValueTask StartDebuggingSessionAsync(Solution solution, bool captureMatchingDocuments, CancellationToken cancellationToken)
{
StartDebuggingSessionImpl?.Invoke(solution, captureMatchingDocuments);
return default;
}

public void StartEditSession(IManagedEditAndContinueDebuggerService debuggerService, out ImmutableArray<DocumentId> documentsToReanalyze)
{
Expand Down
170 changes: 117 additions & 53 deletions src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using System.IO;
using System.Text;

namespace Microsoft.CodeAnalysis.EditAndContinue
{
Expand Down Expand Up @@ -88,11 +88,12 @@ internal enum DocumentState

private readonly object _guard = new();

public CommittedSolution(DebuggingSession debuggingSession, Solution solution)
public CommittedSolution(DebuggingSession debuggingSession, Solution solution, IEnumerable<KeyValuePair<DocumentId, DocumentState>> initialDocumentStates)
{
_solution = solution;
_debuggingSession = debuggingSession;
_documentState = new Dictionary<DocumentId, DocumentState>();
_documentState.AddRange(initialDocumentStates);
}

// test only
Expand All @@ -104,6 +105,15 @@ internal void Test_SetDocumentState(DocumentId documentId, DocumentState state)
}
}

// test only
internal ImmutableArray<(DocumentId id, DocumentState state)> Test_GetDocumentStates()
{
lock (_guard)
{
return _documentState.SelectAsArray(e => (e.Key, e.Value));
}
}

public bool HasNoChanges(Solution solution)
=> _solution == solution;

Expand Down Expand Up @@ -200,17 +210,29 @@ public Task OnSourceFileUpdatedAsync(Document document, CancellationToken cancel
return (null, DocumentState.None);
}

if (!PathUtilities.IsAbsolute(document.FilePath))
if (!EditAndContinueWorkspaceService.SupportsEditAndContinue(document.DocumentState))
{
return (null, DocumentState.DesignTimeOnly);
}

var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var sourceTextVersion = (committedDocument == null) ? await document.GetTextVersionAsync(cancellationToken).ConfigureAwait(false) : default;

var (matchingSourceText, pdbHasDocument) = await Task.Run(
() => TryGetPdbMatchingSourceText(document.FilePath, sourceText.Encoding, document.Project),
cancellationToken).ConfigureAwait(false);
// run file IO on a background thread:
var (matchingSourceText, pdbHasDocument) = await Task.Run(() =>
{
var compilationOutputs = _debuggingSession.GetCompilationOutputs(document.Project);
using var debugInfoReaderProvider = GetMethodDebugInfoReader(compilationOutputs, document.Project.Name);
if (debugInfoReaderProvider == null)
{
return (null, null);
}

var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader();

Contract.ThrowIfNull(document.FilePath);
return TryGetPdbMatchingSourceText(debugInfoReader, document.FilePath, sourceText.Encoding);
}, cancellationToken).ConfigureAwait(false);

lock (_guard)
{
Expand Down Expand Up @@ -283,6 +305,79 @@ public Task OnSourceFileUpdatedAsync(Document document, CancellationToken cancel
}
}

internal static async Task<IEnumerable<KeyValuePair<DocumentId, CommittedSolution.DocumentState>>> GetMatchingDocumentsAsync(Solution solution, Func<Project, CompilationOutputs> compilationOutputsProvider, CancellationToken cancellationToken)
{
var projectTasks = solution.Projects.Select(async project =>
{
using var debugInfoReaderProvider = GetMethodDebugInfoReader(compilationOutputsProvider(project), project.Name);
if (debugInfoReaderProvider == null)
{
return Array.Empty<DocumentId?>();
}

// Skip projects that do not support Roslyn EnC (e.g. F#, etc).
// Source files of these do not even need to be captured in the solution snapshot.
if (!EditAndContinueWorkspaceService.SupportsEditAndContinue(project))
{
return Array.Empty<DocumentId?>();
}

var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader();

var documentTasks = project.State.DocumentStates.States.Select(async documentState =>
{
cancellationToken.ThrowIfCancellationRequested();

if (EditAndContinueWorkspaceService.SupportsEditAndContinue(documentState))
{
var sourceFilePath = documentState.FilePath;
Contract.ThrowIfNull(sourceFilePath);

// Hydrate the solution snapshot with the content of the file.
// It's important to do this before we start watching for changes so that we have a baseline we can compare future snapshots to.
var sourceText = await documentState.GetTextAsync(cancellationToken).ConfigureAwait(false);

// TODO: https://github.com/dotnet/roslyn/issues/51993
// avoid rereading the file in common case - the workspace should create source texts with the right checksum algorithm and encoding
var (source, hasDocument) = TryGetPdbMatchingSourceText(debugInfoReader, sourceFilePath, sourceText.Encoding);
if (source != null)
{
return documentState.Id;
}
}

return null;
});

return await Task.WhenAll(documentTasks).ConfigureAwait(false);
});

var documentIdArrays = await Task.WhenAll(projectTasks).ConfigureAwait(false);

return documentIdArrays.SelectMany(ids => ids.WhereNotNull()).Select(id => KeyValuePairUtil.Create(id, DocumentState.MatchesBuildOutput));
}

private static DebugInformationReaderProvider? GetMethodDebugInfoReader(CompilationOutputs compilationOutputs, string projectName)
{
DebugInformationReaderProvider? debugInfoReaderProvider;
try
{
debugInfoReaderProvider = compilationOutputs.OpenPdb();

if (debugInfoReaderProvider == null)
{
EditAndContinueWorkspaceService.Log.Write("Source file of project '{0}' doesn't match output PDB: PDB '{1}' not found", projectName, compilationOutputs.PdbDisplayPath);
}

return debugInfoReaderProvider;
}
catch (Exception e)
{
EditAndContinueWorkspaceService.Log.Write("Source file of project '{0}' doesn't match output PDB: error opening PDB '{1}': {2}", projectName, compilationOutputs.PdbDisplayPath, e.Message);
return null;
}
}

public void CommitSolution(Solution solution)
{
lock (_guard)
Expand All @@ -291,9 +386,9 @@ public void CommitSolution(Solution solution)
}
}

private (SourceText? Source, bool? HasDocument) TryGetPdbMatchingSourceText(string sourceFilePath, Encoding? encoding, Project project)
private static (SourceText? Source, bool? HasDocument) TryGetPdbMatchingSourceText(EditAndContinueMethodDebugInfoReader debugInfoReader, string sourceFilePath, Encoding? encoding)
{
var hasDocument = TryReadSourceFileChecksumFromPdb(sourceFilePath, project, out var symChecksum, out var algorithm);
var hasDocument = TryReadSourceFileChecksumFromPdb(debugInfoReader, sourceFilePath, out var symChecksum, out var algorithm);
if (hasDocument != true)
{
return (Source: null, hasDocument);
Expand Down Expand Up @@ -331,62 +426,31 @@ public void CommitSolution(Solution solution)
/// False if the document is not found in the PDB.
/// Null if it can't be determined because the PDB is not available or an error occurred while reading the PDB.
/// </summary>
private bool? TryReadSourceFileChecksumFromPdb(string sourceFilePath, Project project, out ImmutableArray<byte> checksum, out SourceHashAlgorithm algorithm)
private static bool? TryReadSourceFileChecksumFromPdb(EditAndContinueMethodDebugInfoReader debugInfoReader, string sourceFilePath, out ImmutableArray<byte> checksum, out SourceHashAlgorithm algorithm)
{
checksum = default;
algorithm = default;

try
{
var compilationOutputs = _debuggingSession.GetCompilationOutputs(project);

DebugInformationReaderProvider? debugInfoReaderProvider;
try
if (!debugInfoReader.TryGetDocumentChecksum(sourceFilePath, out checksum, out var algorithmId))
{
debugInfoReaderProvider = compilationOutputs.OpenPdb();
}
catch (Exception e)
{
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: error opening PDB '{1}': {2}", sourceFilePath, compilationOutputs.PdbDisplayPath, e.Message);
debugInfoReaderProvider = null;
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: no document", sourceFilePath);
return false;
}

if (debugInfoReaderProvider == null)
algorithm = SourceHashAlgorithms.GetSourceHashAlgorithm(algorithmId);
if (algorithm == SourceHashAlgorithm.None)
{
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: PDB '{1}' not found", sourceFilePath, compilationOutputs.PdbDisplayPath);
return null;
// This can only happen if the PDB was post-processed by a misbehaving tool.
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unknown checksum alg", sourceFilePath);
}

try
{
var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader();
if (!debugInfoReader.TryGetDocumentChecksum(sourceFilePath, out checksum, out var algorithmId))
{
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: no document", sourceFilePath);
return false;
}

algorithm = SourceHashAlgorithms.GetSourceHashAlgorithm(algorithmId);
if (algorithm == SourceHashAlgorithm.None)
{
// This can only happen if the PDB was post-processed by a misbehaving tool.
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unknown checksum alg", sourceFilePath);
}

return true;
}
catch (Exception e)
{
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: error reading symbols: {1}", sourceFilePath, e.Message);
}
finally
{
debugInfoReaderProvider.Dispose();
}
return true;
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e))
catch (Exception e)
{
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unexpected exception: {1}", sourceFilePath, e.Message);
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: error reading symbols: {1}", sourceFilePath, e.Message);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,15 @@ internal sealed class DebuggingSession : IDisposable

internal DebuggingSession(
Solution solution,
Func<Project, CompilationOutputs> compilationOutputsProvider)
Func<Project, CompilationOutputs> compilationOutputsProvider,
IEnumerable<KeyValuePair<DocumentId, CommittedSolution.DocumentState>> initialDocumentStates)
{
_compilationOutputsProvider = compilationOutputsProvider;
_projectModuleIds = new Dictionary<ProjectId, (Guid, Diagnostic)>();
_projectEmitBaselines = new Dictionary<ProjectId, EmitBaseline>();
_modulesPreparedForUpdate = new HashSet<Guid>();

LastCommittedSolution = new CommittedSolution(this, solution);
LastCommittedSolution = new CommittedSolution(this, solution, initialDocumentStates);
NonRemappableRegions = ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>.Empty;
}

Expand Down
Loading