From 3286961b4ce9d88f709fad8a205914912272ba36 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 16 Jul 2021 15:32:04 -0700 Subject: [PATCH] Improve caching in RazorSourceGenerator * Cache tag helpers when the declaration build of razor files does not change * Cache generated output invariant of other non _Import files --- .../IncrementalValueProviderExtensions.cs | 22 +- .../RazorSourceGenerationOptions.cs | 20 +- .../RazorSourceGenerator.Helpers.cs | 51 ++--- .../RazorSourceGenerator.RazorProviders.cs | 4 +- .../RazorSourceGenerator.TagHelpers.cs | 4 +- .../SourceGenerators/RazorSourceGenerator.cs | 189 ++++++++++-------- .../StaticCompilationTagHelperFeature.cs | 11 +- 7 files changed, 172 insertions(+), 129 deletions(-) diff --git a/src/RazorSdk/SourceGenerators/IncrementalValueProviderExtensions.cs b/src/RazorSdk/SourceGenerators/IncrementalValueProviderExtensions.cs index ad98180a6d95..cae068838237 100644 --- a/src/RazorSdk/SourceGenerators/IncrementalValueProviderExtensions.cs +++ b/src/RazorSdk/SourceGenerators/IncrementalValueProviderExtensions.cs @@ -10,15 +10,9 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators { internal static class IncrementalValuesProviderExtensions { - internal static IncrementalValuesProvider WithLambdaComparer(this IncrementalValuesProvider source, Func equal) + internal static IncrementalValueProvider WithLambdaComparer(this IncrementalValueProvider source, Func equal, Func getHashCode) { - var comparer = new LambdaComparer(equal); - return source.WithComparer(comparer); - } - - internal static IncrementalValueProvider WithLambdaComparer(this IncrementalValueProvider source, Func equal) - { - var comparer = new LambdaComparer(equal); + var comparer = new LambdaComparer(equal, getHashCode); return source.WithComparer(comparer); } @@ -53,15 +47,17 @@ internal static IncrementalValueProvider ReportDiagnostics(thi internal class LambdaComparer : IEqualityComparer { - private readonly Func _equal; + private readonly Func _equal; + private readonly Func _getHashCode; - public LambdaComparer(Func equal) + public LambdaComparer(Func equal, Func getHashCode) { _equal = equal; + _getHashCode = getHashCode; } - public bool Equals(T? x, T? y) => _equal(x, y); + public bool Equals(T x, T y) => _equal(x, y); - public int GetHashCode(T obj) => EqualityComparer.Default.GetHashCode(obj); + public int GetHashCode(T obj) => _getHashCode(obj); } -} \ No newline at end of file +} diff --git a/src/RazorSdk/SourceGenerators/RazorSourceGenerationOptions.cs b/src/RazorSdk/SourceGenerators/RazorSourceGenerationOptions.cs index a77bc238cabf..bcfc96da4148 100644 --- a/src/RazorSdk/SourceGenerators/RazorSourceGenerationOptions.cs +++ b/src/RazorSdk/SourceGenerators/RazorSourceGenerationOptions.cs @@ -2,12 +2,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.CSharp; namespace Microsoft.NET.Sdk.Razor.SourceGenerators { - internal class RazorSourceGenerationOptions + internal class RazorSourceGenerationOptions : IEquatable { public string RootNamespace { get; set; } = "ASP"; @@ -40,6 +41,19 @@ internal class RazorSourceGenerationOptions /// /// Gets the CSharp language version currently used by the compilation. /// - public LanguageVersion CSharpLanguageVersion { get; set; } = LanguageVersion.Preview; + public LanguageVersion CSharpLanguageVersion { get; set; } = LanguageVersion.CSharp10; + + public bool Equals(RazorSourceGenerationOptions other) + { + return RootNamespace == other.RootNamespace && + Configuration == other.Configuration && + GenerateMetadataSourceChecksumAttributes == other.GenerateMetadataSourceChecksumAttributes && + SuppressRazorSourceGenerator == other.SuppressRazorSourceGenerator && + CSharpLanguageVersion == other.CSharpLanguageVersion; + } + + public override bool Equals(object obj) => obj is RazorSourceGenerationOptions other && Equals(other); + + public override int GetHashCode() => Configuration.GetHashCode(); } -} \ No newline at end of file +} diff --git a/src/RazorSdk/SourceGenerators/RazorSourceGenerator.Helpers.cs b/src/RazorSdk/SourceGenerators/RazorSourceGenerator.Helpers.cs index 2654fe7f5c0d..c5b3b4e0426f 100644 --- a/src/RazorSdk/SourceGenerators/RazorSourceGenerator.Helpers.cs +++ b/src/RazorSdk/SourceGenerators/RazorSourceGenerator.Helpers.cs @@ -2,14 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Text; -using System.Threading; using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Razor; namespace Microsoft.NET.Sdk.Razor.SourceGenerators @@ -37,7 +33,9 @@ private static string GetIdentifierFromPath(string filePath) return builder.ToString(); } - private static RazorProjectEngine GetDiscoveryProjectEngine(StaticCompilationTagHelperFeature tagHelperFeature, IEnumerable references, IEnumerable items, RazorSourceGenerationOptions razorSourceGeneratorOptions) + private static RazorProjectEngine GetDeclarationProjectEngine( + IEnumerable items, + RazorSourceGenerationOptions razorSourceGeneratorOptions) { var fileSystem = new VirtualRazorProjectFileSystem(); foreach (var item in items) @@ -56,26 +54,43 @@ private static RazorProjectEngine GetDiscoveryProjectEngine(StaticCompilationTag b.SetRootNamespace(razorSourceGeneratorOptions.RootNamespace); - b.Features.Add(new DefaultMetadataReferenceFeature { References = references.ToList() }); + CompilerFeatures.Register(b); + RazorExtensions.Register(b); + + b.SetCSharpLanguageVersion(razorSourceGeneratorOptions.CSharpLanguageVersion); + }); + return discoveryProjectEngine; + } + + private static RazorProjectEngine GetDiscoveryProjectEngine( + IReadOnlyList references, + StaticCompilationTagHelperFeature tagHelperFeature) + { + var discoveryProjectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, new VirtualRazorProjectFileSystem(), b => + { + b.Features.Add(new DefaultMetadataReferenceFeature { References = references }); b.Features.Add(tagHelperFeature); b.Features.Add(new DefaultTagHelperDescriptorProvider()); CompilerFeatures.Register(b); RazorExtensions.Register(b); - - b.SetCSharpLanguageVersion(razorSourceGeneratorOptions.CSharpLanguageVersion); }); return discoveryProjectEngine; } - private static RazorProjectEngine GetGenerationProjectEngine(IReadOnlyList tagHelpers, IEnumerable items, RazorSourceGenerationOptions razorSourceGeneratorOptions) + private static RazorProjectEngine GetGenerationProjectEngine( + IReadOnlyList tagHelpers, + SourceGeneratorProjectItem item, + IEnumerable imports, + RazorSourceGenerationOptions razorSourceGeneratorOptions) { var fileSystem = new VirtualRazorProjectFileSystem(); - foreach (var item in items) + fileSystem.Add(item); + foreach (var import in imports) { - fileSystem.Add(item); + fileSystem.Add(import); } var projectEngine = RazorProjectEngine.Create(razorSourceGeneratorOptions.Configuration, fileSystem, b => @@ -99,19 +114,5 @@ private static RazorProjectEngine GetGenerationProjectEngine(IReadOnlyList(RazorProjectEngine engine) - { - var count = engine.EngineFeatures.Count; - for (var i = 0; i < count; i++) - { - if (engine.EngineFeatures[i] is TFeature feature) - { - return feature; - } - } - - return default; - } } } diff --git a/src/RazorSdk/SourceGenerators/RazorSourceGenerator.RazorProviders.cs b/src/RazorSdk/SourceGenerators/RazorSourceGenerator.RazorProviders.cs index f5d868a5af9d..db652b9925a1 100644 --- a/src/RazorSdk/SourceGenerators/RazorSourceGenerator.RazorProviders.cs +++ b/src/RazorSdk/SourceGenerators/RazorSourceGenerator.RazorProviders.cs @@ -40,11 +40,11 @@ private static (RazorSourceGenerationOptions?, Diagnostic?) ComputeRazorSourceGe var razorSourceGenerationOptions = new RazorSourceGenerationOptions() { + Configuration = razorConfiguration, WaitForDebugger = waitForDebugger == "true", SuppressRazorSourceGenerator = suppressRazorSourceGenerator == "true", GenerateMetadataSourceChecksumAttributes = generateMetadataSourceChecksumAttributes == "true", RootNamespace = rootNamespace ?? "ASP", - Configuration = razorConfiguration, CSharpLanguageVersion = ((CSharpParseOptions)parseOptions).LanguageVersion, }; @@ -80,4 +80,4 @@ private static (SourceGeneratorProjectItem?, Diagnostic?) ComputeProjectItems((A return (projectItem, null); } } -} \ No newline at end of file +} diff --git a/src/RazorSdk/SourceGenerators/RazorSourceGenerator.TagHelpers.cs b/src/RazorSdk/SourceGenerators/RazorSourceGenerator.TagHelpers.cs index 0324f861abb9..845b4136471e 100644 --- a/src/RazorSdk/SourceGenerators/RazorSourceGenerator.TagHelpers.cs +++ b/src/RazorSdk/SourceGenerators/RazorSourceGenerator.TagHelpers.cs @@ -24,7 +24,7 @@ private IReadOnlyList GetTagHelpers(IEnumerable GetTagHelpersFromCompilation(Compilation compilation, StaticCompilationTagHelperFeature tagHelperFeature, SyntaxTree syntaxTrees) + private static IReadOnlyList GetTagHelpersFromCompilation(Compilation compilation, StaticCompilationTagHelperFeature tagHelperFeature, SyntaxTree syntaxTrees) { var compilationWithDeclarations = compilation.AddSyntaxTrees(syntaxTrees); @@ -34,4 +34,4 @@ private IReadOnlyList GetTagHelpersFromCompilation(Compilat return tagHelperFeature.GetDescriptors(); } } -} \ No newline at end of file +} diff --git a/src/RazorSdk/SourceGenerators/RazorSourceGenerator.cs b/src/RazorSdk/SourceGenerators/RazorSourceGenerator.cs index 9b1e15a14ccd..482c97a71b91 100644 --- a/src/RazorSdk/SourceGenerators/RazorSourceGenerator.cs +++ b/src/RazorSdk/SourceGenerators/RazorSourceGenerator.cs @@ -1,8 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; using System.Linq; +using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -20,114 +24,141 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var razorSourceGeneratorOptions = razorSourceGeneratorOptionsWithDiagnostics.ReportDiagnostics(context); var sourceItemsWithDiagnostics = context.AdditionalTextsProvider + .Where(static (file) => file.Path.EndsWith(".razor") || file.Path.EndsWith(".cshtml")) .Combine(context.AnalyzerConfigOptionsProvider) - .Where((pair) => pair.Item1.Path.EndsWith(".razor") || pair.Item1.Path.EndsWith(".cshtml")) .Select(ComputeProjectItems); - var sourceItems = sourceItemsWithDiagnostics.ReportDiagnostics(context); + var sourceItems = sourceItemsWithDiagnostics + .ReportDiagnostics(context); - var references = context.CompilationProvider - .WithLambdaComparer( - (c1, c2) => c1 != null && c2 != null && c1.References != c2.References) - .Select((compilation, _) => compilation.References); + var importFiles = sourceItems.Where(static file => + { + var path = file.FilePath; + if (path.EndsWith(".razor")) + { + return Path.GetFileNameWithoutExtension(path) == "_Imports"; + } + else if (path.EndsWith(".cshtml")) + { + return Path.GetFileNameWithoutExtension(path) == "_ViewImports"; + } - var sourceItemsByName = sourceItems.Collect() - .WithLambdaComparer((@new, old) => @new.SequenceEqual(old, new LambdaComparer( - (l, r) => string.Equals(l?.FilePath, r?.FilePath, System.StringComparison.OrdinalIgnoreCase)))); + return false; + }); - var discoveryProjectEngine = references + var generatedCode = razorSourceGeneratorOptions + .Combine(sourceItems.Collect()) + .Select(static (pair, _) => + { + var (razorSourceGeneratorOptions, sourceItems) = pair; + var projectEngine = GetDeclarationProjectEngine(sourceItems, razorSourceGeneratorOptions); + + var generatedCode = ImmutableArray.CreateBuilder(sourceItems.Length); + foreach (var file in sourceItems) + { + var codeGen = projectEngine.Process(file); + generatedCode.Add(codeGen.GetCSharpDocument().GeneratedCode); + } + + return generatedCode.ToImmutable(); + }) + .WithLambdaComparer(static (a, b) => a.SequenceEqual(b, StringComparer.Ordinal), static a => a.Length); + + var tagHelpersFromCompilation = context.CompilationProvider + .Combine(generatedCode) + .Combine(context.ParseOptionsProvider) .Combine(razorSourceGeneratorOptions) - .Combine(sourceItemsByName) - .Select((pair, _) => + .Select(static (pair, _) => { - var ((references, razorSourceGeneratorOptions), projectItems) = pair; + var (((compilation, generatedCode), parseOptions), razorSourceGeneratorOptions) = pair; + var syntaxTree = generatedCode.Select(c => CSharpSyntaxTree.ParseText(c, (CSharpParseOptions)parseOptions)); + var tagHelperFeature = new StaticCompilationTagHelperFeature(); - return GetDiscoveryProjectEngine(tagHelperFeature, references, projectItems, razorSourceGeneratorOptions); - }); + var discoveryProjectEngine = GetDiscoveryProjectEngine(compilation.References.ToImmutableArray(), tagHelperFeature); - var syntaxTrees = sourceItems - .Combine(discoveryProjectEngine) - .Combine(context.ParseOptionsProvider) - .Select((pair, _) => - { - var (itemAndDiscoveryEngine, parseOptions) = pair; - var (item, discoveryProjectEngine) = itemAndDiscoveryEngine; + var syntaxTrees = generatedCode.Select(c => CSharpSyntaxTree.ParseText(c, (CSharpParseOptions)parseOptions)); - var codeGen = discoveryProjectEngine.Process(item); - var generatedCode = codeGen.GetCSharpDocument().GeneratedCode; - return CSharpSyntaxTree.ParseText(generatedCode, (CSharpParseOptions)parseOptions); - }); + var compilationWithDeclarations = compilation.AddSyntaxTrees(syntaxTrees); - var tagHelpersFromCompilation = syntaxTrees - .Combine(context.CompilationProvider) - .Combine(discoveryProjectEngine) - .SelectMany((pair, _) => - { - var ((syntaxTrees, compilation), discoveryProjectEngine) = pair; - var tagHelperFeature = GetFeature(discoveryProjectEngine); - return GetTagHelpersFromCompilation( - compilation, - tagHelperFeature!, - syntaxTrees - ); + tagHelperFeature.Compilation = compilationWithDeclarations; + tagHelperFeature.TargetAssembly = compilationWithDeclarations.Assembly; + + return tagHelperFeature.GetDescriptors(); }); - var tagHelpersFromReferences = discoveryProjectEngine - .Combine(context.CompilationProvider) - .Combine(references) - .Select((pair, _) => + var tagHelpersFromReferences = context.CompilationProvider + .WithLambdaComparer(static (a, b) => a.References.SequenceEqual(b.References), static a => a.References.GetHashCode()) + .Select(static (compilation, _) => { - var (engineAndCompilation, references) = pair; - var (discoveryProjectEngine, compilation) = engineAndCompilation; - var tagHelperFeature = GetFeature(discoveryProjectEngine); - return GetTagHelpers( - references, - tagHelperFeature!, - compilation - ); + var tagHelperFeature = new StaticCompilationTagHelperFeature(); + var discoveryProjectEngine = GetDiscoveryProjectEngine(compilation.References.ToImmutableArray(), tagHelperFeature); + + List descriptors = new(); + tagHelperFeature.Compilation = compilation; + foreach (var reference in compilation.References) + { + if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) + { + tagHelperFeature.TargetAssembly = assembly; + descriptors.AddRange(tagHelperFeature.GetDescriptors()); + } + } + return descriptors; }); - var tagHelpers = tagHelpersFromCompilation.Collect().Combine(tagHelpersFromReferences); - - var generationProjectEngine = tagHelpers.Combine(razorSourceGeneratorOptions).Combine(sourceItems.Collect()) - .Select((pair, _) => + var allTagHelpers = tagHelpersFromCompilation + .Combine(tagHelpersFromReferences) + .Select(static (pair, _) => { - var (tagHelpersAndOptions, items) = pair; - var (tagHelpers, razorSourceGeneratorOptions) = tagHelpersAndOptions; - var (tagHelpersFromCompilation, tagHelpersFromReferences) = tagHelpers; - var tagHelpersCount = tagHelpersFromCompilation.Count() + tagHelpersFromReferences.Count; - var allTagHelpers = new List(tagHelpersCount); - allTagHelpers.AddRange(tagHelpersFromCompilation); - allTagHelpers.AddRange(tagHelpersFromReferences); - - return GetGenerationProjectEngine(allTagHelpers, items, razorSourceGeneratorOptions); + var (tagHelpersFromCompilation, tagHelpersFromReferences) = pair; + var allTagHelpers = new TagHelperDescriptor[tagHelpersFromCompilation.Count + tagHelpersFromReferences.Count]; + tagHelpersFromCompilation.CopyTo(allTagHelpers); + tagHelpersFromReferences.CopyTo(allTagHelpers, tagHelpersFromCompilation.Count); + + return allTagHelpers; }); - var generationInputs = sourceItems + var generatedOutput = sourceItems + .Combine(importFiles.Collect()) + .Combine(allTagHelpers) .Combine(razorSourceGeneratorOptions) - .Combine(generationProjectEngine); + .Combine(context.ParseOptionsProvider) + .Select(static (pair, _) => + { + var ((((sourceItem, imports), allTagHelpers), razorSourceGeneratorOptions), parserOptions) = pair; + if (razorSourceGeneratorOptions.SuppressRazorSourceGenerator) + { + return default; + } - context.RegisterSourceOutput(generationInputs, (context, pair) => - { - var (sourceItemsAndOptions, projectEngine) = pair; - var (projectItem, razorSourceGeneratorOptions) = sourceItemsAndOptions; + // Add a generated suffix so tools, such as coverlet, consider the file to be generated + var hintName = GetIdentifierFromPath(sourceItem.RelativePhysicalPath) + ".g.cs"; + + var projectEngine = GetGenerationProjectEngine(allTagHelpers, sourceItem, imports, razorSourceGeneratorOptions); + + var codeDocument = projectEngine.Process(sourceItem); + var csharpDocument = codeDocument.GetCSharpDocument(); - var codeDocument = projectEngine.Process(projectItem); - var csharpDocument = codeDocument.GetCSharpDocument(); + return (hintName, csharpDocument); + }); - for (var j = 0; j < csharpDocument.Diagnostics.Count; j++) + context.RegisterSourceOutput(generatedOutput, static (context, pair) => + { + var (hintName, csharpDocument) = pair; + if (hintName is null) { - var razorDiagnostic = csharpDocument.Diagnostics[j]; - var csharpDiagnostic = razorDiagnostic.AsDiagnostic(); - context.ReportDiagnostic(csharpDiagnostic); + // Source generator is suppressed. + return; } - if (!razorSourceGeneratorOptions.SuppressRazorSourceGenerator) + for (var i = 0; i < csharpDocument.Diagnostics.Count; i++) { - // Add a generated suffix so tools, such as coverlet, consider the file to be generated - var hintName = GetIdentifierFromPath(projectItem.RelativePhysicalPath) + ".g.cs"; - context.AddSource(hintName, csharpDocument.GeneratedCode); + var razorDiagnostic = csharpDocument.Diagnostics[i]; + var csharpDiagnostic = razorDiagnostic.AsDiagnostic(); + context.ReportDiagnostic(csharpDiagnostic); } + + context.AddSource(hintName, csharpDocument.GeneratedCode); }); } } diff --git a/src/RazorSdk/SourceGenerators/StaticCompilationTagHelperFeature.cs b/src/RazorSdk/SourceGenerators/StaticCompilationTagHelperFeature.cs index 5a6ee1a29e06..ab1d595ac1ac 100644 --- a/src/RazorSdk/SourceGenerators/StaticCompilationTagHelperFeature.cs +++ b/src/RazorSdk/SourceGenerators/StaticCompilationTagHelperFeature.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Razor.Language; @@ -12,18 +11,18 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators { internal sealed class StaticCompilationTagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature { + private static readonly List EmptyList = new(); + private ITagHelperDescriptorProvider[]? _providers; - public IReadOnlyList GetDescriptors() + public List GetDescriptors() { if (Compilation is null) { - return Array.Empty(); + return EmptyList; } - var results = new List(); - var context = TagHelperDescriptorProviderContext.Create(results); context.SetCompilation(Compilation); context.Items.SetTargetAssembly(TargetAssembly!); @@ -36,6 +35,8 @@ public IReadOnlyList GetDescriptors() return results; } + IReadOnlyList ITagHelperFeature.GetDescriptors() => GetDescriptors(); + public Compilation? Compilation { get; set; } public IAssemblySymbol? TargetAssembly { get; set; }