-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #73386 from CyrusNajmabadi/cleanupParallel
- Loading branch information
Showing
4 changed files
with
237 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
180 changes: 180 additions & 0 deletions
180
src/Workspaces/Core/Portable/CodeActions/CodeAction_Cleanup.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
// 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.Immutable; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis.CaseCorrection; | ||
using Microsoft.CodeAnalysis.CodeCleanup; | ||
using Microsoft.CodeAnalysis.Editing; | ||
using Microsoft.CodeAnalysis.Formatting; | ||
using Microsoft.CodeAnalysis.Options; | ||
using Microsoft.CodeAnalysis.PooledObjects; | ||
using Microsoft.CodeAnalysis.Remote; | ||
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
using Microsoft.CodeAnalysis.Shared.Utilities; | ||
using Microsoft.CodeAnalysis.Simplification; | ||
using Roslyn.Utilities; | ||
|
||
namespace Microsoft.CodeAnalysis.CodeActions; | ||
|
||
public abstract partial class CodeAction | ||
{ | ||
/// <summary> | ||
/// We do cleanup in N serialized passes. This allows us to process all documents in parallel, while only forking | ||
/// the solution N times *total* (instead of N times *per* document). | ||
/// </summary> | ||
private static readonly ImmutableArray<Func<Document, CodeCleanupOptions, CancellationToken, Task<Document>>> s_cleanupPasses = | ||
[ | ||
// First, ensure that everything is formatted as the feature asked for. We want to do this prior to doing | ||
// semantic cleanup as the semantic cleanup passes may end up making changes that end up dropping some of | ||
// the formatting/elastic annotations that the feature wanted respected. | ||
static (document, options, cancellationToken) => CleanupSyntaxAsync(document, options, cancellationToken), | ||
// Then add all missing imports to all changed documents. | ||
static (document, options, cancellationToken) => ImportAdder.AddImportsFromSymbolAnnotationAsync(document, Simplifier.AddImportsAnnotation, options.AddImportOptions, cancellationToken), | ||
// Then simplify any expanded constructs. | ||
static (document, options, cancellationToken) => Simplifier.ReduceAsync(document, Simplifier.Annotation, options.SimplifierOptions, cancellationToken), | ||
// The do any necessary case correction for VB files. | ||
static (document, options, cancellationToken) => CaseCorrector.CaseCorrectAsync(document, CaseCorrector.Annotation, cancellationToken), | ||
// Finally, after doing the semantic cleanup, do another syntax cleanup pass to ensure that the tree is in a | ||
// good state. The semantic cleanup passes may have introduced new nodes with elastic trivia that have to be | ||
// cleaned. | ||
static (document, options, cancellationToken) => CleanupSyntaxAsync(document, options, cancellationToken), | ||
]; | ||
|
||
private static async Task<Document> CleanupSyntaxAsync(Document document, CodeCleanupOptions options, CancellationToken cancellationToken) | ||
{ | ||
Contract.ThrowIfFalse(document.SupportsSyntaxTree); | ||
|
||
// format any node with explicit formatter annotation | ||
var document1 = await Formatter.FormatAsync(document, Formatter.Annotation, options.FormattingOptions, cancellationToken).ConfigureAwait(false); | ||
|
||
// format any elastic whitespace | ||
var document2 = await Formatter.FormatAsync(document1, SyntaxAnnotation.ElasticAnnotation, options.FormattingOptions, cancellationToken).ConfigureAwait(false); | ||
return document2; | ||
} | ||
|
||
internal static ImmutableArray<DocumentId> GetAllChangedOrAddedDocumentIds( | ||
Solution originalSolution, | ||
Solution changedSolution) | ||
{ | ||
var solutionChanges = changedSolution.GetChanges(originalSolution); | ||
var documentIds = solutionChanges | ||
.GetProjectChanges() | ||
.SelectMany(p => p.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true).Concat(p.GetAddedDocuments())) | ||
.Concat(solutionChanges.GetAddedProjects().SelectMany(p => p.DocumentIds)) | ||
.ToImmutableArray(); | ||
return documentIds; | ||
} | ||
|
||
internal static async Task<Solution> CleanSyntaxAndSemanticsAsync( | ||
Solution originalSolution, | ||
Solution changedSolution, | ||
CodeCleanupOptionsProvider optionsProvider, | ||
IProgress<CodeAnalysisProgress> progress, | ||
CancellationToken cancellationToken) | ||
{ | ||
var documentIds = GetAllChangedOrAddedDocumentIds(originalSolution, changedSolution); | ||
var documentIdsAndOptionsToClean = await GetDocumentIdsAndOptionsToCleanAsync().ConfigureAwait(false); | ||
|
||
// Then do a pass where we cleanup semantics. | ||
var cleanedSolution = await RunAllCleanupPassesInOrderAsync( | ||
changedSolution, documentIdsAndOptionsToClean, progress, cancellationToken).ConfigureAwait(false); | ||
|
||
return cleanedSolution; | ||
|
||
async Task<ImmutableArray<(DocumentId documentId, CodeCleanupOptions codeCleanupOptions)>> GetDocumentIdsAndOptionsToCleanAsync() | ||
{ | ||
using var _ = ArrayBuilder<(DocumentId documentId, CodeCleanupOptions options)>.GetInstance(documentIds.Length, out var documentIdsAndOptions); | ||
foreach (var documentId in documentIds) | ||
{ | ||
var document = changedSolution.GetRequiredDocument(documentId); | ||
|
||
// Only care about documents that support syntax. Non-C#/VB files can't be cleaned. | ||
if (document.SupportsSyntaxTree) | ||
{ | ||
var codeActionOptions = await document.GetCodeCleanupOptionsAsync(optionsProvider, cancellationToken).ConfigureAwait(false); | ||
documentIdsAndOptions.Add((documentId, codeActionOptions)); | ||
} | ||
} | ||
|
||
return documentIdsAndOptions.ToImmutableAndClear(); | ||
} | ||
} | ||
|
||
internal static async ValueTask<Document> CleanupDocumentAsync(Document document, CodeCleanupOptions options, CancellationToken cancellationToken) | ||
{ | ||
if (!document.SupportsSyntaxTree) | ||
return document; | ||
|
||
var cleanedSolution = await RunAllCleanupPassesInOrderAsync( | ||
document.Project.Solution, | ||
[(document.Id, options)], | ||
CodeAnalysisProgress.None, | ||
cancellationToken).ConfigureAwait(false); | ||
|
||
return cleanedSolution.GetRequiredDocument(document.Id); | ||
} | ||
|
||
private static async Task<Solution> RunAllCleanupPassesInOrderAsync( | ||
Solution solution, | ||
ImmutableArray<(DocumentId documentId, CodeCleanupOptions options)> documentIdsAndOptions, | ||
IProgress<CodeAnalysisProgress> progress, | ||
CancellationToken cancellationToken) | ||
{ | ||
// One item per document per cleanup pass. | ||
progress.AddItems(documentIdsAndOptions.Length * s_cleanupPasses.Length); | ||
|
||
var currentSolution = solution; | ||
foreach (var cleanupPass in s_cleanupPasses) | ||
currentSolution = await RunParallelCleanupPassAsync(currentSolution, cleanupPass).ConfigureAwait(false); | ||
|
||
return currentSolution; | ||
|
||
async Task<Solution> RunParallelCleanupPassAsync( | ||
Solution solution, Func<Document, CodeCleanupOptions, CancellationToken, Task<Document>> cleanupDocumentAsync) | ||
{ | ||
// We're about to making a ton of calls to this new solution, including expensive oop calls to get up to | ||
// date compilations, skeletons and SG docs. Create and pin this solution so that all remote calls operate | ||
// 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( | ||
source: documentIdsAndOptions, | ||
produceItems: static async (documentIdAndOptions, callback, args, cancellationToken) => | ||
{ | ||
// As we finish each document, update our progress. | ||
using var _ = args.progress.ItemCompletedScope(); | ||
var (documentId, options) = documentIdAndOptions; | ||
// Fetch the current state of the document from this fork of the solution. | ||
var document = args.solution.GetRequiredDocument(documentId); | ||
Contract.ThrowIfFalse(document.SupportsSyntaxTree, "GetDocumentIdsAndOptionsAsync should only be returning documents that support syntax"); | ||
// Now, perform the requested cleanup pass on it. | ||
var cleanedDocument = await args.cleanupDocumentAsync(document, options, cancellationToken).ConfigureAwait(false); | ||
if (cleanedDocument is null || cleanedDocument == document) | ||
return; | ||
// Now get the cleaned root and pass it back to the consumer. | ||
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.