Skip to content

Commit 904fd19

Browse files
committed
A couple more perf optimizations focused around memory allocations.
This is the last of the easy wins that I could find for typing in large razor files (minus the logged bug to move the divergence checker code off the UI thread). In the profile for the large document editing, these changes reduce allocated memory during RazorSyntaxTree.Parse by about 25%. CPU wise, the win isn't quite as dramatic, only a couple percent improvement under RazorSyntaxTree.Parse.
1 parent 3a9368b commit 904fd19

File tree

3 files changed

+93
-78
lines changed

3 files changed

+93
-78
lines changed

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -554,13 +554,16 @@ private StateResult VerbatimStringLiteral()
554554
return Transition(CSharpTokenizerState.Data, EndToken(SyntaxKind.StringLiteral));
555555
}
556556

557-
private StateResult QuotedCharacterLiteral() => QuotedLiteral('\'', SyntaxKind.CharacterLiteral);
557+
private StateResult QuotedCharacterLiteral() => QuotedLiteral('\'', IsEndQuotedCharacterLiteral, SyntaxKind.CharacterLiteral);
558558

559-
private StateResult QuotedStringLiteral() => QuotedLiteral('\"', SyntaxKind.StringLiteral);
559+
private StateResult QuotedStringLiteral() => QuotedLiteral('\"', IsEndQuotedStringLiteral, SyntaxKind.StringLiteral);
560560

561-
private StateResult QuotedLiteral(char quote, SyntaxKind literalType)
561+
private Func<char, bool> IsEndQuotedCharacterLiteral = (c) => c == '\\' || c == '\'' || ParserHelpers.IsNewLine(c);
562+
private Func<char, bool> IsEndQuotedStringLiteral = (c) => c == '\\' || c == '\"' || ParserHelpers.IsNewLine(c);
563+
564+
private StateResult QuotedLiteral(char quote, Func<char, bool> isEndQuotedLiteral, SyntaxKind literalType)
562565
{
563-
TakeUntil(c => c == '\\' || c == quote || ParserHelpers.IsNewLine(c));
566+
TakeUntil(isEndQuotedLiteral);
564567
if (CurrentCharacter == '\\')
565568
{
566569
TakeCurrent(); // Take the '\'

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

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ internal class LineTrackingStringBuffer
1313
private readonly IList<TextLine> _lines;
1414
private readonly string _filePath;
1515
private TextLine _currentLine;
16-
private TextLine _endLine;
1716

1817
public LineTrackingStringBuffer(string content, string filePath)
1918
: this(content.ToCharArray(), filePath)
@@ -22,67 +21,66 @@ public LineTrackingStringBuffer(string content, string filePath)
2221

2322
public LineTrackingStringBuffer(char[] content, string filePath)
2423
{
25-
_endLine = new TextLine(0, 0);
26-
_lines = new List<TextLine>() { _endLine };
24+
_lines = new List<TextLine>();
2725

28-
Append(content);
26+
BuildTextLines(content);
2927

3028
_filePath = filePath;
3129
}
3230

3331
public int Length
3432
{
35-
get { return _endLine.End; }
33+
get { return _lines[_lines.Count - 1].End; }
3634
}
3735

3836
public SourceLocation EndLocation
3937
{
40-
get { return new SourceLocation(_filePath, Length, _lines.Count - 1, _lines[_lines.Count - 1].Length); }
38+
get { return new SourceLocation(_filePath, Length, _lines.Count - 1, Length); }
4139
}
4240

4341
public CharacterReference CharAt(int absoluteIndex)
4442
{
4543
var line = FindLine(absoluteIndex);
46-
if (line == null)
44+
if (line.IsDefault)
4745
{
4846
throw new ArgumentOutOfRangeException(nameof(absoluteIndex));
4947
}
5048
var idx = absoluteIndex - line.Start;
5149
return new CharacterReference(line.Content[idx], new SourceLocation(_filePath, absoluteIndex, line.Index, idx));
5250
}
5351

54-
private void Append(char[] content)
52+
private void BuildTextLines(char[] content)
5553
{
54+
string lineText;
55+
var lineStart = 0;
56+
5657
for (int i = 0; i < content.Length; i++)
5758
{
58-
AppendCore(content[i]);
59-
60-
// \r on it's own: Start a new line, otherwise wait for \n
61-
// Other Newline: Start a new line
62-
if ((content[i] == '\r' && (i + 1 == content.Length || content[i + 1] != '\n')) || (content[i] != '\r' && ParserHelpers.IsNewLine(content[i])))
59+
if (ParserHelpers.IsNewLine(content[i]))
6360
{
64-
PushNewLine();
61+
// \r on it's own: Start a new line, otherwise wait for \n
62+
// Other Newline: Start a new line
63+
if (content[i] == '\r' && i + 1 < content.Length && content[i + 1] == '\n')
64+
{
65+
i++;
66+
}
67+
68+
lineText = new string(content, lineStart, (i - lineStart) + 1); // +1 to include the current char
69+
_lines.Add(new TextLine(lineStart, _lines.Count, lineText));
70+
71+
lineStart = i + 1;
6572
}
6673
}
67-
}
6874

69-
private void PushNewLine()
70-
{
71-
_endLine = new TextLine(_endLine.End, _endLine.Index + 1);
72-
_lines.Add(_endLine);
73-
}
74-
75-
private void AppendCore(char chr)
76-
{
77-
Debug.Assert(_lines.Count > 0);
78-
_lines[_lines.Count - 1].Content.Append(chr);
75+
lineText = new string(content, lineStart, content.Length - lineStart); // no +1 as content.Length points past the last char already
76+
_lines.Add(new TextLine(lineStart, _lines.Count, lineText));
7977
}
8078

8179
private TextLine FindLine(int absoluteIndex)
8280
{
83-
TextLine selected = null;
81+
TextLine selected;
8482

85-
if (_currentLine == null)
83+
if (_currentLine.IsDefault)
8684
{
8785
// Scan from line 0
8886
selected = ScanLines(absoluteIndex, 0, _lines.Count);
@@ -104,6 +102,10 @@ private TextLine FindLine(int absoluteIndex)
104102
selected = ScanLines(absoluteIndex, _currentLine.Index, _lines.Count);
105103
}
106104
}
105+
else
106+
{
107+
selected = default;
108+
}
107109
}
108110
else if (absoluteIndex < _currentLine.Start)
109111
{
@@ -122,14 +124,18 @@ private TextLine FindLine(int absoluteIndex)
122124
selected = ScanLines(absoluteIndex, 0, _currentLine.Index);
123125
}
124126
}
127+
else
128+
{
129+
selected = default;
130+
}
125131
}
126132
else
127133
{
128134
// This index is on the last read line
129135
selected = _currentLine;
130136
}
131137

132-
Debug.Assert(selected == null || selected.Contains(absoluteIndex));
138+
Debug.Assert(selected.IsDefault || selected.Contains(absoluteIndex));
133139
_currentLine = selected;
134140
return selected;
135141
}
@@ -159,7 +165,7 @@ private TextLine ScanLines(int absoluteIndex, int startLineIndex, int endLineInd
159165
}
160166
}
161167

162-
return null;
168+
return default;
163169
}
164170

165171
internal struct CharacterReference
@@ -175,28 +181,26 @@ public CharacterReference(char character, SourceLocation location)
175181
public SourceLocation Location { get; }
176182
}
177183

178-
private class TextLine
184+
private struct TextLine
179185
{
180-
private StringBuilder _content = new StringBuilder();
181-
182-
public TextLine(int start, int index)
186+
public TextLine(int start, int index, string content)
183187
{
184188
Start = start;
185189
Index = index;
190+
Content = content;
186191
}
187192

188-
public StringBuilder Content
189-
{
190-
get { return _content; }
191-
}
193+
public string Content { get; }
194+
195+
public bool IsDefault => Content == null;
192196

193197
public int Length
194198
{
195199
get { return Content.Length; }
196200
}
197201

198-
public int Start { get; set; }
199-
public int Index { get; set; }
202+
public int Start { get; }
203+
public int Index { get; }
200204

201205
public int End
202206
{

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

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,25 @@ internal abstract class TokenizerBackedParser<TTokenizer> : ParserBase
1616
private readonly TokenizerView<TTokenizer> _tokenizer;
1717
private SyntaxListBuilder<SyntaxToken>? _tokenBuilder;
1818

19-
// Following four high traffic methods cached as using method groups would cause allocation on every invocation.
20-
protected static readonly Func<SyntaxToken, bool> IsSpacingToken = (token) =>
21-
{
22-
return token.Kind == SyntaxKind.Whitespace;
23-
};
24-
25-
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingNewLines = (token) =>
26-
{
27-
return IsSpacingToken(token) || token.Kind == SyntaxKind.NewLine;
28-
};
29-
30-
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingComments = (token) =>
31-
{
32-
return IsSpacingToken(token) || token.Kind == SyntaxKind.CSharpComment;
33-
};
34-
35-
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingNewLinesAndComments = (token) =>
36-
{
37-
return IsSpacingTokenIncludingNewLines(token) || token.Kind == SyntaxKind.CSharpComment;
19+
// Following four high traffic methods cached as using method groups would cause allocation on every invocation.
20+
protected static readonly Func<SyntaxToken, bool> IsSpacingToken = (token) =>
21+
{
22+
return token.Kind == SyntaxKind.Whitespace;
23+
};
24+
25+
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingNewLines = (token) =>
26+
{
27+
return IsSpacingToken(token) || token.Kind == SyntaxKind.NewLine;
28+
};
29+
30+
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingComments = (token) =>
31+
{
32+
return IsSpacingToken(token) || token.Kind == SyntaxKind.CSharpComment;
33+
};
34+
35+
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingNewLinesAndComments = (token) =>
36+
{
37+
return IsSpacingTokenIncludingNewLines(token) || token.Kind == SyntaxKind.CSharpComment;
3838
};
3939

4040
protected TokenizerBackedParser(LanguageCharacteristics<TTokenizer> language, ParserContext context)
@@ -224,7 +224,19 @@ protected internal void PutCurrentBack()
224224

225225
protected internal bool NextIs(SyntaxKind type)
226226
{
227-
return NextIs(token => token != null && type == token.Kind);
227+
// Duplicated logic with NextIs(Func...) to prevent allocation
228+
var cur = CurrentToken;
229+
var result = false;
230+
if (NextToken())
231+
{
232+
result = (type == CurrentToken.Kind);
233+
PutCurrentBack();
234+
}
235+
236+
PutBack(cur);
237+
EnsureCurrent();
238+
239+
return result;
228240
}
229241

230242
protected internal bool NextIs(params SyntaxKind[] types)
@@ -235,21 +247,17 @@ protected internal bool NextIs(params SyntaxKind[] types)
235247
protected internal bool NextIs(Func<SyntaxToken, bool> condition)
236248
{
237249
var cur = CurrentToken;
250+
var result = false;
238251
if (NextToken())
239252
{
240-
var result = condition(CurrentToken);
253+
result = condition(CurrentToken);
241254
PutCurrentBack();
242-
PutBack(cur);
243-
EnsureCurrent();
244-
return result;
245-
}
246-
else
247-
{
248-
PutBack(cur);
249-
EnsureCurrent();
250255
}
251256

252-
return false;
257+
PutBack(cur);
258+
EnsureCurrent();
259+
260+
return result;
253261
}
254262

255263
protected internal bool Was(SyntaxKind type)
@@ -289,11 +297,11 @@ protected bool EnsureCurrent()
289297

290298
protected internal IReadOnlyList<SyntaxToken> ReadWhile(Func<SyntaxToken, bool> condition)
291299
{
292-
if (!EnsureCurrent() || !condition(CurrentToken))
293-
{
294-
return Array.Empty<SyntaxToken>();
295-
}
296-
300+
if (!EnsureCurrent() || !condition(CurrentToken))
301+
{
302+
return Array.Empty<SyntaxToken>();
303+
}
304+
297305
var result = new List<SyntaxToken>();
298306
do
299307
{

0 commit comments

Comments
 (0)