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

Provide solution methods to allow updating a bulk set of documents at once. #73394

Merged
merged 9 commits into from
May 8, 2024
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 @@ -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(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view with whitespace off.

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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i broke the nested conditionals into if-statements as it was breaking my brain.

{
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
Loading