Skip to content

Commit 4ab512e

Browse files
authored
Perf/generator (#8212)
* Don't re-run declaration code if nothing has changed * Split tag helper discovery into component and compilation discovery * Create a wrapper source generator engine that can selectively run phases * Ensure we don't re-use a rewritten syntax tree * Break output into multiple steps that can be largely skipped as needed - Update event log names - Update tests to match
1 parent 9a6dcf5 commit 4ab512e

12 files changed

+597
-404
lines changed

src/Compiler/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperContextDiscoveryPhase.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal sealed class DefaultRazorTagHelperContextDiscoveryPhase : RazorEnginePh
1717
{
1818
protected override void ExecuteCore(RazorCodeDocument codeDocument)
1919
{
20-
var syntaxTree = codeDocument.GetSyntaxTree();
20+
var syntaxTree = codeDocument.GetPreTagHelperSyntaxTree() ?? codeDocument.GetSyntaxTree();
2121
ThrowForMissingDocumentDependency(syntaxTree);
2222

2323
var descriptors = codeDocument.GetTagHelpers();
@@ -69,7 +69,6 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument)
6969

7070
var context = TagHelperDocumentContext.Create(tagHelperPrefix, descriptors);
7171
codeDocument.SetTagHelperContext(context);
72-
codeDocument.SetPreTagHelperSyntaxTree(syntaxTree);
7372
}
7473

7574
private static bool MatchesDirective(TagHelperDescriptor descriptor, string typePattern, string assemblyName)

src/Compiler/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperRewritePhase.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,27 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
#nullable enable
44

5+
using System.Collections.Generic;
56
using Microsoft.AspNetCore.Razor.Language.Legacy;
67

78
namespace Microsoft.AspNetCore.Razor.Language;
89
internal sealed class DefaultRazorTagHelperRewritePhase : RazorEnginePhaseBase
910
{
1011
protected override void ExecuteCore(RazorCodeDocument codeDocument)
1112
{
12-
var syntaxTree = codeDocument.GetPreTagHelperSyntaxTree();
13+
var syntaxTree = codeDocument.GetPreTagHelperSyntaxTree() ?? codeDocument.GetSyntaxTree();
14+
ThrowForMissingDocumentDependency(syntaxTree);
15+
1316
var context = codeDocument.GetTagHelperContext();
14-
if (syntaxTree is null || context.TagHelpers.Count == 0)
17+
if (context?.TagHelpers.Count > 0)
1518
{
16-
// No descriptors, no-op.
17-
return;
19+
var rewrittenSyntaxTree = TagHelperParseTreeRewriter.Rewrite(syntaxTree, context.Prefix, context.TagHelpers, out var usedHelpers);
20+
codeDocument.SetSyntaxTree(rewrittenSyntaxTree);
21+
codeDocument.SetReferencedTagHelpers(usedHelpers);
22+
}
23+
else
24+
{
25+
codeDocument.SetReferencedTagHelpers(new HashSet<TagHelperDescriptor>());
1826
}
19-
20-
var rewrittenSyntaxTree = TagHelperParseTreeRewriter.Rewrite(syntaxTree, context.Prefix, context.TagHelpers, out var usedHelpers);
21-
22-
codeDocument.SetReferencedTagHelpers(usedHelpers);
23-
codeDocument.SetSyntaxTree(rewrittenSyntaxTree);
2427
}
2528
}

src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/Microsoft.NET.Sdk.Razor.SourceGenerators.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
</ItemGroup>
2020

2121
<ItemGroup>
22+
<ProjectReference Include="..\..\Shared\Microsoft.AspNetCore.Razor.Utilities.Shared\Microsoft.AspNetCore.Razor.Utilities.Shared.csproj" />
2223
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Razor.Extensions\src\Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj" />
2324
<ProjectReference Include="..\Microsoft.AspNetCore.Razor.Language\src\Microsoft.AspNetCore.Razor.Language.csproj" />
2425
<ProjectReference Include="..\Microsoft.CodeAnalysis.Razor\src\Microsoft.CodeAnalysis.Razor.csproj" />

src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGenerator.Helpers.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ private static RazorProjectEngine GetDiscoveryProjectEngine(
8383
return discoveryProjectEngine;
8484
}
8585

86-
private static RazorProjectEngine GetGenerationProjectEngine(
87-
IReadOnlyList<TagHelperDescriptor> tagHelpers,
86+
private static SourceGeneratorProjectEngine GetGenerationProjectEngine(
8887
SourceGeneratorProjectItem item,
8988
IEnumerable<SourceGeneratorProjectItem> imports,
9089
RazorSourceGenerationOptions razorSourceGeneratorOptions)
@@ -96,7 +95,7 @@ private static RazorProjectEngine GetGenerationProjectEngine(
9695
fileSystem.Add(import);
9796
}
9897

99-
var projectEngine = RazorProjectEngine.Create(razorSourceGeneratorOptions.Configuration, fileSystem, b =>
98+
var projectEngine = (DefaultRazorProjectEngine)RazorProjectEngine.Create(razorSourceGeneratorOptions.Configuration, fileSystem, b =>
10099
{
101100
b.Features.Add(new DefaultTypeNameFeature());
102101
b.SetRootNamespace(razorSourceGeneratorOptions.RootNamespace);
@@ -107,16 +106,13 @@ private static RazorProjectEngine GetGenerationProjectEngine(
107106
options.SupportLocalizedComponentNames = razorSourceGeneratorOptions.SupportLocalizedComponentNames;
108107
}));
109108

110-
b.Features.Add(new StaticTagHelperFeature { TagHelpers = tagHelpers });
111-
b.Features.Add(new DefaultTagHelperDescriptorProvider());
112-
113109
CompilerFeatures.Register(b);
114110
RazorExtensions.Register(b);
115111

116112
b.SetCSharpLanguageVersion(razorSourceGeneratorOptions.CSharpLanguageVersion);
117113
});
118114

119-
return projectEngine;
115+
return new SourceGeneratorProjectEngine(projectEngine);
120116
}
121117
}
122118
}

src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators/RazorSourceGenerator.cs

Lines changed: 108 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.Collections.Immutable;
6+
using System.Diagnostics;
77
using System.IO;
88
using System.Linq;
99
using Microsoft.AspNetCore.Razor.Language;
10+
using Microsoft.AspNetCore.Razor.PooledObjects;
1011
using Microsoft.CodeAnalysis;
1112
using Microsoft.CodeAnalysis.CSharp;
13+
using Microsoft.CodeAnalysis.CSharp.Syntax;
1214

1315
namespace Microsoft.NET.Sdk.Razor.SourceGenerators
1416
{
@@ -66,69 +68,82 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6668
var generatedDeclarationCode = componentFiles
6769
.Combine(importFiles.Collect())
6870
.Combine(razorSourceGeneratorOptions)
71+
.WithLambdaComparer((old, @new) => (old.Right.Equals(@new.Right) && old.Left.Left.Equals(@new.Left.Left) && old.Left.Right.SequenceEqual(@new.Left.Right)), (a) => a.GetHashCode())
6972
.Select(static (pair, _) =>
7073
{
7174

7275
var ((sourceItem, importFiles), razorSourceGeneratorOptions) = pair;
73-
RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStart(sourceItem.FilePath);
76+
RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStart(sourceItem.RelativePhysicalPath);
7477

7578
var projectEngine = GetDeclarationProjectEngine(sourceItem, importFiles, razorSourceGeneratorOptions);
7679

7780
var codeGen = projectEngine.Process(sourceItem);
7881

7982
var result = codeGen.GetCSharpDocument().GeneratedCode;
8083

81-
RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStop(sourceItem.FilePath);
84+
RazorSourceGeneratorEventSource.Log.GenerateDeclarationCodeStop(sourceItem.RelativePhysicalPath);
8285

83-
return result;
86+
return (result, sourceItem.RelativePhysicalPath);
8487
});
8588

8689
var generatedDeclarationSyntaxTrees = generatedDeclarationCode
8790
.Combine(parseOptions)
88-
.Select(static (pair, _) =>
91+
.Select(static (pair, ct) =>
8992
{
90-
var (generatedDeclarationCode, parseOptions) = pair;
91-
return CSharpSyntaxTree.ParseText(generatedDeclarationCode, (CSharpParseOptions)parseOptions);
93+
var ((generatedDeclarationCode, filePath), parseOptions) = pair;
94+
return CSharpSyntaxTree.ParseText(generatedDeclarationCode, (CSharpParseOptions)parseOptions, filePath, cancellationToken: ct);
9295
});
9396

94-
var tagHelpersFromCompilation = compilation
95-
.Combine(generatedDeclarationSyntaxTrees.Collect())
97+
var tagHelpersFromComponents = generatedDeclarationSyntaxTrees
98+
.Combine(compilation)
9699
.Combine(razorSourceGeneratorOptions)
97-
.Select(static (pair, _) =>
100+
.SelectMany(static (pair, ct) =>
98101
{
99-
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStart();
100102

101-
var ((compilation, generatedDeclarationSyntaxTrees), razorSourceGeneratorOptions) = pair;
103+
var ((generatedDeclarationSyntaxTree, compilation), razorSourceGeneratorOptions) = pair;
104+
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromComponentStart(generatedDeclarationSyntaxTree.FilePath);
102105

103106
var tagHelperFeature = new StaticCompilationTagHelperFeature();
104107
var discoveryProjectEngine = GetDiscoveryProjectEngine(compilation.References.ToImmutableArray(), tagHelperFeature);
105108

106-
var compilationWithDeclarations = compilation.AddSyntaxTrees(generatedDeclarationSyntaxTrees);
109+
var compilationWithDeclarations = compilation.AddSyntaxTrees(generatedDeclarationSyntaxTree);
110+
111+
// try and find the specific root class this component is declaring, falling back to the assembly if for any reason the code is not in the shape we expect
112+
ISymbol targetSymbol = compilationWithDeclarations.Assembly;
113+
var root = generatedDeclarationSyntaxTree.GetRoot(ct);
114+
if (root is CompilationUnitSyntax { Members: [NamespaceDeclarationSyntax { Members: [ClassDeclarationSyntax classSyntax, ..] }, ..] })
115+
{
116+
var declaredClass = compilationWithDeclarations.GetSemanticModel(generatedDeclarationSyntaxTree).GetDeclaredSymbol(classSyntax, ct);
117+
Debug.Assert(declaredClass is null || declaredClass is { AllInterfaces: [{ Name: "IComponent" }, ..] });
118+
targetSymbol = declaredClass ?? targetSymbol;
119+
}
107120

108121
tagHelperFeature.Compilation = compilationWithDeclarations;
109-
tagHelperFeature.TargetSymbol = compilationWithDeclarations.Assembly;
122+
tagHelperFeature.TargetSymbol = targetSymbol;
110123

111-
var result = (IList<TagHelperDescriptor>)tagHelperFeature.GetDescriptors();
112-
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStop();
124+
var result = tagHelperFeature.GetDescriptors();
125+
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromComponentStop(generatedDeclarationSyntaxTree.FilePath);
113126
return result;
114-
})
115-
.WithLambdaComparer(static (a, b) =>
127+
});
128+
129+
var tagHelpersFromCompilation = compilation
130+
.Combine(razorSourceGeneratorOptions)
131+
.Select(static (pair, _) =>
116132
{
117-
if (a.Count != b.Count)
118-
{
119-
return false;
120-
}
133+
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStart();
121134

122-
for (var i = 0; i < a.Count; i++)
123-
{
124-
if (!a[i].Equals(b[i]))
125-
{
126-
return false;
127-
}
128-
}
135+
var (compilation, razorSourceGeneratorOptions) = pair;
136+
137+
var tagHelperFeature = new StaticCompilationTagHelperFeature();
138+
var discoveryProjectEngine = GetDiscoveryProjectEngine(compilation.References.ToImmutableArray(), tagHelperFeature);
129139

130-
return true;
131-
}, getHashCode: static a => a.Count);
140+
tagHelperFeature.Compilation = compilation;
141+
tagHelperFeature.TargetSymbol = compilation.Assembly;
142+
143+
var result = tagHelperFeature.GetDescriptors();
144+
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromCompilationStop();
145+
return result;
146+
});
132147

133148
var tagHelpersFromReferences = compilation
134149
.Combine(razorSourceGeneratorOptions)
@@ -171,7 +186,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
171186
var tagHelperFeature = new StaticCompilationTagHelperFeature();
172187
var discoveryProjectEngine = GetDiscoveryProjectEngine(compilation.References.ToImmutableArray(), tagHelperFeature);
173188

174-
List<TagHelperDescriptor> descriptors = new();
189+
using var pool = ArrayBuilderPool<TagHelperDescriptor>.GetPooledObject(out var descriptors);
175190
tagHelperFeature.Compilation = compilation;
176191
foreach (var reference in compilation.References)
177192
{
@@ -183,47 +198,84 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
183198
}
184199

185200
RazorSourceGeneratorEventSource.Log.DiscoverTagHelpersFromReferencesStop();
186-
return (ICollection<TagHelperDescriptor>)descriptors;
201+
return descriptors.ToImmutable();
187202
});
188203

189-
var allTagHelpers = tagHelpersFromCompilation
204+
var allTagHelpers = tagHelpersFromComponents.Collect()
205+
.Combine(tagHelpersFromCompilation)
190206
.Combine(tagHelpersFromReferences)
191207
.Select(static (pair, _) =>
192208
{
193-
var (tagHelpersFromCompilation, tagHelpersFromReferences) = pair;
194-
var count = tagHelpersFromCompilation.Count + tagHelpersFromReferences.Count;
209+
var ((tagHelpersFromComponents, tagHelpersFromCompilation), tagHelpersFromReferences) = pair;
210+
var count = tagHelpersFromCompilation.Length + tagHelpersFromReferences.Length + tagHelpersFromComponents.Length;
195211
if (count == 0)
196212
{
197-
return Array.Empty<TagHelperDescriptor>();
213+
return ImmutableArray<TagHelperDescriptor>.Empty;
198214
}
199215

200-
var allTagHelpers = new TagHelperDescriptor[count];
201-
tagHelpersFromCompilation.CopyTo(allTagHelpers, 0);
202-
tagHelpersFromReferences.CopyTo(allTagHelpers, tagHelpersFromCompilation.Count);
216+
using var pool = ArrayBuilderPool<TagHelperDescriptor>.GetPooledObject(out var allTagHelpers);
217+
allTagHelpers.AddRange(tagHelpersFromCompilation);
218+
allTagHelpers.AddRange(tagHelpersFromReferences);
219+
allTagHelpers.AddRange(tagHelpersFromComponents);
203220

204-
return allTagHelpers;
221+
return allTagHelpers.ToImmutable();
205222
});
206223

207224
var generatedOutput = sourceItems
208225
.Combine(importFiles.Collect())
209-
.Combine(allTagHelpers)
226+
.WithLambdaComparer((old, @new) => old.Left.Equals(@new.Left) && old.Right.SequenceEqual(@new.Right), (a) => a.GetHashCode())
210227
.Combine(razorSourceGeneratorOptions)
211228
.Select(static (pair, _) =>
212229
{
213-
var (((sourceItem, imports), allTagHelpers), razorSourceGeneratorOptions) = pair;
230+
var ((sourceItem, imports), razorSourceGeneratorOptions) = pair;
231+
232+
RazorSourceGeneratorEventSource.Log.ParseRazorDocumentStart(sourceItem.RelativePhysicalPath);
233+
234+
var projectEngine = GetGenerationProjectEngine(sourceItem, imports, razorSourceGeneratorOptions);
235+
236+
var document = projectEngine.ProcessInitialParse(sourceItem);
237+
238+
RazorSourceGeneratorEventSource.Log.ParseRazorDocumentStop(sourceItem.RelativePhysicalPath);
239+
return (projectEngine, sourceItem.RelativePhysicalPath, document);
240+
})
241+
242+
// Add the tag helpers in, but ignore if they've changed or not, only reprocessing the actual document changed
243+
.Combine(allTagHelpers)
244+
.WithLambdaComparer((old, @new) => old.Left.Equals(@new.Left), (item) => item.GetHashCode())
245+
.Select((pair, _) =>
246+
{
247+
var ((projectEngine, filePath, codeDocument), allTagHelpers) = pair;
248+
RazorSourceGeneratorEventSource.Log.RewriteTagHelpersStart(filePath);
214249

215-
RazorSourceGeneratorEventSource.Log.RazorCodeGenerateStart(sourceItem.FilePath);
250+
codeDocument = projectEngine.ProcessTagHelpers(codeDocument, allTagHelpers, checkForIdempotency: false);
216251

217-
// Add a generated suffix so tools, such as coverlet, consider the file to be generated
218-
var hintName = GetIdentifierFromPath(sourceItem.RelativePhysicalPath) + ".g.cs";
252+
RazorSourceGeneratorEventSource.Log.RewriteTagHelpersStop(filePath);
253+
return (projectEngine, filePath, codeDocument);
254+
})
255+
256+
// next we do a second parse, along with the helpers, but check for idempotency. If the tag helpers used on the previous parse match, the compiler can skip re-computing them
257+
.Combine(allTagHelpers)
258+
.Select((pair, _) =>
259+
{
219260

220-
var projectEngine = GetGenerationProjectEngine(allTagHelpers, sourceItem, imports, razorSourceGeneratorOptions);
261+
var ((projectEngine, filePath, document), allTagHelpers) = pair;
262+
RazorSourceGeneratorEventSource.Log.CheckAndRewriteTagHelpersStart(filePath);
221263

222-
var codeDocument = projectEngine.Process(sourceItem);
223-
var csharpDocument = codeDocument.GetCSharpDocument();
264+
document = projectEngine.ProcessTagHelpers(document, allTagHelpers, checkForIdempotency: true);
224265

225-
RazorSourceGeneratorEventSource.Log.RazorCodeGenerateStop(sourceItem.FilePath);
226-
return (hintName, csharpDocument);
266+
RazorSourceGeneratorEventSource.Log.CheckAndRewriteTagHelpersStop(filePath);
267+
return (projectEngine, filePath, document);
268+
})
269+
270+
.Select((pair, _) =>
271+
{
272+
var (projectEngine, filePath, document) = pair;
273+
RazorSourceGeneratorEventSource.Log.RazorCodeGenerateStart(filePath);
274+
document = projectEngine.ProcessRemaining(document);
275+
var csharpDocument = document.CodeDocument.GetCSharpDocument();
276+
277+
RazorSourceGeneratorEventSource.Log.RazorCodeGenerateStop(filePath);
278+
return (filePath, csharpDocument);
227279
})
228280
.WithLambdaComparer(static (a, b) =>
229281
{
@@ -238,7 +290,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
238290

239291
context.RegisterSourceOutput(generatedOutput, static (context, pair) =>
240292
{
241-
var (hintName, csharpDocument) = pair;
293+
var (filePath, csharpDocument) = pair;
294+
295+
// Add a generated suffix so tools, such as coverlet, consider the file to be generated
296+
var hintName = GetIdentifierFromPath(filePath) + ".g.cs";
297+
242298
RazorSourceGeneratorEventSource.Log.AddSyntaxTrees(hintName);
243299
for (var i = 0; i < csharpDocument.Diagnostics.Count; i++)
244300
{

0 commit comments

Comments
 (0)