Skip to content
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 @@ -259,10 +259,12 @@ public async Task DefaultPdbMatchingSourceTextProvider()
Assert.True(workspace.SetCurrentSolution(_ => solution, WorkspaceChangeKind.SolutionAdded));
solution = workspace.CurrentSolution;

var moduleId = EmitAndLoadLibraryToDebuggee(source1, sourceFilePath: sourceFile.Path);
var moduleId = EmitAndLoadLibraryToDebuggee(projectId, source1, sourceFilePath: sourceFile.Path);

// hydrate document text and overwrite file content:
var document1 = await solution.GetDocument(documentId).GetTextAsync();
var document1 = solution.GetRequiredDocument(documentId);
_ = await document1.GetTextAsync(CancellationToken.None);

File.WriteAllText(sourceFile.Path, source2, Encoding.UTF8);

await languageService.StartSessionAsync(CancellationToken.None);
Expand All @@ -282,7 +284,7 @@ public async Task DefaultPdbMatchingSourceTextProvider()

// check committed document status:
var debuggingSession = service.GetTestAccessor().GetActiveDebuggingSessions().Single();
var (document, state) = await debuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(documentId, currentDocument: null, CancellationToken.None);
var (document, state) = await debuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(document1, CancellationToken.None);
var text = await document.GetTextAsync();
Assert.Equal(CommittedSolution.DocumentState.MatchesBuildOutput, state);
Assert.Equal(source1, (await document.GetTextAsync(CancellationToken.None)).ToString());
Expand Down
53 changes: 44 additions & 9 deletions src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ namespace Microsoft.CodeAnalysis.EditAndContinue;
/// </summary>
internal sealed class CommittedSolution
{
private readonly DebuggingSession _debuggingSession;

private Solution _solution;

internal enum DocumentState
{
None = 0,
Expand Down Expand Up @@ -60,6 +56,25 @@ internal enum DocumentState
MatchesBuildOutput = 4
}

private readonly DebuggingSession _debuggingSession;

/// <summary>
/// Current solution snapshot used as a baseline for calculating EnC delta.
/// </summary>
private Solution _solution;

/// <summary>
/// Tracks stale projects. Changes in these projects are ignored and their representation in the <see cref="_solution"/> does not match the binaries on disk.
///
/// Build of a multi-targeted project that sets <c>SingleTargetBuildForStartupProjects</c> msbuild property (e.g. MAUI) only
/// builds TFM that's active. Other TFMs of the projects remain unbuilt or stale (from previous build).
///
/// A project is removed from this set if it's rebuilt.
///
/// Lock <see cref="_guard"/> to access.
/// </summary>
private readonly HashSet<ProjectId> _staleProjects = [];

/// <summary>
/// Implements workaround for https://github.com/dotnet/project-system/issues/5457.
///
Expand All @@ -85,6 +100,8 @@ internal enum DocumentState
/// A document state can only change from <see cref="DocumentState.OutOfSync"/> to <see cref="DocumentState.MatchesBuildOutput"/>.
/// Once a document state is <see cref="DocumentState.MatchesBuildOutput"/> or <see cref="DocumentState.DesignTimeOnly"/>
/// it will never change.
///
/// Lock <see cref="_guard"/> to access.
/// </summary>
private readonly Dictionary<DocumentId, DocumentState> _documentState = [];

Expand Down Expand Up @@ -124,6 +141,14 @@ public bool HasNoChanges(Solution solution)
public Project GetRequiredProject(ProjectId id)
=> _solution.GetRequiredProject(id);

public bool IsStaleProject(ProjectId id)
{
lock (_guard)
{
return _staleProjects.Contains(id);
}
}

public ImmutableArray<DocumentId> GetDocumentIdsWithFilePath(string path)
=> _solution.GetDocumentIdsWithFilePath(path);

Expand All @@ -137,12 +162,11 @@ public bool ContainsDocument(DocumentId documentId)
///
/// The result is cached and the next lookup uses the cached value, including failures unless <paramref name="reloadOutOfSyncDocument"/> is true.
/// </summary>
public async Task<(Document? Document, DocumentState State)> GetDocumentAndStateAsync(DocumentId documentId, Document? currentDocument, CancellationToken cancellationToken, bool reloadOutOfSyncDocument = false)
public async Task<(Document? Document, DocumentState State)> GetDocumentAndStateAsync(Document currentDocument, CancellationToken cancellationToken, bool reloadOutOfSyncDocument = false)
{
Contract.ThrowIfFalse(currentDocument == null || documentId == currentDocument.Id);

Solution solution;
var documentState = DocumentState.None;
var documentId = currentDocument.Id;

lock (_guard)
{
Expand Down Expand Up @@ -259,6 +283,12 @@ public bool ContainsDocument(DocumentId documentId)
}
else
{
// The following patches the current committed solution with the actual baseline content of the document, if we could retrieve it.
// This patch is temporary, in effect for the current delta calculation. Once the changes are applied and committed we
// update the committed solution to the latest snapshot of the main workspace solution. This operation drops the changes made here.
// That's ok since we only patch documents that have been modified and therefore their new versions will be the correct baseline for the
// next delta calculation. The baseline content loaded here won't be needed anymore.

// Document exists in the PDB but not in the committed solution.
// Add the document to the committed solution with its current (possibly out-of-sync) text.
if (committedDocument == null)
Expand Down Expand Up @@ -306,7 +336,7 @@ public bool ContainsDocument(DocumentId documentId)
}
}

private async ValueTask<(Optional<SourceText?> matchingSourceText, bool? hasDocument)> TryGetMatchingSourceTextAsync(Document document, SourceText sourceText, Document? currentDocument, CancellationToken cancellationToken)
private async ValueTask<(Optional<SourceText?> matchingSourceText, bool? hasDocument)> TryGetMatchingSourceTextAsync(Document document, SourceText sourceText, Document currentDocument, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(document.FilePath);

Expand Down Expand Up @@ -427,11 +457,16 @@ await TryGetMatchingSourceTextAsync(log, sourceText, sourceFilePath, currentDocu
}
}

public void CommitSolution(Solution solution)
public void CommitChanges(Solution solution, ImmutableArray<ProjectId> projectsToStale, ImmutableArray<ProjectId> projectsToUnstale)
{
Contract.ThrowIfFalse(projectsToStale is [] || projectsToUnstale is []);

lock (_guard)
{
_solution = solution;
_staleProjects.AddRange(projectsToStale);
_staleProjects.RemoveRange(projectsToUnstale);
_documentState.RemoveAll(static (documentId, _, projectsToUnstale) => projectsToUnstale.Contains(documentId.ProjectId), projectsToUnstale);
}
}

Expand Down
40 changes: 29 additions & 11 deletions src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,32 @@ internal sealed class DebuggingSession : IDisposable
internal readonly TraceLog AnalysisLog;

/// <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.
/// Current baselines for given project id.
/// The baselines are updated when changes are committed at the end of edit session.
/// </summary>
/// <remarks>
/// 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.
///
/// 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.
///
/// One project may have multiple baselines. Deltas emitted for the project when source changes are applied are based
/// on the same source changes for all the baselines, however they differ in the baseline they are chained to (MVID and relative tokens).
///
/// For example, in the following scenario:
///
/// A shared library Lib is referenced by two executable projects A and B and Lib.dll is copied to their respective output directories and the following events occur:
/// 1) A is launched, modules A.exe and Lib.dll [1] are loaded.
/// 2) Change is made to Lib.cs and applied.
/// 3) B is launched, which builds new version of Lib.dll [2], and modules B.exe and Lib.dll [2] are loaded.
/// 4) Another change is made to Lib.cs and applied.
///
/// At this point we have two baselines for Lib: Lib.dll [1] and Lib.dll [2], each have different MVID.
/// We need to emit 2 deltas for the change in step 4:
/// - one that chains to the first delta applied to Lib.dll, which itself chains to the baseline of Lib.dll [1].
/// - one that chains to the baseline Lib.dll [2]
/// </remarks>
private readonly Dictionary<ProjectId, ImmutableList<ProjectBaseline>> _projectBaselines = [];
private readonly Dictionary<Guid, (IDisposable metadata, IDisposable pdb)> _initialBaselineModuleReaders = [];
Expand Down Expand Up @@ -455,7 +472,7 @@ public async ValueTask<ImmutableArray<Diagnostic>> GetDocumentDiagnosticsAsync(D
return [];
}

var (oldDocument, oldDocumentState) = await LastCommittedSolution.GetDocumentAndStateAsync(document.Id, document, cancellationToken).ConfigureAwait(false);
var (oldDocument, oldDocumentState) = await LastCommittedSolution.GetDocumentAndStateAsync(document, cancellationToken).ConfigureAwait(false);
if (oldDocumentState is CommittedSolution.DocumentState.OutOfSync or
CommittedSolution.DocumentState.Indeterminate or
CommittedSolution.DocumentState.DesignTimeOnly)
Expand Down Expand Up @@ -518,6 +535,7 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
// based on whether the updates will be applied successfully or not.
StorePendingUpdate(new PendingSolutionUpdate(
solution,
solutionUpdate.ProjectsToStale,
solutionUpdate.ProjectBaselines,
solutionUpdate.ModuleUpdates.Updates,
solutionUpdate.NonRemappableRegions));
Expand All @@ -529,8 +547,8 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
Contract.ThrowIfFalse(solutionUpdate.NonRemappableRegions.IsEmpty);

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

Expand Down Expand Up @@ -587,7 +605,7 @@ from region in moduleRegions.Regions
if (newNonRemappableRegions.IsEmpty)
newNonRemappableRegions = null;

LastCommittedSolution.CommitSolution(pendingSolutionUpdate.Solution);
LastCommittedSolution.CommitChanges(pendingSolutionUpdate.Solution, projectsToStale: pendingSolutionUpdate.ProjectsToStale, projectsToUnstale: []);
}

// update baselines:
Expand Down Expand Up @@ -618,7 +636,7 @@ public void UpdateBaselines(Solution solution, ImmutableArray<ProjectId> rebuilt
// Make sure the solution snapshot has all source-generated documents up-to-date.
solution = solution.WithUpToDateSourceGeneratorDocuments(solution.ProjectIds);

LastCommittedSolution.CommitSolution(solution);
LastCommittedSolution.CommitChanges(solution, projectsToStale: [], projectsToUnstale: rebuiltProjects);

// Wait for all operations on baseline to finish before we dispose the readers.
_baselineAccessLock.EnterWriteLock();
Expand Down Expand Up @@ -726,7 +744,7 @@ public async ValueTask<ImmutableArray<ImmutableArray<ActiveStatementSpan>>> GetB

var newDocument = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);

var (oldDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newDocument.Id, newDocument, cancellationToken).ConfigureAwait(false);
var (oldDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(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.
Expand Down Expand Up @@ -857,7 +875,7 @@ public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveSta
{
var newUnmappedDocument = await newSolution.GetRequiredDocumentAsync(unmappedDocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);

var (oldUnmappedDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newUnmappedDocument.Id, newUnmappedDocument, cancellationToken).ConfigureAwait(false);
var (oldUnmappedDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newUnmappedDocument, cancellationToken).ConfigureAwait(false);
if (oldUnmappedDocument == null)
{
// document out-of-date
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ void AddGeneralDiagnostic(EditAndContinueErrorCode code, string resourceName, Di
AddGeneralDiagnostic(EditAndContinueErrorCode.ErrorReadingFile, nameof(FeaturesResources.ErrorReadingFile));
AddGeneralDiagnostic(EditAndContinueErrorCode.CannotApplyChangesUnexpectedError, nameof(FeaturesResources.CannotApplyChangesUnexpectedError));
AddGeneralDiagnostic(EditAndContinueErrorCode.ChangesDisallowedWhileStoppedAtException, nameof(FeaturesResources.ChangesDisallowedWhileStoppedAtException));
AddGeneralDiagnostic(EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee, nameof(FeaturesResources.DocumentIsOutOfSyncWithDebuggee));
AddGeneralDiagnostic(EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee, nameof(FeaturesResources.DocumentIsOutOfSyncWithDebuggee), DiagnosticSeverity.Warning);
AddGeneralDiagnostic(EditAndContinueErrorCode.UnableToReadSourceFileOrPdb, nameof(FeaturesResources.UnableToReadSourceFileOrPdb));
AddGeneralDiagnostic(EditAndContinueErrorCode.AddingTypeRuntimeCapabilityRequired, nameof(FeaturesResources.ChangesRequiredSynthesizedType));

Expand Down
Loading
Loading