Skip to content
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

Move source-generators out of ProjectState and entirely into SolutinoCompilationState #72834

Merged
merged 5 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<ISourceGenerator, AnalyzerReference> getAnalyzerReference);
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,22 @@ public GeneratorTelemetryKey(ISourceGenerator generator, AnalyzerReference analy
private readonly StatisticLogAggregator<GeneratorTelemetryKey> _elapsedTimeByGenerator = new();
private readonly StatisticLogAggregator<GeneratorTelemetryKey> _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<ISourceGenerator, AnalyzerReference> 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<ISourceGenerator, AnalyzerReference> 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);
}
}

Expand Down
37 changes: 0 additions & 37 deletions src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ internal partial class ProjectState

private AnalyzerOptions? _lazyAnalyzerOptions;

/// <summary>
/// The list of source generators and the analyzer reference they came from.
/// </summary>
private ImmutableDictionary<ISourceGenerator, AnalyzerReference>? _lazySourceGenerators;

private ProjectState(
ProjectInfo projectInfo,
LanguageServices languageServices,
Expand Down Expand Up @@ -737,38 +732,6 @@ public ProjectState WithAnalyzerReferences(IEnumerable<AnalyzerReference> analyz
return With(projectInfo: ProjectInfo.WithAnalyzerReferences(analyzerReferences).WithVersion(Version.GetNewerVersion()));
}

[MemberNotNull(nameof(_lazySourceGenerators))]
private void EnsureSourceGeneratorsInitialized()
{
if (_lazySourceGenerators == null)
{
var builder = ImmutableDictionary.CreateBuilder<ISourceGenerator, AnalyzerReference>();

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<ISourceGenerator> SourceGenerators
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was too broad a surface area. we def did not want to expose inside the workspace that you could get to ISourceGenerator instances.

{
get
{
EnsureSourceGeneratorsInitialized();
return _lazySourceGenerators.Keys;
}
}

public AnalyzerReference GetAnalyzerReferenceForGenerator(ISourceGenerator generator)
{
EnsureSourceGeneratorsInitialized();
return _lazySourceGenerators[generator];
}

public ProjectState AddDocuments(ImmutableArray<DocumentState> documents)
{
if (documents.IsEmpty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ async Task<InProgressState> 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())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this needs to not instantiate generators, but instead call into OOP to determine this info.

Copy link
Member

@sharwell sharwell Apr 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering that the SDK delivers source generators that are enabled by default, it might make sense to just assume the .Any() is true and focus on optimizing the specific case where none of the source generators adds any documents to the compilation.

{
staleCompilationWithGeneratedDocuments = await translationAction.TransformCompilationAsync(staleCompilationWithGeneratedDocuments, cancellationToken).ConfigureAwait(false);
}
Expand Down Expand Up @@ -774,7 +774,7 @@ public async ValueTask<TextDocumentStates<SourceGeneratedDocumentState>> 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())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same.

{
return TextDocumentStates<SourceGeneratedDocumentState>.Empty;
}
Expand All @@ -787,7 +787,7 @@ public async ValueTask<TextDocumentStates<SourceGeneratedDocumentState>> GetSour
public async ValueTask<ImmutableArray<Diagnostic>> GetSourceGeneratorDiagnosticsAsync(
SolutionCompilationState compilationState, CancellationToken cancellationToken)
{
if (!this.ProjectState.SourceGenerators.Any())
if (!GetSourceGenerators(this.ProjectState).Any())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

{
return [];
}
Expand Down Expand Up @@ -816,7 +816,7 @@ public async ValueTask<ImmutableArray<Diagnostic>> GetSourceGeneratorDiagnostics

public async ValueTask<GeneratorDriverRunResult?> GetSourceGeneratorRunResultAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken)
{
if (!this.ProjectState.SourceGenerators.Any())
if (!GetSourceGenerators(this.ProjectState).Any())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same.

{
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this call is fine. We're already in OOP.

return (compilationWithoutGeneratedFiles, TextDocumentStates<SourceGeneratedDocumentState>.Empty, generatorDriver);

// If we don't already have an existing generator driver, create one from scratch
Expand Down Expand Up @@ -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
Expand All @@ -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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this call is fine. We're already in OOP.


foreach (var generatedSource in generatorResult.GeneratedSources)
{
Expand Down Expand Up @@ -394,7 +396,7 @@ static GeneratorDriver CreateGeneratorDriver(ProjectState projectState)

return compilationFactory.CreateGeneratorDriver(
projectState.ParseOptions!,
projectState.SourceGenerators.ToImmutableArray(),
GetSourceGenerators(projectState),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this call is fine. We're already in OOP.

projectState.AnalyzerOptions.AnalyzerConfigOptionsProvider,
additionalTexts);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this need to not happen on the VS side.


return generatorDriver;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis;

using AnalyzerReferencesToSourceGenerators = ConditionalWeakTable<IReadOnlyList<AnalyzerReference>, SolutionCompilationState.SourceGeneratorMap>;

internal partial class SolutionCompilationState
{
internal sealed record SourceGeneratorMap(
ImmutableArray<ISourceGenerator> SourceGenerators,
ImmutableDictionary<ISourceGenerator, AnalyzerReference> SourceGeneratorToAnalyzerReference);
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Cached mapping from language (only C#/VB since those are the only languages that support analyzers) to the lists
/// of analyzer references (see <see cref="ProjectState.AnalyzerReferences"/>) to all the <see
/// cref="ISourceGenerator"/>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).
/// </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>();

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<ISourceGenerator> GetSourceGenerators(ProjectState projectState)
=> GetSourceGenerators(projectState.Language, projectState.AnalyzerReferences);

private static ImmutableArray<ISourceGenerator> GetSourceGenerators(string language, IReadOnlyList<AnalyzerReference> 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<AnalyzerReference> analyzerReferences)
{
var tupleOpt = s_languageToAnalyzerReferencesToSourceGeneratorsMap.FirstOrNull(static (t, language) => t.language == language, language);
if (tupleOpt is null)
return null;

var tuple = tupleOpt.Value;
return tuple.referencesToGenerators.GetValue(analyzerReferences, tuple.callback);
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading