Skip to content

Commit

Permalink
Merge pull request #73394 from CyrusNajmabadi/updateDocumentsAtOnce
Browse files Browse the repository at this point in the history
  • Loading branch information
CyrusNajmabadi authored May 8, 2024
2 parents c0ef482 + 8d017b9 commit 9c9420d
Show file tree
Hide file tree
Showing 11 changed files with 291 additions and 146 deletions.
14 changes: 4 additions & 10 deletions src/VisualStudio/Core/Def/CodeCleanup/AbstractCodeCleanUpFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Progress;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
Expand Down Expand Up @@ -218,7 +219,7 @@ private static async Task<Solution> FixProjectsAsync(
progressTracker.AddItems(projects.Sum(static p => p.DocumentIds.Count));

// Run in parallel across all projects.
return await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync(
var changedRoots = await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync(
source: projects,
produceItems: static async (project, callback, args, cancellationToken) =>
{
Expand All @@ -245,17 +246,10 @@ await RoslynParallel.ForEachAsync(
callback((document.Id, await fixedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false)));
}).ConfigureAwait(false);
},
consumeItems: static async (stream, args, cancellationToken) =>
{
// Now consume the changed documents, applying their new roots to the solution.
var currentSolution = args.solution;
await foreach (var (documentId, newRoot) in stream)
currentSolution = currentSolution.WithDocumentSyntaxRoot(documentId, newRoot);
return currentSolution;
},
args: (globalOptions, solution, enabledFixIds, progressTracker),
cancellationToken).ConfigureAwait(false);

return solution.WithDocumentSyntaxRoots(changedRoots.SelectAsArray(t => (t.documentId, t.newRoot, PreservationMode.PreserveValue)));
}

private static async Task<Document> FixDocumentAsync(
Expand Down
14 changes: 4 additions & 10 deletions src/Workspaces/Core/Portable/CodeActions/CodeAction_Cleanup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ async Task<Solution> RunParallelCleanupPassAsync(
// on the same fork and do not cause the forked solution to be created and dropped repeatedly.
using var _ = await RemoteKeepAliveSession.CreateAsync(solution, cancellationToken).ConfigureAwait(false);

return await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync(
var changedRoots = await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync(
source: documentIdsAndOptions,
produceItems: static async (documentIdAndOptions, callback, args, cancellationToken) =>
{
Expand All @@ -164,17 +164,11 @@ async Task<Solution> RunParallelCleanupPassAsync(
var newRoot = await cleanedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
callback((documentId, newRoot));
},
consumeItems: static async (stream, args, cancellationToken) =>
{
// Grab all the cleaned roots and produce the new solution snapshot from that.
var currentSolution = args.solution;
await foreach (var (documentId, newRoot) in stream)
currentSolution = currentSolution.WithDocumentSyntaxRoot(documentId, newRoot);
return currentSolution;
},
args: (solution, progress, cleanupDocumentAsync),
cancellationToken).ConfigureAwait(false);

// Grab all the cleaned roots and produce the new solution snapshot from that.
return solution.WithDocumentSyntaxRoots(changedRoots.SelectAsArray(t => (t.documentId, t.newRoot, PreservationMode.PreserveValue)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
Expand Down Expand Up @@ -71,7 +72,7 @@ async Task<Solution> GetInitialUncleanedSolutionAsync(Solution originalSolution)
// so we never resync or recompute anything.
using var _ = await RemoteKeepAliveSession.CreateAsync(originalSolution, cancellationToken).ConfigureAwait(false);

return await ProducerConsumer<(DocumentId documentId, (SyntaxNode? node, SourceText? text))>.RunParallelAsync(
var changedRootsAndTexts = await ProducerConsumer<(DocumentId documentId, (SyntaxNode? node, SourceText? text))>.RunParallelAsync(
source: fixAllContexts,
produceItems: static async (fixAllContext, callback, args, cancellationToken) =>
{
Expand All @@ -94,28 +95,22 @@ await args.getFixedDocumentsAsync(
callback((newDocument.Id, (newRoot, newText)));
}).ConfigureAwait(false);
},
consumeItems: static async (stream, args, cancellationToken) =>
{
var currentSolution = args.originalSolution;
// Next, go and insert those all into the solution so all the docs in this particular project point
// at the new trees (or text). At this point though, the trees have not been semantically cleaned
// up. We don't cleanup the documents as they are created, or one at a time as we add them, as that
// would cause us to run semantic cleanup on N different solution forks (which would be very
// expensive as we'd fork, produce semantics, fork, produce semantics, etc. etc.). Instead, by
// adding all the changed documents to one solution, and then cleaning *those* we only perform
// cleanup semantics on one forked solution.
await foreach (var (docId, (newRoot, newText)) in stream)
{
currentSolution = newRoot != null
? currentSolution.WithDocumentSyntaxRoot(docId, newRoot)
: currentSolution.WithDocumentText(docId, newText!);
}
return currentSolution;
},
args: (getFixedDocumentsAsync, progressTracker, originalSolution),
cancellationToken).ConfigureAwait(false);

// Next, go and insert those all into the solution so all the docs in this particular project point
// at the new trees (or text). At this point though, the trees have not been semantically cleaned
// up. We don't cleanup the documents as they are created, or one at a time as we add them, as that
// would cause us to run semantic cleanup on N different solution forks (which would be very
// expensive as we'd fork, produce semantics, fork, produce semantics, etc. etc.). Instead, by
// adding all the changed documents to one solution, and then cleaning *those* we only perform
// cleanup semantics on one forked solution.
var changedRoots = changedRootsAndTexts.SelectAsArray(t => t.Item2.node != null, t => (t.documentId, t.Item2.node!, PreservationMode.PreserveValue));
var changedTexts = changedRootsAndTexts.SelectAsArray(t => t.Item2.text != null, t => (t.documentId, t.Item2.text!, PreservationMode.PreserveValue));

return originalSolution
.WithDocumentSyntaxRoots(changedRoots)
.WithDocumentTexts(changedTexts);
}
}
}
129 changes: 85 additions & 44 deletions src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,28 +185,35 @@ private static async Task<VersionStamp> ComputeLatestDocumentVersionAsync(TextDo
}

private AsyncLazy<VersionStamp> CreateLazyLatestDocumentTopLevelChangeVersion(
TextDocumentState newDocument,
ImmutableArray<TextDocumentState> newDocuments,
TextDocumentStates<DocumentState> newDocumentStates,
TextDocumentStates<AdditionalDocumentState> newAdditionalDocumentStates)
{
if (_lazyLatestDocumentTopLevelChangeVersion.TryGetValue(out var oldVersion))
{
return AsyncLazy.Create(static (arg, c) =>
ComputeTopLevelChangeTextVersionAsync(arg.oldVersion, arg.newDocument, c),
arg: (oldVersion, newDocument));
return AsyncLazy.Create(static (arg, cancellationToken) =>
ComputeTopLevelChangeTextVersionAsync(arg.oldVersion, arg.newDocuments, cancellationToken),
arg: (oldVersion, newDocuments));
}
else
{
return AsyncLazy.Create(static (arg, c) =>
ComputeLatestDocumentTopLevelChangeVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, c),
return AsyncLazy.Create(static (arg, cancellationToken) =>
ComputeLatestDocumentTopLevelChangeVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, cancellationToken),
arg: (newDocumentStates, newAdditionalDocumentStates));
}
}

private static async Task<VersionStamp> ComputeTopLevelChangeTextVersionAsync(VersionStamp oldVersion, TextDocumentState newDocument, CancellationToken cancellationToken)
private static async Task<VersionStamp> ComputeTopLevelChangeTextVersionAsync(
VersionStamp oldVersion, ImmutableArray<TextDocumentState> newDocuments, CancellationToken cancellationToken)
{
var newVersion = await newDocument.GetTopLevelChangeTextVersionAsync(cancellationToken).ConfigureAwait(false);
return newVersion.GetNewerVersion(oldVersion);
var finalVersion = oldVersion;
foreach (var newDocument in newDocuments)
{
var newVersion = await newDocument.GetTopLevelChangeTextVersionAsync(cancellationToken).ConfigureAwait(false);
finalVersion = newVersion.GetNewerVersion(finalVersion);
}

return finalVersion;
}

private static async Task<VersionStamp> ComputeLatestDocumentTopLevelChangeVersionAsync(TextDocumentStates<DocumentState> documentStates, TextDocumentStates<AdditionalDocumentState> additionalDocumentStates, CancellationToken cancellationToken)
Expand Down Expand Up @@ -834,16 +841,25 @@ public ProjectState RemoveAllNormalDocuments()
}

public ProjectState UpdateDocument(DocumentState newDocument, bool contentChanged)
=> UpdateDocuments([newDocument], contentChanged);

public ProjectState UpdateDocuments(ImmutableArray<DocumentState> newDocuments, bool contentChanged)
{
var oldDocument = DocumentStates.GetRequiredState(newDocument.Id);
if (oldDocument == newDocument)
{
var oldDocuments = newDocuments.SelectAsArray(d => DocumentStates.GetRequiredState(d.Id));
if (oldDocuments.SequenceEqual(newDocuments))
return this;
}

var newDocumentStates = DocumentStates.SetState(newDocument.Id, newDocument);
// Must not be empty as we would have otherwise bailed out in the check above.
Contract.ThrowIfTrue(newDocuments.IsEmpty);

var newDocumentStates = DocumentStates.SetStates(newDocuments);

// When computing the latest dependent version, we just need to know how
GetLatestDependentVersions(
newDocumentStates, AdditionalDocumentStates, oldDocument, newDocument, contentChanged,
newDocumentStates, AdditionalDocumentStates,
oldDocuments.CastArray<TextDocumentState>(),
newDocuments.CastArray<TextDocumentState>(),
contentChanged,
out var dependentDocumentVersion, out var dependentSemanticVersion);

return With(
Expand All @@ -860,9 +876,9 @@ public ProjectState UpdateAdditionalDocument(AdditionalDocumentState newDocument
return this;
}

var newDocumentStates = AdditionalDocumentStates.SetState(newDocument.Id, newDocument);
var newDocumentStates = AdditionalDocumentStates.SetState(newDocument);
GetLatestDependentVersions(
DocumentStates, newDocumentStates, oldDocument, newDocument, contentChanged,
DocumentStates, newDocumentStates, [oldDocument], [newDocument], contentChanged,
out var dependentDocumentVersion, out var dependentSemanticVersion);

return this.With(
Expand All @@ -879,7 +895,7 @@ public ProjectState UpdateAnalyzerConfigDocument(AnalyzerConfigDocumentState new
return this;
}

var newDocumentStates = AnalyzerConfigDocumentStates.SetState(newDocument.Id, newDocument);
var newDocumentStates = AnalyzerConfigDocumentStates.SetState(newDocument);

return CreateNewStateForChangedAnalyzerConfigDocuments(newDocumentStates);
}
Expand All @@ -899,46 +915,71 @@ public ProjectState UpdateDocumentsOrder(ImmutableList<DocumentId> documentIds)
private void GetLatestDependentVersions(
TextDocumentStates<DocumentState> newDocumentStates,
TextDocumentStates<AdditionalDocumentState> newAdditionalDocumentStates,
TextDocumentState oldDocument, TextDocumentState newDocument,
ImmutableArray<TextDocumentState> oldDocuments,
ImmutableArray<TextDocumentState> newDocuments,
bool contentChanged,
out AsyncLazy<VersionStamp> dependentDocumentVersion, out AsyncLazy<VersionStamp> dependentSemanticVersion)
out AsyncLazy<VersionStamp> dependentDocumentVersion,
out AsyncLazy<VersionStamp> dependentSemanticVersion)
{
var recalculateDocumentVersion = false;
var recalculateSemanticVersion = false;

if (contentChanged)
{
if (oldDocument.TryGetTextVersion(out var oldVersion))
foreach (var oldDocument in oldDocuments)
{
if (!_lazyLatestDocumentVersion.TryGetValue(out var documentVersion) || documentVersion == oldVersion)
if (oldDocument.TryGetTextVersion(out var oldVersion))
{
recalculateDocumentVersion = true;
}
if (!_lazyLatestDocumentVersion.TryGetValue(out var documentVersion) || documentVersion == oldVersion)
recalculateDocumentVersion = true;

if (!_lazyLatestDocumentTopLevelChangeVersion.TryGetValue(out var semanticVersion) || semanticVersion == oldVersion)
{
recalculateSemanticVersion = true;
if (!_lazyLatestDocumentTopLevelChangeVersion.TryGetValue(out var semanticVersion) || semanticVersion == oldVersion)
recalculateSemanticVersion = true;
}

if (recalculateDocumentVersion && recalculateSemanticVersion)
break;
}
}

dependentDocumentVersion = recalculateDocumentVersion
? AsyncLazy.Create(static (arg, c) =>
ComputeLatestDocumentVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, c),
arg: (newDocumentStates, newAdditionalDocumentStates))
: contentChanged
? AsyncLazy.Create(static (newDocument, c) =>
newDocument.GetTextVersionAsync(c),
arg: newDocument)
: _lazyLatestDocumentVersion;

dependentSemanticVersion = recalculateSemanticVersion
? AsyncLazy.Create(static (arg, c) =>
ComputeLatestDocumentTopLevelChangeVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, c),
arg: (newDocumentStates, newAdditionalDocumentStates))
: contentChanged
? CreateLazyLatestDocumentTopLevelChangeVersion(newDocument, newDocumentStates, newAdditionalDocumentStates)
: _lazyLatestDocumentTopLevelChangeVersion;
if (recalculateDocumentVersion)
{
dependentDocumentVersion = AsyncLazy.Create(static (arg, cancellationToken) =>
ComputeLatestDocumentVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, cancellationToken),
arg: (newDocumentStates, newAdditionalDocumentStates));
}
else if (contentChanged)
{
dependentDocumentVersion = AsyncLazy.Create(
static async (newDocuments, cancellationToken) =>
{
var finalVersion = await newDocuments[0].GetTextVersionAsync(cancellationToken).ConfigureAwait(false);
for (var i = 1; i < newDocuments.Length; i++)
finalVersion = finalVersion.GetNewerVersion(await newDocuments[i].GetTextVersionAsync(cancellationToken).ConfigureAwait(false));
return finalVersion;
},
arg: newDocuments);
}
else
{
dependentDocumentVersion = _lazyLatestDocumentVersion;
}

if (recalculateSemanticVersion)
{
dependentSemanticVersion = AsyncLazy.Create(static (arg, cancellationToken) =>
ComputeLatestDocumentTopLevelChangeVersionAsync(arg.newDocumentStates, arg.newAdditionalDocumentStates, cancellationToken),
arg: (newDocumentStates, newAdditionalDocumentStates));
}
else if (contentChanged)
{
dependentSemanticVersion = CreateLazyLatestDocumentTopLevelChangeVersion(newDocuments, newDocumentStates, newAdditionalDocumentStates);
}
else
{
dependentSemanticVersion = _lazyLatestDocumentTopLevelChangeVersion;
}
}

public void AddDocumentIdsWithFilePath(ref TemporaryArray<DocumentId> temporaryArray, string filePath)
Expand Down
Loading

0 comments on commit 9c9420d

Please sign in to comment.