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 @@ -26,6 +26,9 @@ public sealed class CompilerResolverTests : IDisposable

public CompilerResolverTests()
{
// Ensure that Xunit dependencies are loaded.
Assert.True(true);

TempRoot = new TempRoot();
DefaultLoadContextAssemblies = AssemblyLoadContext.Default.Assemblies.SelectAsArray(a => a.FullName);
CompilerContext = new AssemblyLoadContext(nameof(CompilerResolverTests), isCollectible: true);
Expand Down
31 changes: 17 additions & 14 deletions src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,17 @@ internal enum DocumentState

/// <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.
/// The value is the MVID of the module at the time it was determined to be stale (source code content did not match the PDB).
/// A build that updates the binary to new content (that presumably matches the source code) will update the MVID. When that happens we unstale the project.
///
/// 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.
/// Lock <see cref="_guard"/> to update.
/// </summary>
private readonly HashSet<ProjectId> _staleProjects = [];
private ImmutableDictionary<ProjectId, Guid> _staleProjects = ImmutableDictionary<ProjectId, Guid>.Empty;

/// <summary>
/// Implements workaround for https://github.com/dotnet/project-system/issues/5457.
Expand Down Expand Up @@ -142,13 +144,8 @@ 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 ImmutableDictionary<ProjectId, Guid> StaleProjects
=> _staleProjects;

public ImmutableArray<DocumentId> GetDocumentIdsWithFilePath(string path)
=> _solution.GetDocumentIdsWithFilePath(path);
Expand Down Expand Up @@ -458,16 +455,22 @@ await TryGetMatchingSourceTextAsync(log, sourceText, sourceFilePath, currentDocu
}
}

public void CommitChanges(Solution solution, ImmutableArray<ProjectId> projectsToStale, IReadOnlyCollection<ProjectId> projectsToUnstale)
public void CommitChanges(Solution solution, ImmutableDictionary<ProjectId, Guid>? staleProjects, ImmutableArray<ProjectId>? projectsToUnstale = null)
{
Debug.Assert(projectsToStale.Intersect(projectsToUnstale).IsEmpty());
Contract.ThrowIfTrue(staleProjects is null && projectsToUnstale is null);

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

staleProjects ??= _staleProjects.RemoveRange(projectsToUnstale!);

var oldStaleProjects = _staleProjects;
_staleProjects = staleProjects;

_documentState.RemoveAll(
static (documentId, _, args) => args.oldStaleProjects.ContainsKey(documentId.ProjectId) && !args.staleProjects.ContainsKey(documentId.ProjectId),
(oldStaleProjects, staleProjects));
}
}

Expand Down
26 changes: 16 additions & 10 deletions src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
{
StorePendingUpdate(new PendingSolutionUpdate(
solution,
solutionUpdate.ProjectsToStale,
solutionUpdate.StaleProjects,
solutionUpdate.ProjectsToRebuild,
solutionUpdate.ProjectBaselines,
solutionUpdate.ModuleUpdates.Updates,
Expand All @@ -565,7 +565,7 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(

StorePendingUpdate(new PendingSolutionUpdate(
solution,
solutionUpdate.ProjectsToStale,
solutionUpdate.StaleProjects,
// if partial updates are not allowed we don't treat rebuild as part of solution update:
projectsToRebuild: [],
solutionUpdate.ProjectBaselines,
Expand All @@ -585,7 +585,7 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(

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

Expand All @@ -611,6 +611,8 @@ public void CommitSolutionUpdate()

ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>? newNonRemappableRegions = null;
using var _ = PooledHashSet<ProjectId>.GetInstance(out var projectsToRebuildTransitive);
IEnumerable<ProjectId> baselinesToDiscard = [];
Solution? solution = null;

var pendingUpdate = RetrievePendingUpdate();
if (pendingUpdate is PendingSolutionUpdate pendingSolutionUpdate)
Expand All @@ -626,7 +628,7 @@ from region in moduleRegions.Regions
if (newNonRemappableRegions.IsEmpty)
newNonRemappableRegions = null;

var solution = pendingSolutionUpdate.Solution;
solution = pendingSolutionUpdate.Solution;

// Once the project is rebuilt all its dependencies are going to be up-to-date.
var dependencyGraph = solution.GetProjectDependencyGraph();
Expand All @@ -637,7 +639,7 @@ from region in moduleRegions.Regions
}

// Unstale all projects that will be up-to-date after rebuild.
LastCommittedSolution.CommitChanges(solution, projectsToStale: pendingSolutionUpdate.ProjectsToStale, projectsToUnstale: projectsToRebuildTransitive);
LastCommittedSolution.CommitChanges(solution, staleProjects: pendingSolutionUpdate.StaleProjects.RemoveRange(projectsToRebuildTransitive));

foreach (var projectId in projectsToRebuildTransitive)
{
Expand All @@ -654,12 +656,14 @@ from region in moduleRegions.Regions
{
foreach (var updatedBaseline in pendingUpdate.ProjectBaselines)
{
_projectBaselines[updatedBaseline.ProjectId] = [.. _projectBaselines[updatedBaseline.ProjectId].Select(existingBaseline => existingBaseline.ModuleId == updatedBaseline.ModuleId ? updatedBaseline : existingBaseline)];
_projectBaselines[updatedBaseline.ProjectId] = [.. _projectBaselines[updatedBaseline.ProjectId]
.Select(existingBaseline => existingBaseline.ModuleId == updatedBaseline.ModuleId ? updatedBaseline : existingBaseline)];
}

// Discard any open baseline readers for projects that need to be rebuilt,
// so that the build can overwrite the underlying files.
DiscardProjectBaselinesNoLock(projectsToRebuildTransitive);
Contract.ThrowIfNull(solution);
DiscardProjectBaselinesNoLock(solution, projectsToRebuildTransitive.Concat(baselinesToDiscard));
}

_baselineContentAccessLock.ExitWriteLock();
Expand All @@ -676,7 +680,7 @@ public void DiscardSolutionUpdate()
_ = RetrievePendingUpdate();
}

private void DiscardProjectBaselinesNoLock(IEnumerable<ProjectId> projects)
private void DiscardProjectBaselinesNoLock(Solution solution, IEnumerable<ProjectId> projects)
{
foreach (var projectId in projects)
{
Expand All @@ -693,6 +697,8 @@ private void DiscardProjectBaselinesNoLock(IEnumerable<ProjectId> projects)

_initialBaselineModuleReaders.Remove(projectBaseline.ModuleId);
}

SessionLog.Write($"Baselines discarded: {solution.GetRequiredProject(projectId).GetLogDisplay()}.");
}
}
}
Expand All @@ -705,15 +711,15 @@ 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.CommitChanges(solution, projectsToStale: [], projectsToUnstale: rebuiltProjects);
LastCommittedSolution.CommitChanges(solution, staleProjects: null, projectsToUnstale: rebuiltProjects);

// Wait for all operations on baseline to finish before we dispose the readers.

_baselineContentAccessLock.EnterWriteLock();

lock (_projectEmitBaselinesGuard)
{
DiscardProjectBaselinesNoLock(rebuiltProjects);
DiscardProjectBaselinesNoLock(solution, rebuiltProjects);
}

_baselineContentAccessLock.ExitWriteLock();
Expand Down
30 changes: 19 additions & 11 deletions src/Features/Core/Portable/EditAndContinue/EditSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -900,8 +900,9 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(
using var _1 = ArrayBuilder<ManagedHotReloadUpdate>.GetInstance(out var deltas);
using var _2 = ArrayBuilder<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)>.GetInstance(out var nonRemappableRegions);
using var _3 = ArrayBuilder<ProjectBaseline>.GetInstance(out var newProjectBaselines);
using var _4 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToStale);
using var _5 = PooledDictionary<ProjectId, ArrayBuilder<Diagnostic>>.GetInstance(out var diagnosticBuilders);
using var _4 = ArrayBuilder<(ProjectId id, Guid mvid)>.GetInstance(out var projectsToStale);
using var _5 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToUnstale);
using var _6 = PooledDictionary<ProjectId, ArrayBuilder<Diagnostic>>.GetInstance(out var diagnosticBuilders);
using var projectDifferences = new ProjectDifferences();

// After all projects have been analyzed "true" value indicates changed document that is only included in stale projects.
Expand Down Expand Up @@ -931,6 +932,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
ProjectAnalysisSummary? projectSummaryToReport = null;

var oldSolution = DebuggingSession.LastCommittedSolution;
var staleProjects = oldSolution.StaleProjects;

var hasPersistentErrors = false;
foreach (var newProject in solution.Projects)
Expand Down Expand Up @@ -978,17 +980,23 @@ void UpdateChangedDocumentsStaleness(bool isStale)
Log.Write($"Found {projectDifferences.ChangedOrAddedDocuments.Count} potentially changed, {projectDifferences.DeletedDocuments.Count} deleted document(s) in project {newProject.GetLogDisplay()}");
}

var isStaleProject = oldSolution.IsStaleProject(newProject.Id);
var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(newProject, cancellationToken).ConfigureAwait(false);

// We don't consider document changes in stale projects until they are rebuilt (removed from stale set).
if (isStaleProject)
if (staleProjects.TryGetValue(newProject.Id, out var staleModuleId))
{
Log.Write($"EnC state of {newProject.GetLogDisplay()} queried: project is stale");
UpdateChangedDocumentsStaleness(isStale: true);
continue;
// The module hasn't been rebuilt or we are unable to read the MVID -- keep treating the project as stale.
if (mvid == staleModuleId || mvidReadError != null)
{
Log.Write($"EnC state of {newProject.GetLogDisplay()} queried: project is stale");
UpdateChangedDocumentsStaleness(isStale: true);

continue;
}

staleProjects = staleProjects.Remove(newProject.Id);
}

var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(newProject, cancellationToken).ConfigureAwait(false);
if (mvidReadError != null)
{
// The error hasn't been reported by GetDocumentDiagnosticsAsync since it might have been intermittent.
Expand Down Expand Up @@ -1030,7 +1038,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)
// Treat the project the same as if it hasn't been built. We won't produce delta for it until it gets rebuilt.
Log.Write($"Changes not applied to {newProject.GetLogDisplay()}: binaries not up-to-date");

projectsToStale.Add(newProject.Id);
staleProjects = staleProjects.Add(newProject.Id, mvid);
UpdateChangedDocumentsStaleness(isStale: true);

continue;
Expand Down Expand Up @@ -1318,7 +1326,7 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance

if (hasPersistentErrors)
{
return SolutionUpdate.Empty(diagnostics, syntaxError, ModuleUpdateStatus.Blocked);
return SolutionUpdate.Empty(diagnostics, syntaxError, staleProjects, ModuleUpdateStatus.Blocked);
}

// syntax error is a persistent error
Expand All @@ -1338,7 +1346,7 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance

return new SolutionUpdate(
moduleUpdates,
projectsToStale.ToImmutable(),
staleProjects,
nonRemappableRegions.ToImmutable(),
newProjectBaselines.ToImmutable(),
diagnostics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ internal abstract class PendingUpdate(

internal sealed class PendingSolutionUpdate(
Solution solution,
ImmutableArray<ProjectId> projectsToStale,
ImmutableDictionary<ProjectId, Guid> staleProjects,
ImmutableArray<ProjectId> projectsToRebuild,
ImmutableArray<ProjectBaseline> projectBaselines,
ImmutableArray<ManagedHotReloadUpdate> deltas,
ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)> nonRemappableRegions) : PendingUpdate(projectBaselines, deltas)
{
public readonly Solution Solution = solution;
public readonly ImmutableArray<ProjectId> ProjectsToStale = projectsToStale;
public readonly ImmutableDictionary<ProjectId, Guid> StaleProjects = staleProjects;
public readonly ImmutableArray<ProjectId> ProjectsToRebuild = projectsToRebuild;
public readonly ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)> Regions)> NonRemappableRegions = nonRemappableRegions;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.EditAndContinue;

internal readonly struct SolutionUpdate(
ModuleUpdates moduleUpdates,
ImmutableArray<ProjectId> projectsToStale,
ImmutableDictionary<ProjectId, Guid> staleProjects,
ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)> nonRemappableRegions,
ImmutableArray<ProjectBaseline> projectBaselines,
ImmutableArray<ProjectDiagnostics> diagnostics,
Expand All @@ -20,7 +20,7 @@ internal readonly struct SolutionUpdate(
ImmutableArray<ProjectId> projectsToRebuild)
{
public readonly ModuleUpdates ModuleUpdates = moduleUpdates;
public readonly ImmutableArray<ProjectId> ProjectsToStale = projectsToStale;
public readonly ImmutableDictionary<ProjectId, Guid> StaleProjects = staleProjects;
public readonly ImmutableArray<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)> NonRemappableRegions = nonRemappableRegions;
public readonly ImmutableArray<ProjectBaseline> ProjectBaselines = projectBaselines;

Expand All @@ -33,10 +33,11 @@ internal readonly struct SolutionUpdate(
public static SolutionUpdate Empty(
ImmutableArray<ProjectDiagnostics> diagnostics,
Diagnostic? syntaxError,
ImmutableDictionary<ProjectId, Guid> staleProjects,
ModuleUpdateStatus status)
=> new(
new(status, Updates: []),
projectsToStale: [],
staleProjects: staleProjects,
nonRemappableRegions: [],
projectBaselines: [],
diagnostics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4117,6 +4117,10 @@ public async Task MultiTargetedPartiallyBuiltProjects()

CommitSolutionUpdate(debuggingSession);

// Stale project baselines should get discarded after committing the update.
// Unlocks binaries and allows the user to rebuild the project.
Assert.False(debuggingSession.GetTestAccessor().HasProjectEmitBaseline(projectBId));

// update source file in the editor (source text is now matching the PDB of project B):
var text0 = CreateText(source0);
solution = solution.WithDocumentText(documentA.Id, text0).WithDocumentText(documentB.Id, text0);
Expand Down Expand Up @@ -4144,7 +4148,6 @@ public async Task MultiTargetedPartiallyBuiltProjects()
// Saving is required so that we can fetch the baseline content for the next delta calculation.
File.WriteAllText(sourcePath, source2, Encoding.UTF8);
var mvidB2 = EmitAndLoadLibraryToDebuggee(projectBId, source2, sourceFilePath: sourcePath, assemblyName: "A", targetFramework: TargetFramework.Net90);
debuggingSession.UpdateBaselines(solution, rebuiltProjects: [projectBId]);

// no changes have been made:

Expand Down
Loading