Skip to content

Commit b0d819f

Browse files
authored
Several changes targeted to improving perf of RazorSyntaxTree.Parse (dotnet/razor#1874)
* Several changes targeted to improving perf of RazorSyntaxTree.Parse 1) Modify ParserHelpers.IsNewLine to use a switch instead of Array.IndexOf 2) Modify Tokenizer.CreateToken to take in an array of RazorDiagnostics rather than an IReadOnlyList as that was causing a ToArray call on an empty diagnostics very often (during a SyntaxFactory.Token call) 3) Modify TokenizerBackedParser.Putback to allow an IReadOnlyList as a parameter to not require creation of a reverse enumerator. 4) Cut down allocations in HtmlMarkupParser.GetParserState by: a) Using an IReadOnlyList instead of IEnumerable to get rid of the allocations from the .any calls b) Don't allocate while reading initial spacing c) Inline the IsSpacingToken code so cut down on code executed and need to allocate a separate Func 5) Modify CSharpCodeParser.IsSpacingToken to now be a set of methods instead of a single method that allocates a Func. This is a very high traffic method. 6) Implement a fairly rudimentary Whitespace token cache, as they can be reused. This was based off Roslyn's SyntaxNodeCache, but simplified significantly. It's probably worth investigating whether you should more fully embrance token caching outside of whitespace. * PR feedback and added one more optimization in LocateOwner that's been bugging me for years. Assuming all chidlren are contained within a nodes span, we can short-circuit the DFS this code was doing significantly cutting time in this method which is important as it's exercised on the main thread during typing. * missed a space * StringTextToSnapshot's switch to IsNewLine needed to use start as the index to begin the search, not zero.\n\nCommit migrated from dotnet/razor@45411f7
1 parent ed7338c commit b0d819f

14 files changed

+155
-57
lines changed

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
1717
});
1818

1919
private static readonly Func<SyntaxToken, bool> IsValidStatementSpacingToken =
20-
IsSpacingToken(includeNewLines: true, includeComments: true);
20+
IsSpacingTokenIncludingNewLinesAndComments;
2121

2222
internal static readonly DirectiveDescriptor AddTagHelperDirectiveDescriptor = DirectiveDescriptor.CreateDirective(
2323
SyntaxConstants.CSharp.AddTagHelperKeyword,
@@ -124,7 +124,7 @@ public CSharpCodeBlockSyntax ParseBlock()
124124
{
125125
NextToken();
126126

127-
var precedingWhitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
127+
var precedingWhitespace = ReadWhile(IsSpacingTokenIncludingNewLinesAndComments);
128128

129129
// We are usually called when the other parser sees a transition '@'. Look for it.
130130
SyntaxToken transitionToken = null;
@@ -1317,7 +1317,7 @@ private void ParseExtensibleDirective(in SyntaxListBuilder<RazorSyntaxNode> buil
13171317

13181318
if (At(SyntaxKind.Whitespace))
13191319
{
1320-
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
1320+
AcceptWhile(IsSpacingTokenIncludingComments);
13211321

13221322
if (tokenDescriptor.Kind == DirectiveTokenKind.Member ||
13231323
tokenDescriptor.Kind == DirectiveTokenKind.Namespace ||
@@ -1443,7 +1443,7 @@ private void ParseExtensibleDirective(in SyntaxListBuilder<RazorSyntaxNode> buil
14431443
directiveBuilder.Add(OutputTokensAsStatementLiteral());
14441444
}
14451445

1446-
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
1446+
AcceptWhile(IsSpacingTokenIncludingComments);
14471447
SpanContext.ChunkGenerator = SpanChunkGenerator.Null;
14481448

14491449
switch (descriptor.Kind)
@@ -1455,7 +1455,7 @@ private void ParseExtensibleDirective(in SyntaxListBuilder<RazorSyntaxNode> buil
14551455
TryAccept(SyntaxKind.Semicolon);
14561456
directiveBuilder.Add(OutputAsMetaCode(Output(), AcceptedCharactersInternal.Whitespace));
14571457

1458-
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
1458+
AcceptWhile(IsSpacingTokenIncludingComments);
14591459

14601460
if (At(SyntaxKind.NewLine))
14611461
{
@@ -1478,7 +1478,7 @@ private void ParseExtensibleDirective(in SyntaxListBuilder<RazorSyntaxNode> buil
14781478
directiveBuilder.Add(OutputAsMarkupEphemeralLiteral());
14791479
break;
14801480
case DirectiveKind.RazorBlock:
1481-
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
1481+
AcceptWhile(IsSpacingTokenIncludingNewLinesAndComments);
14821482
SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.AllWhitespace;
14831483
directiveBuilder.Add(OutputTokensAsUnclassifiedLiteral());
14841484

@@ -1502,7 +1502,7 @@ private void ParseExtensibleDirective(in SyntaxListBuilder<RazorSyntaxNode> buil
15021502
});
15031503
break;
15041504
case DirectiveKind.CodeBlock:
1505-
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
1505+
AcceptWhile(IsSpacingTokenIncludingNewLinesAndComments);
15061506
SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.AllWhitespace;
15071507
directiveBuilder.Add(OutputTokensAsUnclassifiedLiteral());
15081508

@@ -1753,7 +1753,7 @@ private void ParseAwaitExpression(SyntaxListBuilder<RazorSyntaxNode> builder, CS
17531753
AcceptAndMoveNext();
17541754

17551755
// Accept 1 or more spaces between the await and the following code.
1756-
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
1756+
AcceptWhile(IsSpacingTokenIncludingComments);
17571757

17581758
// Top level basically indicates if we're within an expression or statement.
17591759
// Ex: topLevel true = @await Foo() | topLevel false = @{ await Foo(); }
@@ -1806,12 +1806,12 @@ private void ParseConditionalBlock(in SyntaxListBuilder<RazorSyntaxNode> builder
18061806
private void ParseConditionalBlock(in SyntaxListBuilder<RazorSyntaxNode> builder, Block block)
18071807
{
18081808
AcceptAndMoveNext();
1809-
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
1809+
AcceptWhile(IsSpacingTokenIncludingNewLinesAndComments);
18101810

18111811
// Parse the condition, if present (if not present, we'll let the C# compiler complain)
18121812
if (TryParseCondition(builder))
18131813
{
1814-
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
1814+
AcceptWhile(IsSpacingTokenIncludingNewLinesAndComments);
18151815

18161816
ParseExpectedCodeBlock(builder, block);
18171817
}
@@ -1874,7 +1874,7 @@ private void ParseUnconditionalBlock(in SyntaxListBuilder<RazorSyntaxNode> build
18741874
Assert(SyntaxKind.Keyword);
18751875
var block = new Block(CurrentToken, CurrentStart);
18761876
AcceptAndMoveNext();
1877-
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
1877+
AcceptWhile(IsSpacingTokenIncludingNewLinesAndComments);
18781878
ParseExpectedCodeBlock(builder, block);
18791879
}
18801880

@@ -1937,7 +1937,7 @@ private void ParseElseClause(in SyntaxListBuilder<RazorSyntaxNode> builder)
19371937
var block = new Block(CurrentToken, CurrentStart);
19381938

19391939
AcceptAndMoveNext();
1940-
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
1940+
AcceptWhile(IsSpacingTokenIncludingNewLinesAndComments);
19411941
if (At(CSharpKeyword.If))
19421942
{
19431943
// ElseIf
@@ -2059,7 +2059,7 @@ private void ParseWhileClause(in SyntaxListBuilder<RazorSyntaxNode> builder)
20592059
Accept(whitespace);
20602060
Assert(CSharpKeyword.While);
20612061
AcceptAndMoveNext();
2062-
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
2062+
AcceptWhile(IsSpacingTokenIncludingNewLinesAndComments);
20632063
if (TryParseCondition(builder) && TryAccept(SyntaxKind.Semicolon))
20642064
{
20652065
SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
@@ -2078,7 +2078,7 @@ private void ParseUsingKeyword(SyntaxListBuilder<RazorSyntaxNode> builder, CShar
20782078
var topLevel = transition != null;
20792079
var block = new Block(CurrentToken, CurrentStart);
20802080
var usingToken = EatCurrentToken();
2081-
var whitespaceOrComments = ReadWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
2081+
var whitespaceOrComments = ReadWhile(IsSpacingTokenIncludingComments);
20822082
var atLeftParen = At(SyntaxKind.LeftParenthesis);
20832083
var atIdentifier = At(SyntaxKind.Identifier);
20842084
var atStatic = At(CSharpKeyword.Static);
@@ -2115,7 +2115,7 @@ private void ParseUsingKeyword(SyntaxListBuilder<RazorSyntaxNode> builder, CShar
21152115
builder.Add(transition);
21162116
}
21172117
AcceptAndMoveNext();
2118-
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
2118+
AcceptWhile(IsSpacingTokenIncludingComments);
21192119
ParseStandardStatement(builder);
21202120
}
21212121
else
@@ -2132,7 +2132,7 @@ private void ParseUsingKeyword(SyntaxListBuilder<RazorSyntaxNode> builder, CShar
21322132
}
21332133

21342134
AcceptAndMoveNext();
2135-
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
2135+
AcceptWhile(IsSpacingTokenIncludingComments);
21362136
}
21372137

21382138
if (topLevel)
@@ -2145,7 +2145,7 @@ private void ParseUsingStatement(in SyntaxListBuilder<RazorSyntaxNode> builder,
21452145
{
21462146
Assert(CSharpKeyword.Using);
21472147
AcceptAndMoveNext();
2148-
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
2148+
AcceptWhile(IsSpacingTokenIncludingComments);
21492149

21502150
Assert(SyntaxKind.LeftParenthesis);
21512151
if (transition != null)
@@ -2156,7 +2156,7 @@ private void ParseUsingStatement(in SyntaxListBuilder<RazorSyntaxNode> builder,
21562156
// Parse condition
21572157
if (TryParseCondition(builder))
21582158
{
2159-
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
2159+
AcceptWhile(IsSpacingTokenIncludingNewLinesAndComments);
21602160

21612161
// Parse code block
21622162
ParseExpectedCodeBlock(builder, block);
@@ -2174,22 +2174,22 @@ private void ParseUsingDeclaration(in SyntaxListBuilder<RazorSyntaxNode> builder
21742174
AcceptAndMoveNext();
21752175
var isStatic = false;
21762176
var nonNamespaceTokenCount = TokenBuilder.Count;
2177-
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
2177+
AcceptWhile(IsSpacingTokenIncludingComments);
21782178
var start = CurrentStart;
21792179
if (At(SyntaxKind.Identifier))
21802180
{
21812181
// non-static using
21822182
nonNamespaceTokenCount = TokenBuilder.Count;
21832183
TryParseNamespaceOrTypeName(directiveBuilder);
2184-
var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
2184+
var whitespace = ReadWhile(IsSpacingTokenIncludingNewLinesAndComments);
21852185
if (At(SyntaxKind.Assign))
21862186
{
21872187
// Alias
21882188
Accept(whitespace);
21892189
Assert(SyntaxKind.Assign);
21902190
AcceptAndMoveNext();
21912191

2192-
AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
2192+
AcceptWhile(IsSpacingTokenIncludingNewLinesAndComments);
21932193

21942194
// One more namespace or type name
21952195
TryParseNamespaceOrTypeName(directiveBuilder);
@@ -2205,7 +2205,7 @@ private void ParseUsingDeclaration(in SyntaxListBuilder<RazorSyntaxNode> builder
22052205
// static using
22062206
isStatic = true;
22072207
AcceptAndMoveNext();
2208-
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
2208+
AcceptWhile(IsSpacingTokenIncludingComments);
22092209
nonNamespaceTokenCount = TokenBuilder.Count;
22102210
TryParseNamespaceOrTypeName(directiveBuilder);
22112211
}
@@ -2391,7 +2391,7 @@ private IEnumerable<SyntaxToken> SkipToNextImportantToken(in SyntaxListBuilder<R
23912391
{
23922392
while (!EndOfFile)
23932393
{
2394-
var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
2394+
var whitespace = ReadWhile(IsSpacingTokenIncludingNewLinesAndComments);
23952395
if (At(SyntaxKind.RazorCommentTransition))
23962396
{
23972397
Accept(whitespace);
@@ -2622,11 +2622,24 @@ protected internal bool At(CSharpKeyword keyword)
26222622
result.Value == keyword;
26232623
}
26242624

2625-
protected static Func<SyntaxToken, bool> IsSpacingToken(bool includeNewLines, bool includeComments)
2626-
{
2627-
return token => token.Kind == SyntaxKind.Whitespace ||
2628-
(includeNewLines && token.Kind == SyntaxKind.NewLine) ||
2629-
(includeComments && token.Kind == SyntaxKind.CSharpComment);
2625+
protected static bool IsSpacingToken(SyntaxToken token)
2626+
{
2627+
return token.Kind == SyntaxKind.Whitespace;
2628+
}
2629+
2630+
protected static bool IsSpacingTokenIncludingNewLines(SyntaxToken token)
2631+
{
2632+
return IsSpacingToken(token) || token.Kind == SyntaxKind.NewLine;
2633+
}
2634+
2635+
protected static bool IsSpacingTokenIncludingComments(SyntaxToken token)
2636+
{
2637+
return IsSpacingToken(token) || token.Kind == SyntaxKind.CSharpComment;
2638+
}
2639+
2640+
protected static bool IsSpacingTokenIncludingNewLinesAndComments(SyntaxToken token)
2641+
{
2642+
return IsSpacingTokenIncludingNewLines(token) || token.Kind == SyntaxKind.CSharpComment;
26302643
}
26312644

26322645
protected class Block

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpLanguageCharacteristics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public override CSharpTokenizer CreateTokenizer(ITextDocument source)
7575
return new CSharpTokenizer(source);
7676
}
7777

78-
protected override SyntaxToken CreateToken(string content, SyntaxKind kind, IReadOnlyList<RazorDiagnostic> errors)
78+
protected override SyntaxToken CreateToken(string content, SyntaxKind kind, RazorDiagnostic[] errors)
7979
{
8080
return SyntaxFactory.Token(kind, content, errors);
8181
}

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpTokenizer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ protected override string GetTokenContent(SyntaxKind type)
344344
return base.GetTokenContent(type);
345345
}
346346

347-
protected override SyntaxToken CreateToken(string content, SyntaxKind kind, IReadOnlyList<RazorDiagnostic> errors)
347+
protected override SyntaxToken CreateToken(string content, SyntaxKind kind, RazorDiagnostic [] errors)
348348
{
349349
return SyntaxFactory.Token(kind, content, errors);
350350
}

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/HtmlLanguageCharacteristics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public override SyntaxKind GetKnownTokenType(KnownTokenType type)
120120
}
121121
}
122122

123-
protected override SyntaxToken CreateToken(string content, SyntaxKind kind, IReadOnlyList<RazorDiagnostic> errors)
123+
protected override SyntaxToken CreateToken(string content, SyntaxKind kind, RazorDiagnostic [] errors)
124124
{
125125
return SyntaxFactory.Token(kind, content, errors);
126126
}

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/HtmlMarkupParser.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,12 +1718,33 @@ private bool AcceptTokenUntilAll(in SyntaxListBuilder<RazorSyntaxNode> builder,
17181718
return false;
17191719
}
17201720

1721+
private IReadOnlyList<SyntaxToken> FastReadWhitespaceAndNewLines()
1722+
{
1723+
if (EnsureCurrent() && (CurrentToken.Kind == SyntaxKind.Whitespace || CurrentToken.Kind == SyntaxKind.NewLine))
1724+
{
1725+
var whitespaceTokens = new List<SyntaxToken>();
1726+
1727+
whitespaceTokens.Add(CurrentToken);
1728+
NextToken();
1729+
1730+
while (EnsureCurrent() && (CurrentToken.Kind == SyntaxKind.Whitespace || CurrentToken.Kind == SyntaxKind.NewLine))
1731+
{
1732+
whitespaceTokens.Add(CurrentToken);
1733+
NextToken();
1734+
}
1735+
1736+
return whitespaceTokens;
1737+
}
1738+
1739+
return Array.Empty<SyntaxToken>();
1740+
}
1741+
17211742
private ParserState GetParserState(ParseMode mode)
17221743
{
1723-
var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true));
1744+
var whitespace = FastReadWhitespaceAndNewLines();
17241745
try
17251746
{
1726-
if (!whitespace.Any() && EndOfFile)
1747+
if (whitespace.Count == 0 && EndOfFile)
17271748
{
17281749
return ParserState.EOF;
17291750
}
@@ -1742,7 +1763,7 @@ private ParserState GetParserState(ParseMode mode)
17421763
// Let the transition parser handle the preceding whitespace.
17431764
return ParserState.CodeTransition;
17441765
}
1745-
else if (whitespace.Any())
1766+
else if (whitespace.Count > 0)
17461767
{
17471768
// This whitespace isn't sensitive to what comes after it.
17481769
return ParserState.Misc;
@@ -1792,7 +1813,7 @@ private ParserState GetParserState(ParseMode mode)
17921813
}
17931814
finally
17941815
{
1795-
if (whitespace.Any())
1816+
if (whitespace.Count > 0)
17961817
{
17971818
PutCurrentBack();
17981819
PutBack(whitespace);

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/HtmlTokenizer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public override SyntaxKind RazorCommentStarKind
3737
get { return SyntaxKind.RazorCommentStar; }
3838
}
3939

40-
protected override SyntaxToken CreateToken(string content, SyntaxKind type, IReadOnlyList<RazorDiagnostic> errors)
40+
protected override SyntaxToken CreateToken(string content, SyntaxKind type, RazorDiagnostic[] errors)
4141
{
4242
return SyntaxFactory.Token(type, content, errors);
4343
}

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LanguageCharacteristics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,6 @@ public virtual bool KnowsTokenType(KnownTokenType type)
103103
return type == KnownTokenType.Unknown || !Equals(GetKnownTokenType(type), GetKnownTokenType(KnownTokenType.Unknown));
104104
}
105105

106-
protected abstract SyntaxToken CreateToken(string content, SyntaxKind type, IReadOnlyList<RazorDiagnostic> errors);
106+
protected abstract SyntaxToken CreateToken(string content, SyntaxKind type, RazorDiagnostic[] errors);
107107
}
108108
}

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LegacySyntaxNodeExtensions.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,19 @@ public static SyntaxNode LocateOwner(this SyntaxNode node, SourceChange change)
9595
return null;
9696
}
9797

98+
if (node.EndPosition < change.Span.AbsoluteIndex)
99+
{
100+
// no need to look into this node as it completely precedes the change
101+
return null;
102+
}
103+
98104
if (IsSpanKind(node))
99105
{
100106
var editHandler = node.GetSpanContext()?.EditHandler ?? SpanEditHandler.CreateDefault();
101107
return editHandler.OwnsChange(node, change) ? node : null;
102108
}
103109

104-
SyntaxNode owner = null;
105-
IEnumerable<SyntaxNode> children;
110+
IReadOnlyList<SyntaxNode> children;
106111
if (node is MarkupStartTagSyntax startTag)
107112
{
108113
children = startTag.Children;
@@ -124,8 +129,10 @@ public static SyntaxNode LocateOwner(this SyntaxNode node, SourceChange change)
124129
children = node.ChildNodes();
125130
}
126131

127-
foreach (var child in children)
132+
SyntaxNode owner = null;
133+
for (int i = 0; i < children.Count; i++)
128134
{
135+
var child = children[i];
129136
owner = LocateOwner(child, change);
130137
if (owner != null)
131138
{

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/ParserHelpers.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
1010
{
1111
internal static class ParserHelpers
1212
{
13-
public static char[] NewLineCharacters = new[]
13+
public static bool IsNewLine(char value)
1414
{
15-
'\r', // Carriage return
16-
'\n', // Linefeed
17-
'\u0085', // Next Line
18-
'\u2028', // Line separator
19-
'\u2029' // Paragraph separator
20-
};
15+
switch (value)
16+
{
17+
case '\r': // Carriage return
18+
case '\n': // Linefeed
19+
case '\u0085': // Next Line
20+
case '\u2028': // Line separator
21+
case '\u2029': // Paragraph separator
22+
return true;
23+
}
2124

22-
public static bool IsNewLine(char value) => Array.IndexOf<char>(NewLineCharacters, value) != -1;
25+
return false;
26+
}
2327

2428
public static bool IsNewLine(string value)
2529
{

0 commit comments

Comments
 (0)