|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.EditAndContinue;
/// <summary>
/// Represents a debugging session.
/// </summary>
internal sealed class DebuggingSession : IDisposable
{
private readonly Func<Project, CompilationOutputs> _compilationOutputsProvider;
internal readonly IPdbMatchingSourceTextProvider SourceTextProvider;
private readonly CancellationTokenSource _cancellationSource = new();
/// <summary>
/// MVIDs read from the assembly built for given project id.
/// Only contains ids for projects that support EnC.
/// </summary>
private readonly Dictionary<ProjectId, (Guid Mvid, Diagnostic Error)> _projectModuleIds = [];
private readonly Dictionary<Guid, ProjectId> _moduleIds = [];
private readonly object _projectModuleIdsGuard = new();
/// <summary>
/// The current baseline for given project id.
/// The baseline is updated when changes are committed at the end of edit session.
/// The backing module readers of initial baselines need to be kept alive -- store them in
/// <see cref="_initialBaselineModuleReaders"/> and dispose them at the end of the debugging session.
/// </summary>
/// <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="_projectBaselines"/> by a newer baseline.
/// </remarks>
private readonly Dictionary<ProjectId, ProjectBaseline> _projectBaselines = [];
private readonly Dictionary<ProjectId, (IDisposable metadata, IDisposable pdb)> _initialBaselineModuleReaders = [];
private readonly object _projectEmitBaselinesGuard = new();
/// <summary>
/// To avoid accessing metadata/symbol readers that have been disposed,
/// 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 bool _isDisposed;
internal EditSession EditSession { get; private set; }
private readonly HashSet<Guid> _modulesPreparedForUpdate = [];
private readonly object _modulesPreparedForUpdateGuard = new();
internal readonly DebuggingSessionId Id;
/// <summary>
/// Incremented on every emit update invocation. Used by logging.
/// </summary>
private int _updateOrdinal;
/// <summary>
/// The solution captured when the debugging session entered run mode (application debugging started),
/// or the solution which the last changes committed to the debuggee at the end of edit session were calculated from.
/// The solution reflecting the current state of the modules loaded in the debugee.
/// </summary>
internal readonly CommittedSolution LastCommittedSolution;
internal readonly IManagedHotReloadService DebuggerService;
/// <summary>
/// True if the diagnostics produced by the session should be reported to the diagnotic analyzer.
/// </summary>
internal readonly bool ReportDiagnostics;
private readonly DebuggingSessionTelemetry _telemetry;
private readonly EditSessionTelemetry _editSessionTelemetry = new();
private PendingUpdate? _pendingUpdate;
private Action<DebuggingSessionTelemetry.Data> _reportTelemetry;
/// <summary>
/// Last array of module updates generated during the debugging session.
/// Useful for crash dump diagnostics.
/// </summary>
private ImmutableArray<ManagedHotReloadUpdate> _lastModuleUpdatesLog;
internal DebuggingSession(
DebuggingSessionId id,
Solution solution,
IManagedHotReloadService debuggerService,
Func<Project, CompilationOutputs> compilationOutputsProvider,
IPdbMatchingSourceTextProvider sourceTextProvider,
IEnumerable<KeyValuePair<DocumentId, CommittedSolution.DocumentState>> initialDocumentStates,
bool reportDiagnostics)
{
EditAndContinueService.Log.Write($"Debugging session started: #{id}");
_compilationOutputsProvider = compilationOutputsProvider;
SourceTextProvider = sourceTextProvider;
_reportTelemetry = ReportTelemetry;
_telemetry = new DebuggingSessionTelemetry(solution.SolutionState.SolutionAttributes.TelemetryId);
Id = id;
DebuggerService = debuggerService;
LastCommittedSolution = new CommittedSolution(this, solution, initialDocumentStates);
EditSession = new EditSession(
this,
nonRemappableRegions: ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>.Empty,
_editSessionTelemetry,
lazyActiveStatementMap: null,
inBreakState: false);
ReportDiagnostics = reportDiagnostics;
}
public void Dispose()
{
Debug.Assert(!_isDisposed);
_isDisposed = true;
_cancellationSource.Cancel();
_cancellationSource.Dispose();
// Wait for all operations on baseline to finish before we dispose the readers.
_baselineAccessLock.EnterWriteLock();
lock (_projectEmitBaselinesGuard)
{
foreach (var (_, readers) in _initialBaselineModuleReaders)
{
readers.metadata.Dispose();
readers.pdb.Dispose();
}
}
_baselineAccessLock.ExitWriteLock();
_baselineAccessLock.Dispose();
if (Interlocked.Exchange(ref _pendingUpdate, null) != null)
{
throw new InvalidOperationException($"Pending update has not been committed or discarded.");
}
}
internal void ThrowIfDisposed()
{
if (_isDisposed)
throw new ObjectDisposedException(nameof(DebuggingSession));
}
private void StorePendingUpdate(PendingUpdate update)
{
var previousPendingUpdate = Interlocked.Exchange(ref _pendingUpdate, update);
// commit/discard was not called:
if (previousPendingUpdate != null)
{
throw new InvalidOperationException($"Previous update has not been committed or discarded.");
}
}
private PendingUpdate RetrievePendingUpdate()
{
var pendingUpdate = Interlocked.Exchange(ref _pendingUpdate, null);
if (pendingUpdate == null)
{
throw new InvalidOperationException($"No pending update.");
}
return pendingUpdate;
}
private void EndEditSession()
{
var editSessionTelemetryData = EditSession.Telemetry.GetDataAndClear();
_telemetry.LogEditSession(editSessionTelemetryData);
}
public void EndSession(out DebuggingSessionTelemetry.Data telemetryData)
{
ThrowIfDisposed();
EndEditSession();
telemetryData = _telemetry.GetDataAndClear();
_reportTelemetry(telemetryData);
Dispose();
EditAndContinueService.Log.Write($"Debugging session ended: #{Id}");
}
public void BreakStateOrCapabilitiesChanged(bool? inBreakState)
=> RestartEditSession(nonRemappableRegions: null, inBreakState);
internal void RestartEditSession(ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>? nonRemappableRegions, bool? inBreakState)
{
EditAndContinueService.Log.Write($"Edit session restarted (break state: {inBreakState?.ToString() ?? "null"})");
ThrowIfDisposed();
EndEditSession();
EditSession = new EditSession(
this,
nonRemappableRegions ?? EditSession.NonRemappableRegions,
EditSession.Telemetry,
(inBreakState == null) ? EditSession.BaseActiveStatements : null,
inBreakState ?? EditSession.InBreakState);
}
internal CompilationOutputs GetCompilationOutputs(Project project)
=> _compilationOutputsProvider(project);
private bool AddModulePreparedForUpdate(Guid mvid)
{
lock (_modulesPreparedForUpdateGuard)
{
return _modulesPreparedForUpdate.Add(mvid);
}
}
/// <summary>
/// Reads the MVID of a compiled project.
/// </summary>
/// <returns>
/// An MVID and an error message to report, in case an IO exception occurred while reading the binary.
/// The MVID is <see cref="Guid.Empty"/> if either the project is not built, or the MVID can't be read from the module binary.
/// </returns>
internal async Task<(Guid Mvid, Diagnostic? Error)> GetProjectModuleIdAsync(Project project, CancellationToken cancellationToken)
{
Debug.Assert(project.SupportsEditAndContinue());
lock (_projectModuleIdsGuard)
{
if (_projectModuleIds.TryGetValue(project.Id, out var id))
{
return id;
}
}
(Guid Mvid, Diagnostic? Error) ReadMvid()
{
var outputs = GetCompilationOutputs(project);
try
{
return (outputs.ReadAssemblyModuleVersionId(), Error: null);
}
catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException)
{
return (Mvid: Guid.Empty, Error: null);
}
catch (Exception e)
{
var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile);
return (Mvid: Guid.Empty, Error: Diagnostic.Create(descriptor, Location.None, [outputs.AssemblyDisplayPath, e.Message]));
}
}
var newId = await Task.Run(ReadMvid, cancellationToken).ConfigureAwait(false);
lock (_projectModuleIdsGuard)
{
if (_projectModuleIds.TryGetValue(project.Id, out var id))
{
return id;
}
// Do not cache failures. The error might be intermittent and might be corrected next time we attempt to read the MVID.
if (newId.Mvid != Guid.Empty)
{
_moduleIds[newId.Mvid] = project.Id;
_projectModuleIds[project.Id] = newId;
}
return newId;
}
}
internal bool TryGetProjectId(Guid moduleId, [NotNullWhen(true)] out ProjectId? projectId)
{
lock (_projectModuleIdsGuard)
{
return _moduleIds.TryGetValue(moduleId, out projectId);
}
}
/// <summary>
/// Get <see cref="EmitBaseline"/> for given project.
/// </summary>
/// <param name="baselineProject">Project used to create the initial baseline, if the baseline does not exist yet.</param>
/// <param name="baselineCompilation">Compilation used to create the initial baseline, if the baseline does not exist yet.</param>
/// <returns>True unless the project outputs can't be read.</returns>
internal bool TryGetOrCreateEmitBaseline(
Project baselineProject,
Compilation baselineCompilation,
out ImmutableArray<Diagnostic> diagnostics,
[NotNullWhen(true)] out ProjectBaseline? baseline,
[NotNullWhen(true)] out ReaderWriterLockSlim? baselineAccessLock)
{
baselineAccessLock = _baselineAccessLock;
lock (_projectEmitBaselinesGuard)
{
if (_projectBaselines.TryGetValue(baselineProject.Id, out baseline))
{
diagnostics = [];
return true;
}
}
var outputs = GetCompilationOutputs(baselineProject);
if (!TryCreateInitialBaseline(baselineCompilation, outputs, baselineProject.Id, out diagnostics, out var initialBaseline, out var debugInfoReaderProvider, out var metadataReaderProvider))
{
// 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.
baseline = null;
return false;
}
lock (_projectEmitBaselinesGuard)
{
if (_projectBaselines.TryGetValue(baselineProject.Id, out baseline))
{
metadataReaderProvider.Dispose();
debugInfoReaderProvider.Dispose();
return true;
}
baseline = new ProjectBaseline(baselineProject.Id, initialBaseline, generation: 0);
_projectBaselines.Add(baselineProject.Id, baseline);
_initialBaselineModuleReaders.Add(baselineProject.Id, (metadataReaderProvider, debugInfoReaderProvider));
}
return true;
}
private static unsafe bool TryCreateInitialBaseline(
Compilation compilation,
CompilationOutputs compilationOutputs,
ProjectId projectId,
out ImmutableArray<Diagnostic> diagnostics,
[NotNullWhen(true)] out EmitBaseline? baseline,
[NotNullWhen(true)] out DebugInformationReaderProvider? debugInfoReaderProvider,
[NotNullWhen(true)] out MetadataReaderProvider? metadataReaderProvider)
{
// Read the metadata and symbols from the disk. Close the files as soon as we are done emitting the delta to minimize
// the time when they are being locked. Since we need to use the baseline that is produced by delta emit for the subsequent
// delta emit we need to keep the module metadata and symbol info backing the symbols of the baseline alive in memory.
// Alternatively, we could drop the data once we are done with emitting the delta and re-emit the baseline again
// when we need it next time and the module is loaded.
diagnostics = default;
baseline = null;
debugInfoReaderProvider = null;
metadataReaderProvider = null;
var success = false;
var fileBeingRead = compilationOutputs.PdbDisplayPath;
try
{
debugInfoReaderProvider = compilationOutputs.OpenPdb();
if (debugInfoReaderProvider == null)
{
throw new FileNotFoundException();
}
var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader();
fileBeingRead = compilationOutputs.AssemblyDisplayPath;
metadataReaderProvider = compilationOutputs.OpenAssemblyMetadata(prefetch: true);
if (metadataReaderProvider == null)
{
throw new FileNotFoundException();
}
var metadataReader = metadataReaderProvider.GetMetadataReader();
var moduleMetadata = ModuleMetadata.CreateFromMetadata((IntPtr)metadataReader.MetadataPointer, metadataReader.MetadataLength);
baseline = EmitBaseline.CreateInitialBaseline(
compilation,
moduleMetadata,
debugInfoReader.GetDebugInfo,
debugInfoReader.GetLocalSignature,
debugInfoReader.IsPortable);
success = true;
return true;
}
catch (Exception e)
{
EditAndContinueService.Log.Write("Failed to create baseline for '{0}': {1}", projectId, e.Message);
var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile);
diagnostics = [Diagnostic.Create(descriptor, Location.None, [fileBeingRead, e.Message])];
}
finally
{
if (!success)
{
debugInfoReaderProvider?.Dispose();
metadataReaderProvider?.Dispose();
}
}
return false;
}
private static ImmutableDictionary<K, ImmutableArray<V>> GroupToImmutableDictionary<K, V>(IEnumerable<IGrouping<K, V>> items)
where K : notnull
{
var builder = ImmutableDictionary.CreateBuilder<K, ImmutableArray<V>>();
foreach (var item in items)
{
builder.Add(item.Key, [.. item]);
}
return builder.ToImmutable();
}
public async ValueTask<ImmutableArray<Diagnostic>> GetDocumentDiagnosticsAsync(Document document, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken)
{
try
{
if (_isDisposed)
{
return [];
}
// Not a C# or VB project.
var project = document.Project;
if (!project.SupportsEditAndContinue())
{
return [];
}
// Document does not compile to the assembly (e.g. cshtml files, .g.cs files generated for completion only)
if (!document.DocumentState.SupportsEditAndContinue())
{
return [];
}
// Do not analyze documents (and report diagnostics) of projects that have not been built.
// Allow user to make any changes in these documents, they won't be applied within the current debugging session.
// Do not report the file read error - it might be an intermittent issue. The error will be reported when the
// change is attempted to be applied.
var (mvid, _) = await GetProjectModuleIdAsync(project, cancellationToken).ConfigureAwait(false);
if (mvid == Guid.Empty)
{
return [];
}
var (oldDocument, oldDocumentState) = await LastCommittedSolution.GetDocumentAndStateAsync(document.Id, document, cancellationToken).ConfigureAwait(false);
if (oldDocumentState is CommittedSolution.DocumentState.OutOfSync or
CommittedSolution.DocumentState.Indeterminate or
CommittedSolution.DocumentState.DesignTimeOnly)
{
// Do not report diagnostics for existing out-of-sync documents or design-time-only documents.
return [];
}
var analysis = await EditSession.Analyses.GetDocumentAnalysisAsync(LastCommittedSolution, oldDocument, document, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false);
if (analysis.HasChanges)
{
// Once we detected a change in a document let the debugger know that the corresponding loaded module
// is about to be updated, so that it can start initializing it for EnC update, reducing the amount of time applying
// the change blocks the UI when the user "continues".
if (AddModulePreparedForUpdate(mvid))
{
// fire and forget:
_ = Task.Run(() => DebuggerService.PrepareModuleForUpdateAsync(mvid, cancellationToken), cancellationToken);
}
}
if (analysis.RudeEdits.IsEmpty)
{
return [];
}
EditSession.Telemetry.LogRudeEditDiagnostics(analysis.RudeEdits, project.State.Attributes.TelemetryId);
var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
return analysis.RudeEdits.SelectAsArray((e, t) => e.ToDiagnostic(t), tree);
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
{
return [];
}
}
public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
Solution solution,
IImmutableSet<ProjectId> runningProjects,
ActiveStatementSpanProvider activeStatementSpanProvider,
CancellationToken cancellationToken)
{
ThrowIfDisposed();
var updateId = new UpdateId(Id, Interlocked.Increment(ref _updateOrdinal));
// 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);
solutionUpdate.Log(EditAndContinueService.Log, updateId);
_lastModuleUpdatesLog = solutionUpdate.ModuleUpdates.Updates;
if (solutionUpdate.ModuleUpdates.Status == ModuleUpdateStatus.Ready)
{
StorePendingUpdate(new PendingSolutionUpdate(
solution,
solutionUpdate.ProjectBaselines,
solutionUpdate.ModuleUpdates.Updates,
solutionUpdate.NonRemappableRegions));
}
using var _ = ArrayBuilder<ProjectDiagnostics>.GetInstance(out var rudeEditDiagnostics);
foreach (var (projectId, projectRudeEdits) in solutionUpdate.DocumentsWithRudeEdits.GroupBy(static e => e.DocumentId.ProjectId))
{
foreach (var (documentId, rudeEdits) in projectRudeEdits)
{
var document = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);
var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
rudeEditDiagnostics.Add(new(projectId, rudeEdits.SelectAsArray(static (rudeEdit, tree) => rudeEdit.ToDiagnostic(tree), tree)));
}
}
EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart(
solution,
solutionUpdate.ModuleUpdates,
rudeEditDiagnostics,
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,
Diagnostics = solutionUpdate.Diagnostics,
RudeEdits = rudeEditDiagnostics.ToImmutable(),
SyntaxError = solutionUpdate.SyntaxError,
ProjectsToRestart = projectsToRestart,
ProjectsToRebuild = projectsToRebuild
};
}
public void CommitSolutionUpdate()
{
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.
newNonRemappableRegions = GroupToImmutableDictionary(
from moduleRegions in pendingSolutionUpdate.NonRemappableRegions
from region in moduleRegions.Regions
group region.Region by new ManagedMethodId(moduleRegions.ModuleId, region.Method));
if (newNonRemappableRegions.IsEmpty)
newNonRemappableRegions = null;
LastCommittedSolution.CommitSolution(pendingSolutionUpdate.Solution);
}
// update baselines:
lock (_projectEmitBaselinesGuard)
{
foreach (var baseline in pendingUpdate.ProjectBaselines)
{
_projectBaselines[baseline.ProjectId] = baseline;
}
}
_editSessionTelemetry.LogCommitted();
// Restart edit session with no active statements (switching to run mode).
RestartEditSession(newNonRemappableRegions, inBreakState: false);
}
public void DiscardSolutionUpdate()
{
ThrowIfDisposed();
_ = RetrievePendingUpdate();
}
public void UpdateBaselines(Solution solution, ImmutableArray<ProjectId> rebuiltProjects)
{
ThrowIfDisposed();
// Make sure the solution snapshot has all source-generated documents up-to-date.
solution = solution.WithUpToDateSourceGeneratorDocuments(solution.ProjectIds);
LastCommittedSolution.CommitSolution(solution);
lock (_projectEmitBaselinesGuard)
{
foreach (var projectId in rebuiltProjects)
{
if (_projectBaselines.Remove(projectId))
{
var (metadata, pdb) = _initialBaselineModuleReaders[projectId];
metadata.Dispose();
pdb.Dispose();
_initialBaselineModuleReaders.Remove(projectId);
}
}
}
foreach (var projectId in rebuiltProjects)
{
_editSessionTelemetry.LogUpdatedBaseline(solution.GetRequiredProject(projectId).State.ProjectInfo.Attributes.TelemetryId);
}
// Restart edit session reusing previous non-remappable regions and break state:
RestartEditSession(nonRemappableRegions: null, inBreakState: null);
}
/// <summary>
/// Returns <see cref="ActiveStatementSpan"/>s for each document of <paramref name="documentIds"/>,
/// or default if not in a break state.
/// </summary>
public async ValueTask<ImmutableArray<ImmutableArray<ActiveStatementSpan>>> GetBaseActiveStatementSpansAsync(Solution solution, ImmutableArray<DocumentId> documentIds, CancellationToken cancellationToken)
{
try
{
if (_isDisposed || !EditSession.InBreakState)
{
return default;
}
var baseActiveStatements = await EditSession.BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false);
using var _1 = PooledDictionary<string, ArrayBuilder<(ProjectId, int)>>.GetInstance(out var documentIndicesByMappedPath);
using var _2 = PooledHashSet<ProjectId>.GetInstance(out var projectIds);
// Construct map of mapped file path to a text document in the current solution
// and a set of projects these documents are contained in.
for (var i = 0; i < documentIds.Length; i++)
{
var documentId = documentIds[i];
var document = await solution.GetTextDocumentAsync(documentId, cancellationToken).ConfigureAwait(false);
if (document?.State.SupportsEditAndContinue() != true)
{
// document has been deleted or doesn't support EnC (can't have an active statement anymore):
continue;
}
if (!document.Project.SupportsEditAndContinue())
{
// document is in a project that does not support EnC
continue;
}
Contract.ThrowIfNull(document.FilePath);
// Multiple documents may have the same path (linked file).
// The documents represent the files that #line directives map to.
// Documents that have the same path must have different project id.
documentIndicesByMappedPath.MultiAdd(document.FilePath, (documentId.ProjectId, i));
projectIds.Add(documentId.ProjectId);
}
using var _3 = PooledDictionary<ActiveStatement, ArrayBuilder<(DocumentId unmappedDocumentId, LinePositionSpan span)>>.GetInstance(
out var activeStatementsInChangedDocuments);
// Analyze changed documents in projects containing active statements:
foreach (var projectId in projectIds)
{
var oldProject = LastCommittedSolution.GetProject(projectId);
if (oldProject == null)
{
// document is in a project that's been added to the solution
continue;
}
var newProject = solution.GetRequiredProject(projectId);
Debug.Assert(oldProject.SupportsEditAndContinue());
Debug.Assert(newProject.SupportsEditAndContinue());
var analyzer = newProject.Services.GetRequiredService<IEditAndContinueAnalyzer>();
await foreach (var documentId in EditSession.GetChangedDocumentsAsync(oldProject, newProject, cancellationToken).ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
var newDocument = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);
var (oldDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newDocument.Id, newDocument, cancellationToken).ConfigureAwait(false);
if (oldDocument == null)
{
// Document is out-of-sync, can't reason about its content with respect to the binaries loaded in the debuggee.
continue;
}
var oldDocumentActiveStatements = await baseActiveStatements.GetOldActiveStatementsAsync(analyzer, oldDocument, cancellationToken).ConfigureAwait(false);
var analysis = await analyzer.AnalyzeDocumentAsync(
oldProject,
EditSession.BaseActiveStatements,
newDocument,
newActiveStatementSpans: [],
EditSession.Capabilities,
cancellationToken).ConfigureAwait(false);
// Document content did not change or unable to determine active statement spans in a document with syntax errors:
if (!analysis.ActiveStatements.IsDefault)
{
for (var i = 0; i < oldDocumentActiveStatements.Length; i++)
{
// Note: It is possible that one active statement appears in multiple documents if the documents represent a linked file.
// Example (old and new contents):
// #if Condition #if Condition
// #line 1 a.txt #line 1 a.txt
// [|F(1);|] [|F(1000);|]
// #else #else
// #line 1 a.txt #line 1 a.txt
// [|F(2);|] [|F(2);|]
// #endif #endif
//
// In the new solution the AS spans are different depending on which document view of the same file we are looking at.
// Different views correspond to different projects.
activeStatementsInChangedDocuments.MultiAdd(oldDocumentActiveStatements[i].Statement, (analysis.DocumentId, analysis.ActiveStatements[i].Span));
}
}
}
}
using var _4 = ArrayBuilder<ImmutableArray<ActiveStatementSpan>>.GetInstance(out var spans);
spans.AddMany([], documentIds.Length);
foreach (var (mappedPath, documentBaseActiveStatements) in baseActiveStatements.DocumentPathMap)
{
if (documentIndicesByMappedPath.TryGetValue(mappedPath, out var indices))
{
// translate active statements from base solution to the new solution, if the documents they are contained in changed:
foreach (var (projectId, index) in indices)
{
spans[index] = documentBaseActiveStatements.SelectAsArray(
activeStatement =>
{
LinePositionSpan span;
DocumentId? unmappedDocumentId;
if (activeStatementsInChangedDocuments.TryGetValue(activeStatement, out var newSpans))
{
(unmappedDocumentId, span) = newSpans.Single(ns => ns.unmappedDocumentId.ProjectId == projectId);
}
else
{
span = activeStatement.Span;
unmappedDocumentId = null;
}
return new ActiveStatementSpan(activeStatement.Id, span, activeStatement.Flags, unmappedDocumentId);
});
}
}
}
documentIndicesByMappedPath.FreeValues();
activeStatementsInChangedDocuments.FreeValues();
Debug.Assert(spans.Count == documentIds.Length);
return spans.ToImmutableAndClear();
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
{
throw ExceptionUtilities.Unreachable();
}
}
public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveStatementSpansAsync(TextDocument mappedDocument, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken)
{
try
{
if (_isDisposed || !EditSession.InBreakState || !mappedDocument.State.SupportsEditAndContinue() || !mappedDocument.Project.SupportsEditAndContinue())
{
return [];
}
Contract.ThrowIfNull(mappedDocument.FilePath);
var newProject = mappedDocument.Project;
var newSolution = newProject.Solution;
var oldProject = LastCommittedSolution.GetProject(newProject.Id);
if (oldProject == null)
{
// TODO: https://github.com/dotnet/roslyn/issues/1204
// Enumerate all documents of the new project.
return [];
}
var baseActiveStatements = await EditSession.BaseActiveStatements.GetValueAsync(cancellationToken).ConfigureAwait(false);
if (!baseActiveStatements.DocumentPathMap.TryGetValue(mappedDocument.FilePath, out var oldMappedDocumentActiveStatements))
{
// no active statements in this document
return [];
}
var newDocumentActiveStatementSpans = await activeStatementSpanProvider(mappedDocument.Id, mappedDocument.FilePath, cancellationToken).ConfigureAwait(false);
if (newDocumentActiveStatementSpans.IsEmpty)
{
return [];
}
var analyzer = newProject.Services.GetRequiredService<IEditAndContinueAnalyzer>();
using var _ = ArrayBuilder<ActiveStatementSpan>.GetInstance(out var adjustedMappedSpans);
// Start with the current locations of the tracking spans.
adjustedMappedSpans.AddRange(newDocumentActiveStatementSpans);
// Update tracking spans to the latest known locations of the active statements contained in changed documents based on their analysis.
await foreach (var unmappedDocumentId in EditSession.GetChangedDocumentsAsync(oldProject, newProject, cancellationToken).ConfigureAwait(false))
{
var newUnmappedDocument = await newSolution.GetRequiredDocumentAsync(unmappedDocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);
var (oldUnmappedDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newUnmappedDocument.Id, newUnmappedDocument, cancellationToken).ConfigureAwait(false);
if (oldUnmappedDocument == null)
{
// document out-of-date
continue;
}
var analysis = await EditSession.Analyses.GetDocumentAnalysisAsync(LastCommittedSolution, oldUnmappedDocument, newUnmappedDocument, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false);
// Document content did not change or unable to determine active statement spans in a document with syntax errors:
if (!analysis.ActiveStatements.IsDefault)
{
foreach (var activeStatement in analysis.ActiveStatements)
{
var i = adjustedMappedSpans.FindIndex(static (s, id) => s.Id == id, activeStatement.Id);
if (i >= 0)
{
adjustedMappedSpans[i] = new ActiveStatementSpan(activeStatement.Id, activeStatement.Span, activeStatement.Flags, unmappedDocumentId);
}
}
}
}
return adjustedMappedSpans.ToImmutableAndClear();
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
{
throw ExceptionUtilities.Unreachable();
}
}
private static void ReportTelemetry(DebuggingSessionTelemetry.Data data)
{
// report telemetry (fire and forget):
_ = Task.Run(() => DebuggingSessionTelemetry.Log(data, Logger.Log, CorrelationIdFactory.GetNextId));
}
internal TestAccessor GetTestAccessor()
=> new(this);
internal readonly struct TestAccessor(DebuggingSession instance)
{
public ImmutableHashSet<Guid> GetModulesPreparedForUpdate()
{
lock (instance._modulesPreparedForUpdateGuard)
{
return [.. instance._modulesPreparedForUpdate];
}
}
public EmitBaseline GetProjectEmitBaseline(ProjectId id)
{
lock (instance._projectEmitBaselinesGuard)
{
return instance._projectBaselines[id].EmitBaseline;
}
}
public bool HasProjectEmitBaseline(ProjectId id)
{
lock (instance._projectEmitBaselinesGuard)
{
return instance._projectBaselines.ContainsKey(id);
}
}
public ImmutableArray<IDisposable> GetBaselineModuleReaders()
{
lock (instance._projectEmitBaselinesGuard)
{
return instance._initialBaselineModuleReaders.Values.SelectMany(entry => new IDisposable[] { entry.metadata, entry.pdb }).ToImmutableArray();
}
}
public PendingUpdate? GetPendingSolutionUpdate()
=> instance._pendingUpdate;
public void SetTelemetryLogger(Action<FunctionId, LogMessage> logger, Func<int> getNextId)
=> instance._reportTelemetry = data => DebuggingSessionTelemetry.Log(data, logger, getNextId);
}
}
|