-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Use IAsyncEnumerable in DesignerAttributeScanning as well. #64616
Changes from 26 commits
e28254c
a817931
fa69c39
a49728d
c286a2e
74de942
603ab84
662ed93
9c82e2c
8367e95
ae73c44
4741d77
a640e5c
04b3ce6
3ee80ae
9294e94
6a75f57
68a6cc1
315438a
fb73214
45f5450
9c6e84e
9f6448e
e908fda
6a8f876
58b571c
5bac3e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,15 +3,12 @@ | |
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Linq; | ||
using System.Runtime.CompilerServices; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis.ErrorReporting; | ||
using Microsoft.CodeAnalysis.Host; | ||
using Microsoft.CodeAnalysis.Host.Mef; | ||
using Microsoft.CodeAnalysis.PooledObjects; | ||
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
|
@@ -22,154 +19,51 @@ namespace Microsoft.CodeAnalysis.DesignerAttribute | |
[ExportWorkspaceService(typeof(IDesignerAttributeDiscoveryService)), Shared] | ||
internal sealed partial class DesignerAttributeDiscoveryService : IDesignerAttributeDiscoveryService | ||
{ | ||
/// <summary> | ||
/// Protects mutable state in this type. | ||
/// </summary> | ||
private readonly SemaphoreSlim _gate = new SemaphoreSlim(initialCount: 1); | ||
|
||
/// <summary> | ||
/// Keep track of the last information we reported. We will avoid notifying the host if we recompute and these | ||
/// don't change. | ||
/// </summary> | ||
private readonly ConcurrentDictionary<DocumentId, (string? category, VersionStamp projectVersion)> _documentToLastReportedInformation = new(); | ||
|
||
[ImportingConstructor] | ||
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
public DesignerAttributeDiscoveryService() | ||
{ | ||
} | ||
|
||
public async ValueTask ProcessSolutionAsync( | ||
Solution solution, | ||
DocumentId? priorityDocumentId, | ||
IDesignerAttributeDiscoveryService.ICallback callback, | ||
CancellationToken cancellationToken) | ||
{ | ||
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) | ||
{ | ||
// Remove any documents that are now gone. | ||
foreach (var docId in _documentToLastReportedInformation.Keys) | ||
{ | ||
if (!solution.ContainsDocument(docId)) | ||
_documentToLastReportedInformation.TryRemove(docId, out _); | ||
} | ||
|
||
// Handle the priority doc first. | ||
var priorityDocument = solution.GetDocument(priorityDocumentId); | ||
if (priorityDocument != null) | ||
await ProcessProjectAsync(priorityDocument.Project, priorityDocument, callback, cancellationToken).ConfigureAwait(false); | ||
|
||
// Process the rest of the projects in dependency order so that their data is ready when we hit the | ||
// projects that depend on them. | ||
var dependencyGraph = solution.GetProjectDependencyGraph(); | ||
foreach (var projectId in dependencyGraph.GetTopologicallySortedProjects(cancellationToken)) | ||
{ | ||
if (projectId != priorityDocumentId?.ProjectId) | ||
await ProcessProjectAsync(solution.GetRequiredProject(projectId), specificDocument: null, callback, cancellationToken).ConfigureAwait(false); | ||
} | ||
} | ||
} | ||
|
||
private async Task ProcessProjectAsync( | ||
public async IAsyncEnumerable<DesignerAttributeData> ProcessProjectAsync( | ||
Project project, | ||
Document? specificDocument, | ||
IDesignerAttributeDiscoveryService.ICallback callback, | ||
CancellationToken cancellationToken) | ||
DocumentId? priorityDocumentId, | ||
[EnumeratorCancellation] CancellationToken cancellationToken) | ||
{ | ||
// Ignore projects that don't support compilation or don't even have the DesignerCategoryAttribute in it. | ||
if (!project.SupportsCompilation) | ||
return; | ||
yield break; | ||
|
||
var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); | ||
var designerCategoryType = compilation.DesignerCategoryAttributeType(); | ||
if (designerCategoryType == null) | ||
return; | ||
|
||
await ScanForDesignerCategoryUsageAsync( | ||
project, specificDocument, callback, designerCategoryType, cancellationToken).ConfigureAwait(false); | ||
|
||
// If we scanned just a specific document in the project, now scan the rest of the files. | ||
if (specificDocument != null) | ||
await ScanForDesignerCategoryUsageAsync(project, specificDocument: null, callback, designerCategoryType, cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
private async Task ScanForDesignerCategoryUsageAsync( | ||
Project project, | ||
Document? specificDocument, | ||
IDesignerAttributeDiscoveryService.ICallback callback, | ||
INamedTypeSymbol designerCategoryType, | ||
CancellationToken cancellationToken) | ||
{ | ||
// We need to reanalyze the project whenever it (or any of its dependencies) have | ||
// changed. We need to know about dependencies since if a downstream project adds the | ||
// DesignerCategory attribute to a class, that can affect us when we examine the classes | ||
// in this project. | ||
var projectVersion = await project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); | ||
yield break; | ||
|
||
// Now get all the values that actually changed and notify VS about them. We don't need | ||
// to tell it about the ones that didn't change since that will have no effect on the | ||
// user experience. | ||
var changedData = await ComputeChangedDataAsync( | ||
project, specificDocument, projectVersion, designerCategoryType, cancellationToken).ConfigureAwait(false); | ||
|
||
// Only bother reporting non-empty information to save an unnecessary RPC. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this isn't necessary anymore. by definition, if we don't yield something, we have nothing to send over the rpc channel. |
||
if (!changedData.IsEmpty) | ||
await callback.ReportDesignerAttributeDataAsync(changedData, cancellationToken).ConfigureAwait(false); | ||
|
||
// Now, keep track of what we've reported to the host so we won't report unchanged files in the future. We | ||
// do this after the report has gone through as we want to make sure that if it cancels for any reason we | ||
// don't hold onto values that may not have made it all the way to the project system. | ||
foreach (var data in changedData) | ||
_documentToLastReportedInformation[data.DocumentId] = (data.Category, projectVersion); | ||
} | ||
// If there is a priority doc, then scan that first. | ||
var priorityDocument = priorityDocumentId == null ? null : project.GetDocument(priorityDocumentId); | ||
if (priorityDocument is { FilePath: not null }) | ||
{ | ||
var data = await ComputeDesignerAttributeDataAsync(designerCategoryType, priorityDocument, cancellationToken).ConfigureAwait(false); | ||
if (data != null) | ||
yield return data.Value; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is much cleaner too. we only send the data for things that actually have a category. in practice this is a tiny handful of messages in a few projects. This is different from before when we'd send info about every document, even those without a category. |
||
} | ||
|
||
private async Task<ImmutableArray<DesignerAttributeData>> ComputeChangedDataAsync( | ||
Project project, | ||
Document? specificDocument, | ||
VersionStamp projectVersion, | ||
INamedTypeSymbol designerCategoryType, | ||
CancellationToken cancellationToken) | ||
{ | ||
using var _1 = ArrayBuilder<Task<DesignerAttributeData?>>.GetInstance(out var tasks); | ||
// now process the rest of the documents. | ||
using var _ = ArrayBuilder<Task<DesignerAttributeData?>>.GetInstance(out var tasks); | ||
foreach (var document in project.Documents) | ||
{ | ||
// If we're only analyzing a specific document, then skip the rest. | ||
if (specificDocument != null && document != specificDocument) | ||
continue; | ||
|
||
// If we don't have a path for this document, we cant proceed with it. | ||
// We need that path to inform the project system which file we're referring to. | ||
if (document.FilePath == null) | ||
if (document == priorityDocument || document.FilePath is null) | ||
continue; | ||
|
||
// If nothing has changed at the top level between the last time we analyzed this document and now, then | ||
// no need to analyze again. | ||
if (_documentToLastReportedInformation.TryGetValue(document.Id, out var existingInfo) && | ||
existingInfo.projectVersion == projectVersion) | ||
{ | ||
continue; | ||
} | ||
|
||
tasks.Add(ComputeDesignerAttributeDataAsync(designerCategoryType, document, cancellationToken)); | ||
} | ||
|
||
using var _2 = ArrayBuilder<DesignerAttributeData>.GetInstance(tasks.Count, out var results); | ||
|
||
// Avoid unnecessary allocation of result array. | ||
await Task.WhenAll((IEnumerable<Task>)tasks).ConfigureAwait(false); | ||
|
||
foreach (var task in tasks) | ||
// Convert the tasks into one final stream we can read all the results from. | ||
await foreach (var dataOpt in tasks.ToImmutable().StreamAsync(cancellationToken).ConfigureAwait(false)) | ||
{ | ||
var dataOpt = await task.ConfigureAwait(false); | ||
if (dataOpt == null) | ||
continue; | ||
|
||
var data = dataOpt.Value; | ||
_documentToLastReportedInformation.TryGetValue(data.DocumentId, out var existingInfo); | ||
if (existingInfo.category != data.Category) | ||
results.Add(data); | ||
if (dataOpt != null) | ||
yield return dataOpt.Value; | ||
} | ||
|
||
return results.ToImmutableAndClear(); | ||
} | ||
|
||
private static async Task<DesignerAttributeData?> ComputeDesignerAttributeDataAsync( | ||
|
@@ -179,18 +73,16 @@ private async Task<ImmutableArray<DesignerAttributeData>> ComputeChangedDataAsyn | |
{ | ||
Contract.ThrowIfNull(document.FilePath); | ||
|
||
// We either haven't computed the designer info, or our data was out of date. We need | ||
// So recompute here. Figure out what the current category is, and if that's different | ||
// from what we previously stored. | ||
var category = await DesignerAttributeHelpers.ComputeDesignerAttributeCategoryAsync( | ||
designerCategoryType, document, cancellationToken).ConfigureAwait(false); | ||
|
||
return new DesignerAttributeData | ||
{ | ||
Category = category, | ||
DocumentId = document.Id, | ||
FilePath = document.FilePath, | ||
}; | ||
// If there's no category (the common case) don't return anything. The host itself will see no results | ||
// returned and can handle that case (for example, if a type previously had the attribute but doesn't | ||
// any longer). | ||
if (category == null) | ||
return null; | ||
|
||
return new DesignerAttributeData(category, document.Id, document.FilePath); | ||
} | ||
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) | ||
{ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the remote side. it became completely stateless (which is good). there was a lot of complexity about the remote side trying to keep track of what hte host side might now and preventing updates. this was error prone and doesn't work in the IASyncEnumerable world. e.g. in remote IAE, jsut because you yielded the data, you dont' know that hte host has actually gotten it.
this moved to a cleaner model where the remote side knows nothing, and the host side keeps track.