Skip to content

Commit 42419be

Browse files
Compiler: Replace SyntaxListBuilder with PooledArrayBuilder<T> and miscellaneous performance tweaks (#11841)
This change is primarily about removing the `Syntax.SyntaxListBuilder` and `Syntax.SyntaxListBuilder<T>` from the compiler and replacing usages with `PooledArrayBuilder<T>`. This allows the compiler to use stack memory for small lists and only acquire pooled heap memory for larger lists. In addition, I've made several other changes and some small refactorings along the way. In particular, I've introduced `BaseMarkupStartTagSyntax` and `BaseMarkupEndTagSyntax` types to share code between the normal markup tag syntax types and their tag helper variants. ---- CI Build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2704937&view=results Test Insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/635185 Toolset Test Run: https://dev.azure.com/dnceng/internal/_build/results?buildId=2704938&view=results#SourceCard
2 parents 8bbfe6c + 562477a commit 42419be

35 files changed

+1218
-1019
lines changed

src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DirectiveTokenEditHandlerTest.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using Microsoft.AspNetCore.Razor.Language.Legacy;
99
using Microsoft.AspNetCore.Razor.Language.Syntax;
10+
using Microsoft.AspNetCore.Razor.PooledObjects;
1011
using Xunit;
1112

1213
namespace Microsoft.AspNetCore.Razor.Language.Test;
@@ -63,13 +64,14 @@ public void CanAcceptChange_RejectsWhitespaceChanges(int index, int length, stri
6364

6465
private static CSharpStatementLiteralSyntax GetSyntaxNode(DirectiveTokenEditHandler editHandler, string content)
6566
{
66-
using var _ = SyntaxListBuilderPool.GetPooledBuilder<SyntaxToken>(out var builder);
67+
using PooledArrayBuilder<SyntaxToken> builder = [];
6768

6869
var tokens = NativeCSharpLanguageCharacteristics.Instance.TokenizeString(content).ToArray();
6970
foreach (var token in tokens)
7071
{
7172
builder.Add((SyntaxToken)token.CreateRed());
7273
}
74+
7375
var node = SyntaxFactory.CSharpStatementLiteral(builder.ToList(), SpanChunkGenerator.Null);
7476

7577
return node.WithEditHandler(editHandler);

src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Legacy/CodeBlockEditHandlerTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Linq;
77
using Microsoft.AspNetCore.Razor.Language.Legacy;
88
using Microsoft.AspNetCore.Razor.Language.Syntax;
9+
using Microsoft.AspNetCore.Razor.PooledObjects;
910
using Xunit;
1011

1112
namespace Microsoft.AspNetCore.Razor.Language.Test.Legacy;
@@ -281,7 +282,7 @@ public void ContainsInvalidContent_ValidContent_ReturnsFalse(string content)
281282

282283
private static SyntaxNode GetSpan(SourceLocation start, string content)
283284
{
284-
using var _ = SyntaxListBuilderPool.GetPooledBuilder<SyntaxToken>(out var builder);
285+
using PooledArrayBuilder<SyntaxToken> builder = [];
285286

286287
var tokens = NativeCSharpLanguageCharacteristics.Instance.TokenizeString(content).ToArray();
287288
foreach (var token in tokens)

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorIntermediateNodeLoweringPhase.cs

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,9 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument, Cancellation
128128
documentNode.Diagnostics.Add(diagnostic);
129129
}
130130

131-
if (imports is { IsDefault: false } importsArray)
131+
if (!imports.IsDefaultOrEmpty)
132132
{
133-
foreach (var import in importsArray)
133+
foreach (var import in imports)
134134
{
135135
foreach (var diagnostic in import.Diagnostics)
136136
{
@@ -492,38 +492,30 @@ public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax no
492492
return node.GetSourceSpan(SourceDocument);
493493
}
494494

495-
protected static SyntaxList<SyntaxToken> MergeLiterals(
496-
SyntaxList<SyntaxToken>? literal1,
497-
SyntaxList<SyntaxToken>? literal2,
498-
SyntaxList<SyntaxToken>? literal3 = null,
499-
SyntaxList<SyntaxToken>? literal4 = null,
500-
SyntaxList<SyntaxToken>? literal5 = null)
495+
protected static SyntaxList<SyntaxToken> MergeTokenLists(params ReadOnlySpan<SyntaxList<SyntaxToken>?> tokenLists)
501496
{
502-
using var _ = SyntaxListBuilderPool.GetPooledBuilder<SyntaxToken>(out var builder);
497+
var count = 0;
503498

504-
if (literal1 is { } tokens1)
499+
foreach (var tokenList in tokenLists)
505500
{
506-
builder.AddRange(tokens1);
501+
count += tokenList?.Count ?? 0;
507502
}
508503

509-
if (literal2 is { } tokens2)
504+
if (count == 0)
510505
{
511-
builder.AddRange(tokens2);
506+
return default;
512507
}
513508

514-
if (literal3 is { } tokens3)
515-
{
516-
builder.AddRange(tokens3);
517-
}
509+
using var builder = new PooledArrayBuilder<SyntaxToken>(count);
518510

519-
if (literal4 is { } tokens4)
511+
foreach (var tokenList in tokenLists)
520512
{
521-
builder.AddRange(tokens4);
522-
}
513+
if (tokenList == null)
514+
{
515+
continue;
516+
}
523517

524-
if (literal5 is { } tokens5)
525-
{
526-
builder.AddRange(tokens5);
518+
builder.AddRange(tokenList.GetValueOrDefault());
527519
}
528520

529521
return builder.ToList();
@@ -574,7 +566,10 @@ public bool TryCast<TNode>(out ImmutableArray<TNode> result)
574566

575567
protected static MarkupTextLiteralSyntax MergeAttributeValue(MarkupLiteralAttributeValueSyntax node)
576568
{
577-
var valueTokens = MergeLiterals(node.Prefix?.LiteralTokens, node.Value?.LiteralTokens);
569+
var valueTokens = MergeTokenLists(
570+
node.Prefix?.LiteralTokens,
571+
node.Value?.LiteralTokens);
572+
578573
var rewritten = node.Prefix?.Update(valueTokens, node.Prefix.ChunkGenerator) ?? node.Value?.Update(valueTokens, node.Value.ChunkGenerator);
579574
rewritten = (MarkupTextLiteralSyntax)rewritten?.Green.CreateRed(node, node.Position);
580575

@@ -606,12 +601,13 @@ public LegacyFileKindVisitor(DocumentIntermediateNode document, IntermediateNode
606601
// Suffix="
607602
public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
608603
{
609-
var prefixTokens = MergeLiterals(
604+
var prefixTokens = MergeTokenLists(
610605
node.NamePrefix?.LiteralTokens,
611606
node.Name.LiteralTokens,
612607
node.NameSuffix?.LiteralTokens,
613-
node.EqualsToken != null ? new SyntaxList<SyntaxToken>(node.EqualsToken) : default,
608+
new SyntaxList<SyntaxToken>(node.EqualsToken),
614609
node.ValuePrefix?.LiteralTokens);
610+
615611
var prefix = (MarkupTextLiteralSyntax)SyntaxFactory.MarkupTextLiteral(prefixTokens, chunkGenerator: null).Green.CreateRed(node, node.NamePrefix?.Position ?? node.Name.Position);
616612

617613
var name = node.Name.GetContent();
@@ -629,7 +625,7 @@ public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
629625

630626
if (children.TryCast<MarkupLiteralAttributeValueSyntax>(out var attributeLiteralArray))
631627
{
632-
using var _ = SyntaxListBuilderPool.GetPooledBuilder<SyntaxToken>(out var builder);
628+
using var builder = new PooledArrayBuilder<SyntaxToken>();
633629

634630
foreach (var literal in attributeLiteralArray)
635631
{
@@ -639,7 +635,11 @@ public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
639635

640636
var rewritten = SyntaxFactory.MarkupTextLiteral(builder.ToList(), chunkGenerator: null);
641637

642-
var mergedLiterals = MergeLiterals(prefix?.LiteralTokens, rewritten.LiteralTokens, node.ValueSuffix?.LiteralTokens);
638+
var mergedLiterals = MergeTokenLists(
639+
prefix?.LiteralTokens,
640+
rewritten.LiteralTokens,
641+
node.ValueSuffix?.LiteralTokens);
642+
643643
var mergedAttribute = SyntaxFactory.MarkupTextLiteral(mergedLiterals, chunkGenerator: null).Green.CreateRed(node.Parent, node.Position);
644644
Visit(mergedAttribute);
645645

@@ -675,9 +675,10 @@ public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttribute
675675
}
676676

677677
// Minimized attributes are just html content.
678-
var literals = MergeLiterals(
678+
var literals = MergeTokenLists(
679679
node.NamePrefix?.LiteralTokens,
680680
node.Name?.LiteralTokens);
681+
681682
var literal = SyntaxFactory.MarkupTextLiteral(literals, chunkGenerator: null).Green.CreateRed(node.Parent, node.Position);
682683

683684
Visit(literal);
@@ -1188,7 +1189,7 @@ private void VisitAttributeValue(SyntaxNode node)
11881189

11891190
if (children.TryCast<MarkupLiteralAttributeValueSyntax>(out var attributeLiteralArray))
11901191
{
1191-
using var _ = SyntaxListBuilderPool.GetPooledBuilder<SyntaxToken>(out var builder);
1192+
using PooledArrayBuilder<SyntaxToken> builder = [];
11921193

11931194
foreach (var literal in attributeLiteralArray)
11941195
{
@@ -1201,7 +1202,7 @@ private void VisitAttributeValue(SyntaxNode node)
12011202
}
12021203
else if (children.TryCast<MarkupTextLiteralSyntax>(out var markupLiteralArray))
12031204
{
1204-
using var _ = SyntaxListBuilderPool.GetPooledBuilder<SyntaxToken>(out var builder);
1205+
using PooledArrayBuilder<SyntaxToken> builder = [];
12051206

12061207
foreach (var literal in markupLiteralArray)
12071208
{
@@ -1213,7 +1214,7 @@ private void VisitAttributeValue(SyntaxNode node)
12131214
}
12141215
else if (children.TryCast<CSharpExpressionLiteralSyntax>(out var expressionLiteralArray))
12151216
{
1216-
using var _ = SyntaxListBuilderPool.GetPooledBuilder<SyntaxToken>(out var builder);
1217+
using PooledArrayBuilder<SyntaxToken> builder = [];
12171218

12181219
SpanEditHandler editHandler = null;
12191220
ISpanChunkGenerator generator = null;
@@ -1244,16 +1245,16 @@ private void Combine(HtmlContentIntermediateNode node, SyntaxNode item)
12441245
Source = BuildSourceSpanFromNode(item),
12451246
});
12461247

1247-
if (node.Source != null)
1248+
if (node.Source is SourceSpan source)
12481249
{
12491250
node.Source = new SourceSpan(
1250-
node.Source.Value.FilePath,
1251-
node.Source.Value.AbsoluteIndex,
1252-
node.Source.Value.LineIndex,
1253-
node.Source.Value.CharacterIndex,
1254-
node.Source.Value.Length + item.Width,
1255-
node.Source.Value.LineCount,
1256-
node.Source.Value.EndCharacterIndex);
1251+
source.FilePath,
1252+
source.AbsoluteIndex,
1253+
source.LineIndex,
1254+
source.CharacterIndex,
1255+
source.Length + item.Width,
1256+
source.LineCount,
1257+
source.EndCharacterIndex);
12571258
}
12581259
}
12591260
}
@@ -1298,8 +1299,8 @@ public override void VisitMarkupElement(MarkupElementSyntax node)
12981299
if (node.StartTag != null && node.EndTag != null && node.StartTag.IsVoidElement())
12991300
{
13001301
element.Diagnostics.Add(
1301-
ComponentDiagnosticFactory.Create_UnexpectedClosingTagForVoidElement(
1302-
BuildSourceSpanFromNode(node.EndTag), node.EndTag.GetTagNameWithOptionalBang()));
1302+
ComponentDiagnosticFactory.Create_UnexpectedClosingTagForVoidElement(
1303+
BuildSourceSpanFromNode(node.EndTag), node.EndTag.GetTagNameWithOptionalBang()));
13031304
}
13041305
else if (node.StartTag != null && node.EndTag == null && !node.StartTag.IsVoidElement() && !node.StartTag.IsSelfClosing())
13051306
{
@@ -1388,12 +1389,13 @@ public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
13881389
// building Prefix and Suffix, even though we don't really use them. If we
13891390
// end up using another node type in the future this can be simplified quite
13901391
// a lot.
1391-
var prefixTokens = MergeLiterals(
1392+
var prefixTokens = MergeTokenLists(
13921393
node.NamePrefix?.LiteralTokens,
13931394
node.Name.LiteralTokens,
13941395
node.NameSuffix?.LiteralTokens,
1395-
node.EqualsToken == null ? new SyntaxList<SyntaxToken>() : new SyntaxList<SyntaxToken>(node.EqualsToken),
1396+
new SyntaxList<SyntaxToken>(node.EqualsToken),
13961397
node.ValuePrefix?.LiteralTokens);
1398+
13971399
var prefix = (MarkupTextLiteralSyntax)SyntaxFactory.MarkupTextLiteral(prefixTokens, chunkGenerator: null).Green.CreateRed(node, node.NamePrefix?.Position ?? node.Name.Position);
13981400

13991401
var name = node.Name.GetContent();
@@ -1412,7 +1414,10 @@ public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
14121414

14131415
public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node)
14141416
{
1415-
var prefixTokens = MergeLiterals(node.NamePrefix?.LiteralTokens, node.Name.LiteralTokens);
1417+
var prefixTokens = MergeTokenLists(
1418+
node.NamePrefix?.LiteralTokens,
1419+
node.Name.LiteralTokens);
1420+
14161421
var prefix = (MarkupTextLiteralSyntax)SyntaxFactory.MarkupTextLiteral(prefixTokens, chunkGenerator: null).Green.CreateRed(node, node.NamePrefix?.Position ?? node.Name.Position);
14171422

14181423
var name = node.Name.GetContent();
@@ -2158,7 +2163,7 @@ private void VisitAttributeValue(SyntaxNode node)
21582163

21592164
if (children.TryCast<MarkupLiteralAttributeValueSyntax>(out var attributeLiteralArray))
21602165
{
2161-
using var _ = SyntaxListBuilderPool.GetPooledBuilder<SyntaxToken>(out var valueTokens);
2166+
using PooledArrayBuilder<SyntaxToken> valueTokens = [];
21622167

21632168
foreach (var literal in attributeLiteralArray)
21642169
{
@@ -2171,7 +2176,7 @@ private void VisitAttributeValue(SyntaxNode node)
21712176
}
21722177
else if (children.TryCast<MarkupTextLiteralSyntax>(out var markupLiteralArray))
21732178
{
2174-
using var _ = SyntaxListBuilderPool.GetPooledBuilder<SyntaxToken>(out var builder);
2179+
using PooledArrayBuilder<SyntaxToken> builder = [];
21752180

21762181
foreach (var literal in markupLiteralArray)
21772182
{
@@ -2183,7 +2188,7 @@ private void VisitAttributeValue(SyntaxNode node)
21832188
}
21842189
else if (children.TryCast<CSharpExpressionLiteralSyntax>(out var expressionLiteralArray))
21852190
{
2186-
using var _ = SyntaxListBuilderPool.GetPooledBuilder<SyntaxToken>(out var builder);
2191+
using PooledArrayBuilder<SyntaxToken> builder = [];
21872192

21882193
ISpanChunkGenerator generator = null;
21892194
SpanEditHandler editHandler = null;
@@ -2195,7 +2200,12 @@ private void VisitAttributeValue(SyntaxNode node)
21952200
}
21962201

21972202
var rewritten = SyntaxFactory.CSharpExpressionLiteral(builder.ToList(), generator).Green.CreateRed(node.Parent, position);
2198-
rewritten = editHandler != null ? rewritten.WithEditHandler(editHandler) : rewritten;
2203+
2204+
if (editHandler != null)
2205+
{
2206+
rewritten = rewritten.WithEditHandler(editHandler);
2207+
}
2208+
21992209
Visit(rewritten);
22002210
}
22012211
else

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/LegacySyntaxNodeExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public static TNode WithEditHandler<TNode>(this TNode node, SpanEditHandler? edi
164164

165165
if (node.IsSpanKind())
166166
{
167-
var editHandler = node.GetEditHandler() ?? SpanEditHandler.CreateDefault(AcceptedCharactersInternal.Any);
167+
var editHandler = node.GetEditHandler() ?? SpanEditHandler.GetDefault(AcceptedCharactersInternal.Any);
168168
return editHandler.OwnsChange(node, change) ? node : null;
169169
}
170170

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/SpanEditHandler.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,43 @@
55

66
using System;
77
using System.Collections.Generic;
8+
using System.Collections.Immutable;
89
using System.Diagnostics;
9-
using System.Linq;
1010
using Microsoft.AspNetCore.Razor.Language.Syntax;
1111

1212
namespace Microsoft.AspNetCore.Razor.Language.Legacy;
1313

1414
internal class SpanEditHandler
1515
{
16+
internal static readonly Func<string, IEnumerable<Syntax.InternalSyntax.SyntaxToken>> NoTokenizer = _ => [];
17+
18+
private static readonly ImmutableArray<SpanEditHandler> s_defaultEditHandlers =
19+
[
20+
// AcceptedCharactersInternal consists up of 3 bit flags.
21+
// So, there are 8 possible combinations from 0 to 7.
22+
CreateDefault(NoTokenizer, AcceptedCharactersInternal.None),
23+
CreateDefault(NoTokenizer, (AcceptedCharactersInternal)1),
24+
CreateDefault(NoTokenizer, (AcceptedCharactersInternal)2),
25+
CreateDefault(NoTokenizer, (AcceptedCharactersInternal)3),
26+
CreateDefault(NoTokenizer, (AcceptedCharactersInternal)4),
27+
CreateDefault(NoTokenizer, (AcceptedCharactersInternal)5),
28+
CreateDefault(NoTokenizer, (AcceptedCharactersInternal)6),
29+
CreateDefault(NoTokenizer, (AcceptedCharactersInternal)7)
30+
];
31+
1632
private static readonly int TypeHashCode = typeof(SpanEditHandler).GetHashCode();
1733

1834
public required AcceptedCharactersInternal AcceptedCharacters { get; init; }
1935
public required Func<string, IEnumerable<Syntax.InternalSyntax.SyntaxToken>> Tokenizer { get; init; }
2036

21-
public static SpanEditHandler CreateDefault(AcceptedCharactersInternal acceptedCharacters)
37+
public static SpanEditHandler GetDefault(AcceptedCharactersInternal acceptedCharacters)
2238
{
23-
return CreateDefault(static c => Enumerable.Empty<Syntax.InternalSyntax.SyntaxToken>(), acceptedCharacters);
39+
var index = (int)acceptedCharacters;
40+
41+
ArgHelper.ThrowIfNegative(index, nameof(acceptedCharacters));
42+
ArgHelper.ThrowIfGreaterThanOrEqual(index, 8, nameof(acceptedCharacters));
43+
44+
return s_defaultEditHandlers[index];
2445
}
2546

2647
public static SpanEditHandler CreateDefault(Func<string, IEnumerable<Syntax.InternalSyntax.SyntaxToken>> tokenizer, AcceptedCharactersInternal acceptedCharacters)

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/SpanEditHandlerBuilder.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7-
using System.Linq;
87
using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax;
98

109
namespace Microsoft.AspNetCore.Razor.Language.Legacy;
1110

1211
internal sealed class SpanEditHandlerBuilder
1312
{
14-
private static readonly Func<string, IEnumerable<SyntaxToken>> DefaultTokenizer = static content => Enumerable.Empty<SyntaxToken>();
15-
private static readonly SpanEditHandler DefaultEditHandler = SpanEditHandler.CreateDefault(AcceptedCharactersInternal.Any);
13+
private static readonly Func<string, IEnumerable<SyntaxToken>> DefaultTokenizer = SpanEditHandler.NoTokenizer;
14+
private static readonly SpanEditHandler DefaultEditHandler = SpanEditHandler.GetDefault(AcceptedCharactersInternal.Any);
1615

1716
private readonly Func<string, IEnumerable<SyntaxToken>>? _defaultLanguageTokenizer;
1817
private readonly SpanEditHandler? _defaultLanguageEditHandler;

0 commit comments

Comments
 (0)