-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Stop loading ISourceGenerators within VS entirely. #72835
Changes from 12 commits
7c8093f
57f949d
6d08e05
fefad98
fa80b6f
3d7d80c
477d757
7313d4c
099eb3b
456b4b7
f93b32d
deaa0a9
c4ada55
a2c2850
72b3945
4290774
c2a430c
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 |
---|---|---|
|
@@ -31,6 +31,12 @@ internal interface IRemoteSourceGenerationService | |
/// </summary> | ||
ValueTask<ImmutableArray<string>> GetContentsAsync( | ||
Checksum solutionChecksum, ProjectId projectId, ImmutableArray<DocumentId> documentIds, CancellationToken cancellationToken); | ||
|
||
/// <summary> | ||
/// Whether or not the specified <paramref name="projectId"/> has source generators or not. | ||
/// </summary> | ||
ValueTask<bool> HasGeneratorsAsync( | ||
Checksum solutionChecksum, ProjectId projectId, CancellationToken cancellationToken); | ||
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. Pretty self-explanatory. |
||
} | ||
|
||
/// <summary> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,17 +2,19 @@ | |
// 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.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using System.Runtime.CompilerServices; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using Microsoft.CodeAnalysis.Remote; | ||
using Microsoft.CodeAnalysis.Shared.Collections; | ||
using Microsoft.CodeAnalysis.SourceGeneration; | ||
using Roslyn.Utilities; | ||
|
||
namespace Microsoft.CodeAnalysis; | ||
|
||
using AnalyzerReferencesToSourceGenerators = ConditionalWeakTable<IReadOnlyList<AnalyzerReference>, SolutionCompilationState.SourceGeneratorMap>; | ||
|
||
internal partial class SolutionCompilationState | ||
{ | ||
internal sealed record SourceGeneratorMap( | ||
|
@@ -26,52 +28,92 @@ internal sealed record SourceGeneratorMap( | |
/// of things so that we don't cause source generators to be loaded (and fixed) within VS (which is .net framework | ||
/// only). | ||
/// </summary> | ||
private static readonly ImmutableArray<(string language, AnalyzerReferencesToSourceGenerators referencesToGenerators, AnalyzerReferencesToSourceGenerators.CreateValueCallback callback)> s_languageToAnalyzerReferencesToSourceGeneratorsMap = | ||
[ | ||
(LanguageNames.CSharp, new(), (static rs => ComputeSourceGenerators(rs, LanguageNames.CSharp))), | ||
(LanguageNames.VisualBasic, new(), (static rs => ComputeSourceGenerators(rs, LanguageNames.VisualBasic))), | ||
]; | ||
|
||
private static SourceGeneratorMap ComputeSourceGenerators(IReadOnlyList<AnalyzerReference> analyzerReferences, string language) | ||
{ | ||
using var generators = TemporaryArray<ISourceGenerator>.Empty; | ||
var generatorToAnalyzerReference = ImmutableDictionary.CreateBuilder<ISourceGenerator, AnalyzerReference>(); | ||
private static readonly ConditionalWeakTable<ProjectState, SourceGeneratorMap> s_projectStateToSourceGeneratorsMap = new(); | ||
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 was a simpler way to organize the existing information. ProjectState already has the language and analyzerreferences. It's also what callers already pass in. And it's a green node which rarely changes. Note: if it does change, we'll 'recompute' the SGs, but end up finding the same set, since the AnalyzerRef objects themselves cache and return the same instances.
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
foreach (var reference in analyzerReferences) | ||
{ | ||
foreach (var generator in reference.GetGenerators(language).Distinct()) | ||
{ | ||
generators.Add(generator); | ||
generatorToAnalyzerReference.Add(generator, reference); | ||
} | ||
} | ||
|
||
return new(generators.ToImmutableAndClear(), generatorToAnalyzerReference.ToImmutable()); | ||
} | ||
/// <summary> | ||
/// Cached information about if a project has source generators or not. Note: this is distinct from <see | ||
/// cref="s_projectStateToSourceGeneratorsMap"/> as we want to be able to compute it by calling over to our OOP | ||
/// process (if present) and having it make the determination, without the host necessarily loading generators | ||
/// itself. | ||
/// </summary> | ||
private static readonly ConditionalWeakTable<ProjectState, AsyncLazy<bool>> s_hasSourceGeneratorsMap = new(); | ||
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. new to this PR. 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. 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. tried, but it started getting very ugly/allocy |
||
|
||
/// <summary> | ||
/// This method should only be called in a .net core host like our out of process server. | ||
/// </summary> | ||
private static ImmutableArray<ISourceGenerator> GetSourceGenerators(ProjectState projectState) | ||
=> GetSourceGenerators(projectState.Language, projectState.AnalyzerReferences); | ||
|
||
private static ImmutableArray<ISourceGenerator> GetSourceGenerators(string language, IReadOnlyList<AnalyzerReference> analyzerReferences) | ||
{ | ||
var map = GetSourceGeneratorMap(language, analyzerReferences); | ||
var map = GetSourceGeneratorMap(projectState); | ||
return map is null ? [] : map.SourceGenerators; | ||
} | ||
|
||
/// <summary> | ||
/// This method should only be called in a .net core host like our out of process server. | ||
/// </summary> | ||
private static AnalyzerReference GetAnalyzerReference(ProjectState projectState, ISourceGenerator sourceGenerator) | ||
{ | ||
var map = GetSourceGeneratorMap(projectState.Language, projectState.AnalyzerReferences); | ||
var map = GetSourceGeneratorMap(projectState); | ||
Contract.ThrowIfNull(map); | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return map.SourceGeneratorToAnalyzerReference[sourceGenerator]; | ||
} | ||
|
||
private static SourceGeneratorMap? GetSourceGeneratorMap(string language, IReadOnlyList<AnalyzerReference> analyzerReferences) | ||
private static SourceGeneratorMap? GetSourceGeneratorMap(ProjectState projectState) | ||
{ | ||
var tupleOpt = s_languageToAnalyzerReferencesToSourceGeneratorsMap.FirstOrNull(static (t, language) => t.language == language, language); | ||
if (tupleOpt is null) | ||
if (!projectState.SupportsCompilation) | ||
return null; | ||
|
||
var tuple = tupleOpt.Value; | ||
return tuple.referencesToGenerators.GetValue(analyzerReferences, tuple.callback); | ||
return s_projectStateToSourceGeneratorsMap.GetValue(projectState, ComputeSourceGenerators); | ||
|
||
static SourceGeneratorMap ComputeSourceGenerators(ProjectState projectState) | ||
{ | ||
using var generators = TemporaryArray<ISourceGenerator>.Empty; | ||
var generatorToAnalyzerReference = ImmutableDictionary.CreateBuilder<ISourceGenerator, AnalyzerReference>(); | ||
|
||
foreach (var reference in projectState.AnalyzerReferences) | ||
{ | ||
foreach (var generator in reference.GetGenerators(projectState.Language).Distinct()) | ||
{ | ||
generators.Add(generator); | ||
generatorToAnalyzerReference.Add(generator, reference); | ||
} | ||
} | ||
|
||
return new(generators.ToImmutableAndClear(), generatorToAnalyzerReference.ToImmutable()); | ||
} | ||
} | ||
|
||
public async Task<bool> HasSourceGeneratorsAsync(ProjectId projectId, CancellationToken cancellationToken) | ||
{ | ||
var projectState = this.SolutionState.GetRequiredProjectState(projectId); | ||
|
||
if (!s_hasSourceGeneratorsMap.TryGetValue(projectState, out var lazy)) | ||
{ | ||
// Extracted into local function to prevent allocations in the case where we find a value in the cache. | ||
lazy = GetLazy(projectState); | ||
} | ||
|
||
return await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
AsyncLazy<bool> GetLazy(ProjectState projectState) | ||
=> s_hasSourceGeneratorsMap.GetValue( | ||
projectState, | ||
projectState => AsyncLazy.Create(cancellationToken => ComputeHasSourceGeneratorsAsync(projectState, cancellationToken))); | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
async Task<bool> ComputeHasSourceGeneratorsAsync( | ||
ProjectState projectState, CancellationToken cancellationToken) | ||
{ | ||
var client = await RemoteHostClient.TryGetClientAsync(this.Services, cancellationToken).ConfigureAwait(false); | ||
// If in proc, just load the generators and see if we have any. | ||
if (client is null) | ||
return GetSourceGenerators(projectState).Any(); | ||
|
||
// Out of process, call to the remote to figure this out. | ||
var result = await client.TryInvokeAsync<IRemoteSourceGenerationService, bool>( | ||
this, | ||
projectId, | ||
(service, solution, cancellationToken) => service.HasGeneratorsAsync(solution, projectId, cancellationToken), | ||
cancellationToken).ConfigureAwait(false); | ||
return result.HasValue && result.Value; | ||
} | ||
} | ||
} |
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.
helper overload for the case where we're asking about a particular project in the context of a particular SolutionCompilationState (first time we've needed to do that).