Skip to content

Commit fca6e1f

Browse files
Merge pull request #73368 from CyrusNajmabadi/parallelCodeCleanup
2 parents 57b457e + 0bf8be7 commit fca6e1f

File tree

1 file changed

+68
-114
lines changed

1 file changed

+68
-114
lines changed

src/VisualStudio/Core/Def/CodeCleanup/AbstractCodeCleanUpFixer.cs

Lines changed: 68 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,10 @@
1111
using Microsoft.CodeAnalysis;
1212
using Microsoft.CodeAnalysis.CodeActions;
1313
using Microsoft.CodeAnalysis.CodeCleanup;
14-
using Microsoft.CodeAnalysis.Editor;
1514
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
1615
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
17-
using Microsoft.CodeAnalysis.Formatting;
1816
using Microsoft.CodeAnalysis.Host;
1917
using Microsoft.CodeAnalysis.Options;
20-
using Microsoft.CodeAnalysis.PooledObjects;
2118
using Microsoft.CodeAnalysis.Progress;
2219
using Microsoft.CodeAnalysis.Shared.Extensions;
2320
using Microsoft.CodeAnalysis.Shared.Utilities;
@@ -69,7 +66,13 @@ private async Task<bool> FixHierarchyContentAsync(IVsHierarchyCodeCleanupScope h
6966
var hierarchy = hierarchyContent.Hierarchy;
7067
if (hierarchy == null)
7168
{
72-
return await FixSolutionAsync(_workspace.CurrentSolution, context).ConfigureAwait(true);
69+
var solution = _workspace.CurrentSolution;
70+
return await FixAsync(
71+
_workspace,
72+
// Just defer to FixProjectsAsync, passing in all fixable projects in the solution.
73+
(progress, cancellationToken) => FixProjectsAsync(
74+
_globalOptions, solution, solution.Projects.Where(p => p.SupportsCompilation).ToImmutableArray(), context.EnabledFixIds, progress, cancellationToken),
75+
context).ConfigureAwait(false);
7376
}
7477

7578
// Map the hierarchy to a ProjectId. For hierarchies mapping to multitargeted projects, we first try to
@@ -90,9 +93,7 @@ private async Task<bool> FixHierarchyContentAsync(IVsHierarchyCodeCleanupScope h
9093
{
9194
var projectHierarchyItem = _vsHierarchyItemManager.GetHierarchyItem(hierarchyContent.Hierarchy, (uint)VSConstants.VSITEMID.Root);
9295
if (!hierarchyToProjectMap.TryGetProjectId(projectHierarchyItem, targetFrameworkMoniker: null, out projectId))
93-
{
9496
return false;
95-
}
9697
}
9798

9899
var itemId = hierarchyContent.ItemId;
@@ -101,12 +102,15 @@ private async Task<bool> FixHierarchyContentAsync(IVsHierarchyCodeCleanupScope h
101102
await TaskScheduler.Default;
102103

103104
var project = _workspace.CurrentSolution.GetProject(projectId);
104-
if (project == null)
105-
{
105+
if (project == null || !project.SupportsCompilation)
106106
return false;
107-
}
108107

109-
return await FixProjectAsync(project, context).ConfigureAwait(true);
108+
return await FixAsync(
109+
_workspace,
110+
// Just defer to FixProjectsAsync, passing in this single project to fix.
111+
(progress, cancellationToken) => FixProjectsAsync(
112+
_globalOptions, project.Solution, [project], context.EnabledFixIds, progress, cancellationToken),
113+
context).ConfigureAwait(false);
110114
}
111115
else if (hierarchy.GetCanonicalName(itemId, out var path) == 0)
112116
{
@@ -126,54 +130,25 @@ private async Task<bool> FixHierarchyContentAsync(IVsHierarchyCodeCleanupScope h
126130
var documentIds = solution.GetDocumentIdsWithFilePath(path);
127131
var documentId = documentIds.FirstOrDefault(id => id.ProjectId == projectId);
128132
if (documentId is null)
129-
{
130133
return false;
131-
}
132134

133135
var document = solution.GetRequiredDocument(documentId);
134136
var options = _globalOptions.GetCodeActionOptions(document.Project.Services);
135-
return await FixDocumentAsync(document, options, context).ConfigureAwait(true);
137+
138+
return await FixAsync(
139+
_workspace,
140+
async (progress, cancellationToken) =>
141+
{
142+
var newDocument = await FixDocumentAsync(document, context.EnabledFixIds, progress, options, cancellationToken).ConfigureAwait(true);
143+
return newDocument.Project.Solution;
144+
},
145+
context).ConfigureAwait(false);
136146
}
137147
}
138148

139149
return false;
140150
}
141151

142-
private Task<bool> FixSolutionAsync(Solution solution, ICodeCleanUpExecutionContext context)
143-
{
144-
return FixAsync(_workspace, ApplyFixAsync, context);
145-
146-
// Local function
147-
Task<Solution> ApplyFixAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
148-
{
149-
return FixSolutionAsync(solution, context.EnabledFixIds, progress, cancellationToken);
150-
}
151-
}
152-
153-
private Task<bool> FixProjectAsync(Project project, ICodeCleanUpExecutionContext context)
154-
{
155-
return FixAsync(_workspace, ApplyFixAsync, context);
156-
157-
// Local function
158-
async Task<Solution> ApplyFixAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
159-
{
160-
var newProject = await FixProjectAsync(project, context.EnabledFixIds, progress, addProgressItemsForDocuments: true, cancellationToken).ConfigureAwait(true);
161-
return newProject.Solution;
162-
}
163-
}
164-
165-
private Task<bool> FixDocumentAsync(Document document, CodeActionOptions options, ICodeCleanUpExecutionContext context)
166-
{
167-
return FixAsync(document.Project.Solution.Workspace, ApplyFixAsync, context);
168-
169-
// Local function
170-
async Task<Solution> ApplyFixAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
171-
{
172-
var newDocument = await FixDocumentAsync(document, context.EnabledFixIds, progress, options, cancellationToken).ConfigureAwait(true);
173-
return newDocument.Project.Solution;
174-
}
175-
}
176-
177152
private Task<bool> FixTextBufferAsync(TextBufferCodeCleanUpScope textBufferScope, ICodeCleanUpExecutionContext context)
178153
{
179154
var buffer = textBufferScope.SubjectBuffer;
@@ -215,9 +190,7 @@ private async Task<bool> FixAsync(
215190
{
216191
var workspaceStatusService = workspace.Services.GetService<IWorkspaceStatusService>();
217192
if (workspaceStatusService != null)
218-
{
219193
await workspaceStatusService.WaitUntilFullyLoadedAsync(context.OperationContext.UserCancellationToken).ConfigureAwait(true);
220-
}
221194
}
222195

223196
using (var scope = context.OperationContext.AddScope(allowCancellation: true, description: EditorFeaturesResources.Applying_changes))
@@ -233,87 +206,68 @@ private async Task<bool> FixAsync(
233206
}
234207
}
235208

236-
private async Task<Solution> FixSolutionAsync(
209+
private static async Task<Solution> FixProjectsAsync(
210+
IGlobalOptionService globalOptions,
237211
Solution solution,
212+
ImmutableArray<Project> projects,
238213
FixIdContainer enabledFixIds,
239214
IProgress<CodeAnalysisProgress> progressTracker,
240215
CancellationToken cancellationToken)
241216
{
242-
// Prepopulate the solution progress tracker with the total number of documents to process
243-
foreach (var projectId in solution.ProjectIds)
244-
{
245-
var project = solution.GetRequiredProject(projectId);
246-
if (!CanCleanupProject(project))
247-
{
248-
continue;
249-
}
250-
251-
progressTracker.AddItems(project.DocumentIds.Count);
252-
}
253-
254-
foreach (var projectId in solution.ProjectIds)
255-
{
256-
cancellationToken.ThrowIfCancellationRequested();
257-
258-
var project = solution.GetRequiredProject(projectId);
259-
var newProject = await FixProjectAsync(project, enabledFixIds, progressTracker, addProgressItemsForDocuments: false, cancellationToken).ConfigureAwait(false);
260-
solution = newProject.Solution;
261-
}
262-
263-
return solution;
264-
}
265-
266-
private async Task<Project> FixProjectAsync(
267-
Project project,
268-
FixIdContainer enabledFixIds,
269-
IProgress<CodeAnalysisProgress> progressTracker,
270-
bool addProgressItemsForDocuments,
271-
CancellationToken cancellationToken)
272-
{
273-
if (!CanCleanupProject(project))
274-
{
275-
return project;
276-
}
277-
278-
if (addProgressItemsForDocuments)
279-
{
280-
progressTracker.AddItems(project.DocumentIds.Count);
281-
}
282-
283-
var ideOptions = _globalOptions.GetCodeActionOptions(project.Services);
284-
285-
foreach (var documentId in project.DocumentIds)
286-
{
287-
cancellationToken.ThrowIfCancellationRequested();
288-
289-
var document = project.GetRequiredDocument(documentId);
290-
progressTracker.Report(CodeAnalysisProgress.Description(document.Name));
217+
// Add an item for each document in all the projects we're processing.
218+
progressTracker.AddItems(projects.Sum(static p => p.DocumentIds.Count));
291219

292-
// FixDocumentAsync reports progress within a document, but we only want to report progress at the
293-
// project granularity. So we pass CodeAnalysisProgress.None here so that inner progress updates don't
294-
// affect us.
295-
var fixedDocument = await FixDocumentAsync(document, enabledFixIds, CodeAnalysisProgress.None, ideOptions, cancellationToken).ConfigureAwait(false);
296-
project = fixedDocument.Project;
297-
progressTracker.ItemCompleted();
298-
}
299-
300-
return project;
220+
// Run in parallel across all projects.
221+
return await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync(
222+
source: projects,
223+
produceItems: static async (project, callback, args, cancellationToken) =>
224+
{
225+
Contract.ThrowIfFalse(project.SupportsCompilation);
226+
cancellationToken.ThrowIfCancellationRequested();
227+
228+
var ideOptions = args.globalOptions.GetCodeActionOptions(project.Services);
229+
230+
// And for each project, process all the documents in parallel.
231+
await RoslynParallel.ForEachAsync(
232+
source: project.Documents,
233+
cancellationToken,
234+
async (document, cancellationToken) =>
235+
{
236+
using var _ = args.progressTracker.ItemCompletedScope();
237+
238+
// FixDocumentAsync reports progress within a document, but we only want to report progress at
239+
// the document granularity. So we pass CodeAnalysisProgress.None here so that inner progress
240+
// updates don't affect us.
241+
var fixedDocument = await FixDocumentAsync(document, args.enabledFixIds, CodeAnalysisProgress.None, ideOptions, cancellationToken).ConfigureAwait(false);
242+
if (fixedDocument == document)
243+
return;
244+
245+
callback((document.Id, await fixedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false)));
246+
}).ConfigureAwait(false);
247+
},
248+
consumeItems: static async (stream, args, cancellationToken) =>
249+
{
250+
// Now consume the changed documents, applying their new roots to the solution.
251+
var currentSolution = args.solution;
252+
await foreach (var (documentId, newRoot) in stream)
253+
currentSolution = currentSolution.WithDocumentSyntaxRoot(documentId, newRoot);
254+
255+
return currentSolution;
256+
},
257+
args: (globalOptions, solution, enabledFixIds, progressTracker),
258+
cancellationToken).ConfigureAwait(false);
301259
}
302260

303-
private static bool CanCleanupProject(Project project)
304-
=> project.Services.GetService<ICodeCleanupService>() != null;
305-
306261
private static async Task<Document> FixDocumentAsync(
307262
Document document,
308263
FixIdContainer enabledFixIds,
309264
IProgress<CodeAnalysisProgress> progressTracker,
310265
CodeActionOptions ideOptions,
311266
CancellationToken cancellationToken)
312267
{
268+
cancellationToken.ThrowIfCancellationRequested();
313269
if (document.IsGeneratedCode(cancellationToken))
314-
{
315270
return document;
316-
}
317271

318272
var codeCleanupService = document.GetRequiredLanguageService<ICodeCleanupService>();
319273

0 commit comments

Comments
 (0)