From 7d13eae87cc08adf7bde0b7b7636ef61e4546c02 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 1 Apr 2024 15:14:40 -0700 Subject: [PATCH 1/3] in progress --- .../Workspace/Solution/ProjectState.cs | 37 ------------ ...lutionCompilationState_SourceGenerators.cs | 60 +++++++++++++++++++ 2 files changed, 60 insertions(+), 37 deletions(-) create mode 100644 src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index 9de59969c270a..013c0f9aaa830 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -61,11 +61,6 @@ internal partial class ProjectState private AnalyzerOptions? _lazyAnalyzerOptions; - /// - /// The list of source generators and the analyzer reference they came from. - /// - private ImmutableDictionary? _lazySourceGenerators; - private ProjectState( ProjectInfo projectInfo, LanguageServices languageServices, @@ -737,38 +732,6 @@ public ProjectState WithAnalyzerReferences(IEnumerable analyz return With(projectInfo: ProjectInfo.WithAnalyzerReferences(analyzerReferences).WithVersion(Version.GetNewerVersion())); } - [MemberNotNull(nameof(_lazySourceGenerators))] - private void EnsureSourceGeneratorsInitialized() - { - if (_lazySourceGenerators == null) - { - var builder = ImmutableDictionary.CreateBuilder(); - - foreach (var analyzerReference in AnalyzerReferences) - { - foreach (var generator in analyzerReference.GetGenerators(Language)) - builder.Add(generator, analyzerReference); - } - - Interlocked.CompareExchange(ref _lazySourceGenerators, builder.ToImmutable(), comparand: null); - } - } - - public IEnumerable SourceGenerators - { - get - { - EnsureSourceGeneratorsInitialized(); - return _lazySourceGenerators.Keys; - } - } - - public AnalyzerReference GetAnalyzerReferenceForGenerator(ISourceGenerator generator) - { - EnsureSourceGeneratorsInitialized(); - return _lazySourceGenerators[generator]; - } - public ProjectState AddDocuments(ImmutableArray documents) { if (documents.IsEmpty) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs new file mode 100644 index 0000000000000..842bb79e7d19a --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs @@ -0,0 +1,60 @@ +// 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.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Shared.Collections; + +namespace Microsoft.CodeAnalysis; + +using AnalyzerReferencesToSourceGenerators = ConditionalWeakTable, SolutionCompilationState.SourceGeneratorMap>; + +internal partial class SolutionCompilationState +{ + internal sealed record SourceGeneratorMap(ImmutableArray SourceGenerators, ImmutableDictionary SourceGeneratorToAnalyzerReference) + + /// + /// Cached mapping from language (only C#/VB since those are the only languages that support analyzers) to the lists + /// of analyzer references (see ) to all the s produced by those references. This should only be created and cached on the OOP side + /// of things so that we don't cause source generators to be loaded (and fixed) within VS (which is .net framework + /// only). + /// + private static readonly Dictionary s_languageToAnalyzerReferencesToSourceGeneratorsMap = new() + { + { LanguageNames.CSharp, (new(), (static rs => ComputeSourceGenerators(rs, LanguageNames.CSharp))) }, + { LanguageNames.VisualBasic, (new(), (static rs => ComputeSourceGenerators(rs, LanguageNames.VisualBasic))) }, + }; + + private static SourceGeneratorMap ComputeSourceGenerators(IReadOnlyList analyzerReferences, string language) + { + using var generators = TemporaryArray.Empty; + var generatorToAnalyzerReference = ImmutableDictionary.CreateBuilder(); + + 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()); + } + + private static ImmutableArray GetSourceGenerators(ProjectState projectState) + => GetSourceGenerators(projectState.Language, projectState.AnalyzerReferences); + + private static ImmutableArray GetSourceGenerators(string language, IReadOnlyList analyzerReferences) + { + if (!s_languageToAnalyzerReferencesToSourceGeneratorsMap.TryGetValue(language, out var tuple)) + return []; + + var map = tuple.map.GetValue(analyzerReferences, tuple.callback); + return map.SourceGenerators; + } +} From d26d6d5631dd836334add08fb504b64845cf1453 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 1 Apr 2024 15:17:01 -0700 Subject: [PATCH 2/3] Use tuple --- ...lutionCompilationState_SourceGenerators.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs index 842bb79e7d19a..53a3039e81fbb 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Shared.Collections; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis; @@ -14,7 +15,9 @@ namespace Microsoft.CodeAnalysis; internal partial class SolutionCompilationState { - internal sealed record SourceGeneratorMap(ImmutableArray SourceGenerators, ImmutableDictionary SourceGeneratorToAnalyzerReference) + internal sealed record SourceGeneratorMap( + ImmutableArray SourceGenerators, + ImmutableDictionary SourceGeneratorToAnalyzerReference); /// /// Cached mapping from language (only C#/VB since those are the only languages that support analyzers) to the lists @@ -23,11 +26,11 @@ internal sealed record SourceGeneratorMap(ImmutableArray Sourc /// of things so that we don't cause source generators to be loaded (and fixed) within VS (which is .net framework /// only). /// - private static readonly Dictionary s_languageToAnalyzerReferencesToSourceGeneratorsMap = new() - { - { LanguageNames.CSharp, (new(), (static rs => ComputeSourceGenerators(rs, LanguageNames.CSharp))) }, - { LanguageNames.VisualBasic, (new(), (static rs => ComputeSourceGenerators(rs, LanguageNames.VisualBasic))) }, - }; + 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 analyzerReferences, string language) { @@ -51,10 +54,12 @@ private static ImmutableArray GetSourceGenerators(ProjectState private static ImmutableArray GetSourceGenerators(string language, IReadOnlyList analyzerReferences) { - if (!s_languageToAnalyzerReferencesToSourceGeneratorsMap.TryGetValue(language, out var tuple)) + var tupleOpt = s_languageToAnalyzerReferencesToSourceGeneratorsMap.FirstOrNull(static (t, language) => t.language == language, language); + if (tupleOpt is null) return []; - var map = tuple.map.GetValue(analyzerReferences, tuple.callback); + var tuple = tupleOpt.Value; + var map = tuple.referencesToGenerators.GetValue(analyzerReferences, tuple.callback); return map.SourceGenerators; } } From e3b7872e3b46b6cd31e5337a4c30119bd85032a8 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Mon, 1 Apr 2024 15:28:28 -0700 Subject: [PATCH 3/3] Simplify --- ...eratorTelemetryCollectorWorkspaceService.cs | 4 +++- ...eratorTelemetryCollectorWorkspaceService.cs | 13 ++++++++----- ...utionCompilationState.CompilationTracker.cs | 8 ++++---- ...ationState.CompilationTracker_Generators.cs | 10 ++++++---- ...mpilationState.TranslationAction_Actions.cs | 2 +- ...olutionCompilationState_SourceGenerators.cs | 18 +++++++++++++++--- 6 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/Workspaces/Core/Portable/SourceGeneratorTelemetry/ISourceGeneratorTelemetryCollectorWorkspaceService.cs b/src/Workspaces/Core/Portable/SourceGeneratorTelemetry/ISourceGeneratorTelemetryCollectorWorkspaceService.cs index 0d106c168eb18..27422e425657a 100644 --- a/src/Workspaces/Core/Portable/SourceGeneratorTelemetry/ISourceGeneratorTelemetryCollectorWorkspaceService.cs +++ b/src/Workspaces/Core/Portable/SourceGeneratorTelemetry/ISourceGeneratorTelemetryCollectorWorkspaceService.cs @@ -2,11 +2,13 @@ // 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 Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis.SourceGeneratorTelemetry; internal interface ISourceGeneratorTelemetryCollectorWorkspaceService : IWorkspaceService { - void CollectRunResult(GeneratorDriverRunResult driverRunResult, GeneratorDriverTimingInfo driverTimingInfo, ProjectState project); + void CollectRunResult(GeneratorDriverRunResult driverRunResult, GeneratorDriverTimingInfo driverTimingInfo, Func getAnalyzerReference); } diff --git a/src/Workspaces/Core/Portable/SourceGeneratorTelemetry/SourceGeneratorTelemetryCollectorWorkspaceService.cs b/src/Workspaces/Core/Portable/SourceGeneratorTelemetry/SourceGeneratorTelemetryCollectorWorkspaceService.cs index 4414a2e4cfd44..48fa40db7307f 100644 --- a/src/Workspaces/Core/Portable/SourceGeneratorTelemetry/SourceGeneratorTelemetryCollectorWorkspaceService.cs +++ b/src/Workspaces/Core/Portable/SourceGeneratorTelemetry/SourceGeneratorTelemetryCollectorWorkspaceService.cs @@ -42,19 +42,22 @@ public GeneratorTelemetryKey(ISourceGenerator generator, AnalyzerReference analy private readonly StatisticLogAggregator _elapsedTimeByGenerator = new(); private readonly StatisticLogAggregator _producedFilesByGenerator = new(); - private GeneratorTelemetryKey GetTelemetryKey(ISourceGenerator generator, ProjectState project) - => _generatorTelemetryKeys.GetValue(generator, g => new GeneratorTelemetryKey(g, project.GetAnalyzerReferenceForGenerator(g))); + private GeneratorTelemetryKey GetTelemetryKey(ISourceGenerator generator, Func getAnalyzerReference) + => _generatorTelemetryKeys.GetValue(generator, g => new GeneratorTelemetryKey(g, getAnalyzerReference(g))); - public void CollectRunResult(GeneratorDriverRunResult driverRunResult, GeneratorDriverTimingInfo driverTimingInfo, ProjectState project) + public void CollectRunResult( + GeneratorDriverRunResult driverRunResult, + GeneratorDriverTimingInfo driverTimingInfo, + Func getAnalyzerReference) { foreach (var generatorTime in driverTimingInfo.GeneratorTimes) { - _elapsedTimeByGenerator.AddDataPoint(GetTelemetryKey(generatorTime.Generator, project), generatorTime.ElapsedTime); + _elapsedTimeByGenerator.AddDataPoint(GetTelemetryKey(generatorTime.Generator, getAnalyzerReference), generatorTime.ElapsedTime); } foreach (var generatorResult in driverRunResult.Results) { - _producedFilesByGenerator.AddDataPoint(GetTelemetryKey(generatorResult.Generator, project), generatorResult.GeneratedSources.Length); + _producedFilesByGenerator.AddDataPoint(GetTelemetryKey(generatorResult.Generator, getAnalyzerReference), generatorResult.GeneratedSources.Length); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs index 6c524abf9010b..d69f69a9b5f3b 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs @@ -422,7 +422,7 @@ async Task CollapseInProgressStateAsync(InProgressState initial // Also transform the compilation that has generated files; we won't do that though if the transformation either would cause problems with // the generated documents, or if don't have any source generators in the first place. if (translationAction.CanUpdateCompilationWithStaleGeneratedTreesIfGeneratorsGiveSameOutput && - translationAction.OldProjectState.SourceGenerators.Any()) + GetSourceGenerators(translationAction.OldProjectState).Any()) { staleCompilationWithGeneratedDocuments = await translationAction.TransformCompilationAsync(staleCompilationWithGeneratedDocuments, cancellationToken).ConfigureAwait(false); } @@ -774,7 +774,7 @@ public async ValueTask> GetSour SolutionCompilationState compilationState, CancellationToken cancellationToken) { // If we don't have any generators, then we know we have no generated files, so we can skip the computation entirely. - if (!this.ProjectState.SourceGenerators.Any()) + if (!GetSourceGenerators(this.ProjectState).Any()) { return TextDocumentStates.Empty; } @@ -787,7 +787,7 @@ public async ValueTask> GetSour public async ValueTask> GetSourceGeneratorDiagnosticsAsync( SolutionCompilationState compilationState, CancellationToken cancellationToken) { - if (!this.ProjectState.SourceGenerators.Any()) + if (!GetSourceGenerators(this.ProjectState).Any()) { return []; } @@ -816,7 +816,7 @@ public async ValueTask> GetSourceGeneratorDiagnostics public async ValueTask GetSourceGeneratorRunResultAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken) { - if (!this.ProjectState.SourceGenerators.Any()) + if (!GetSourceGenerators(this.ProjectState).Any()) { return null; } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker_Generators.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker_Generators.cs index c281f43cda13c..7d6c4cbd90ddf 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker_Generators.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker_Generators.cs @@ -233,7 +233,7 @@ await newGeneratedDocuments.States.Values.SelectAsArrayAsync( // the "InCurrentProcess" call so that it will normally run only in the OOP process, thus ensuring that we // get accurate information about what SourceGenerators we actually have (say, in case they they are rebuilt // by the user while VS is running). - if (!this.ProjectState.SourceGenerators.Any()) + if (!GetSourceGenerators(this.ProjectState).Any()) return (compilationWithoutGeneratedFiles, TextDocumentStates.Empty, generatorDriver); // If we don't already have an existing generator driver, create one from scratch @@ -268,7 +268,9 @@ await newGeneratedDocuments.States.Values.SelectAsArrayAsync( var runResult = generatorDriver.GetRunResult(); - telemetryCollector?.CollectRunResult(runResult, generatorDriver.GetTimingInfo(), ProjectState); + telemetryCollector?.CollectRunResult( + runResult, generatorDriver.GetTimingInfo(), + g => GetAnalyzerReference(this.ProjectState, g)); // We may be able to reuse compilationWithStaleGeneratedTrees if the generated trees are identical. We will assign null // to compilationWithStaleGeneratedTrees if we at any point realize it can't be used. We'll first check the count of trees @@ -293,7 +295,7 @@ await newGeneratedDocuments.States.Values.SelectAsArrayAsync( if (IsGeneratorRunResultToIgnore(generatorResult)) continue; - var generatorAnalyzerReference = this.ProjectState.GetAnalyzerReferenceForGenerator(generatorResult.Generator); + var generatorAnalyzerReference = GetAnalyzerReference(this.ProjectState, generatorResult.Generator); foreach (var generatedSource in generatorResult.GeneratedSources) { @@ -394,7 +396,7 @@ static GeneratorDriver CreateGeneratorDriver(ProjectState projectState) return compilationFactory.CreateGeneratorDriver( projectState.ParseOptions!, - projectState.SourceGenerators.ToImmutableArray(), + GetSourceGenerators(projectState), projectState.AnalyzerOptions.AnalyzerConfigOptionsProvider, additionalTexts); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs index 9f15aed0ffec4..30961e91913ed 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.TranslationAction_Actions.cs @@ -355,7 +355,7 @@ public override GeneratorDriver TransformGeneratorDriver(GeneratorDriver _) .ReplaceAdditionalTexts(this.NewProjectState.AdditionalDocumentStates.SelectAsArray(static documentState => documentState.AdditionalText)) .WithUpdatedParseOptions(this.NewProjectState.ParseOptions!) .WithUpdatedAnalyzerConfigOptions(this.NewProjectState.AnalyzerOptions.AnalyzerConfigOptionsProvider) - .ReplaceGenerators(this.NewProjectState.SourceGenerators.ToImmutableArray()); + .ReplaceGenerators(GetSourceGenerators(this.NewProjectState)); return generatorDriver; } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs index 53a3039e81fbb..c09183d3e661b 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState_SourceGenerators.cs @@ -53,13 +53,25 @@ private static ImmutableArray GetSourceGenerators(ProjectState => GetSourceGenerators(projectState.Language, projectState.AnalyzerReferences); private static ImmutableArray GetSourceGenerators(string language, IReadOnlyList analyzerReferences) + { + var map = GetSourceGeneratorMap(language, analyzerReferences); + return map is null ? [] : map.SourceGenerators; + } + + private static AnalyzerReference GetAnalyzerReference(ProjectState projectState, ISourceGenerator sourceGenerator) + { + var map = GetSourceGeneratorMap(projectState.Language, projectState.AnalyzerReferences); + Contract.ThrowIfNull(map); + return map.SourceGeneratorToAnalyzerReference[sourceGenerator]; + } + + private static SourceGeneratorMap? GetSourceGeneratorMap(string language, IReadOnlyList analyzerReferences) { var tupleOpt = s_languageToAnalyzerReferencesToSourceGeneratorsMap.FirstOrNull(static (t, language) => t.language == language, language); if (tupleOpt is null) - return []; + return null; var tuple = tupleOpt.Value; - var map = tuple.referencesToGenerators.GetValue(analyzerReferences, tuple.callback); - return map.SourceGenerators; + return tuple.referencesToGenerators.GetValue(analyzerReferences, tuple.callback); } }