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.
This commit is contained in:
Todd Grunke 2020-06-11 18:11:29 -07:00
parent 3a9368b26b
commit 904fd19605
3 changed files with 93 additions and 78 deletions

View File

@ -554,13 +554,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return Transition(CSharpTokenizerState.Data, EndToken(SyntaxKind.StringLiteral));
}
private StateResult QuotedCharacterLiteral() => QuotedLiteral('\'', SyntaxKind.CharacterLiteral);
private StateResult QuotedCharacterLiteral() => QuotedLiteral('\'', IsEndQuotedCharacterLiteral, SyntaxKind.CharacterLiteral);
private StateResult QuotedStringLiteral() => QuotedLiteral('\"', SyntaxKind.StringLiteral);
private StateResult QuotedStringLiteral() => QuotedLiteral('\"', IsEndQuotedStringLiteral, SyntaxKind.StringLiteral);
private StateResult QuotedLiteral(char quote, SyntaxKind literalType)
private Func<char, bool> IsEndQuotedCharacterLiteral = (c) => c == '\\' || c == '\'' || ParserHelpers.IsNewLine(c);
private Func<char, bool> IsEndQuotedStringLiteral = (c) => c == '\\' || c == '\"' || ParserHelpers.IsNewLine(c);
private StateResult QuotedLiteral(char quote, Func<char, bool> isEndQuotedLiteral, SyntaxKind literalType)
{
TakeUntil(c => c == '\\' || c == quote || ParserHelpers.IsNewLine(c));
TakeUntil(isEndQuotedLiteral);
if (CurrentCharacter == '\\')
{
TakeCurrent(); // Take the '\'

View File

@ -13,7 +13,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
private readonly IList<TextLine> _lines;
private readonly string _filePath;
private TextLine _currentLine;
private TextLine _endLine;
public LineTrackingStringBuffer(string content, string filePath)
: this(content.ToCharArray(), filePath)
@ -22,28 +21,27 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
public LineTrackingStringBuffer(char[] content, string filePath)
{
_endLine = new TextLine(0, 0);
_lines = new List<TextLine>() { _endLine };
_lines = new List<TextLine>();
Append(content);
BuildTextLines(content);
_filePath = filePath;
}
public int Length
{
get { return _endLine.End; }
get { return _lines[_lines.Count - 1].End; }
}
public SourceLocation EndLocation
{
get { return new SourceLocation(_filePath, Length, _lines.Count - 1, _lines[_lines.Count - 1].Length); }
get { return new SourceLocation(_filePath, Length, _lines.Count - 1, Length); }
}
public CharacterReference CharAt(int absoluteIndex)
{
var line = FindLine(absoluteIndex);
if (line == null)
if (line.IsDefault)
{
throw new ArgumentOutOfRangeException(nameof(absoluteIndex));
}
@ -51,38 +49,38 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return new CharacterReference(line.Content[idx], new SourceLocation(_filePath, absoluteIndex, line.Index, idx));
}
private void Append(char[] content)
private void BuildTextLines(char[] content)
{
string lineText;
var lineStart = 0;
for (int i = 0; i < content.Length; i++)
{
AppendCore(content[i]);
// \r on it's own: Start a new line, otherwise wait for \n
// Other Newline: Start a new line
if ((content[i] == '\r' && (i + 1 == content.Length || content[i + 1] != '\n')) || (content[i] != '\r' && ParserHelpers.IsNewLine(content[i])))
if (ParserHelpers.IsNewLine(content[i]))
{
PushNewLine();
// \r on it's own: Start a new line, otherwise wait for \n
// Other Newline: Start a new line
if (content[i] == '\r' && i + 1 < content.Length && content[i + 1] == '\n')
{
i++;
}
lineText = new string(content, lineStart, (i - lineStart) + 1); // +1 to include the current char
_lines.Add(new TextLine(lineStart, _lines.Count, lineText));
lineStart = i + 1;
}
}
}
private void PushNewLine()
{
_endLine = new TextLine(_endLine.End, _endLine.Index + 1);
_lines.Add(_endLine);
}
private void AppendCore(char chr)
{
Debug.Assert(_lines.Count > 0);
_lines[_lines.Count - 1].Content.Append(chr);
lineText = new string(content, lineStart, content.Length - lineStart); // no +1 as content.Length points past the last char already
_lines.Add(new TextLine(lineStart, _lines.Count, lineText));
}
private TextLine FindLine(int absoluteIndex)
{
TextLine selected = null;
TextLine selected;
if (_currentLine == null)
if (_currentLine.IsDefault)
{
// Scan from line 0
selected = ScanLines(absoluteIndex, 0, _lines.Count);
@ -104,6 +102,10 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
selected = ScanLines(absoluteIndex, _currentLine.Index, _lines.Count);
}
}
else
{
selected = default;
}
}
else if (absoluteIndex < _currentLine.Start)
{
@ -122,6 +124,10 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
selected = ScanLines(absoluteIndex, 0, _currentLine.Index);
}
}
else
{
selected = default;
}
}
else
{
@ -129,7 +135,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
selected = _currentLine;
}
Debug.Assert(selected == null || selected.Contains(absoluteIndex));
Debug.Assert(selected.IsDefault || selected.Contains(absoluteIndex));
_currentLine = selected;
return selected;
}
@ -159,7 +165,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
return null;
return default;
}
internal struct CharacterReference
@ -175,28 +181,26 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
public SourceLocation Location { get; }
}
private class TextLine
private struct TextLine
{
private StringBuilder _content = new StringBuilder();
public TextLine(int start, int index)
public TextLine(int start, int index, string content)
{
Start = start;
Index = index;
Content = content;
}
public StringBuilder Content
{
get { return _content; }
}
public string Content { get; }
public bool IsDefault => Content == null;
public int Length
{
get { return Content.Length; }
}
public int Start { get; set; }
public int Index { get; set; }
public int Start { get; }
public int Index { get; }
public int End
{

View File

@ -16,25 +16,25 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
private readonly TokenizerView<TTokenizer> _tokenizer;
private SyntaxListBuilder<SyntaxToken>? _tokenBuilder;
// Following four high traffic methods cached as using method groups would cause allocation on every invocation.
protected static readonly Func<SyntaxToken, bool> IsSpacingToken = (token) =>
{
return token.Kind == SyntaxKind.Whitespace;
};
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingNewLines = (token) =>
{
return IsSpacingToken(token) || token.Kind == SyntaxKind.NewLine;
};
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingComments = (token) =>
{
return IsSpacingToken(token) || token.Kind == SyntaxKind.CSharpComment;
};
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingNewLinesAndComments = (token) =>
{
return IsSpacingTokenIncludingNewLines(token) || token.Kind == SyntaxKind.CSharpComment;
// Following four high traffic methods cached as using method groups would cause allocation on every invocation.
protected static readonly Func<SyntaxToken, bool> IsSpacingToken = (token) =>
{
return token.Kind == SyntaxKind.Whitespace;
};
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingNewLines = (token) =>
{
return IsSpacingToken(token) || token.Kind == SyntaxKind.NewLine;
};
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingComments = (token) =>
{
return IsSpacingToken(token) || token.Kind == SyntaxKind.CSharpComment;
};
protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingNewLinesAndComments = (token) =>
{
return IsSpacingTokenIncludingNewLines(token) || token.Kind == SyntaxKind.CSharpComment;
};
protected TokenizerBackedParser(LanguageCharacteristics<TTokenizer> language, ParserContext context)
@ -224,7 +224,19 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
protected internal bool NextIs(SyntaxKind type)
{
return NextIs(token => token != null && type == token.Kind);
// Duplicated logic with NextIs(Func...) to prevent allocation
var cur = CurrentToken;
var result = false;
if (NextToken())
{
result = (type == CurrentToken.Kind);
PutCurrentBack();
}
PutBack(cur);
EnsureCurrent();
return result;
}
protected internal bool NextIs(params SyntaxKind[] types)
@ -235,21 +247,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
protected internal bool NextIs(Func<SyntaxToken, bool> condition)
{
var cur = CurrentToken;
var result = false;
if (NextToken())
{
var result = condition(CurrentToken);
result = condition(CurrentToken);
PutCurrentBack();
PutBack(cur);
EnsureCurrent();
return result;
}
else
{
PutBack(cur);
EnsureCurrent();
}
return false;
PutBack(cur);
EnsureCurrent();
return result;
}
protected internal bool Was(SyntaxKind type)
@ -289,11 +297,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
protected internal IReadOnlyList<SyntaxToken> ReadWhile(Func<SyntaxToken, bool> condition)
{
if (!EnsureCurrent() || !condition(CurrentToken))
{
return Array.Empty<SyntaxToken>();
}
if (!EnsureCurrent() || !condition(CurrentToken))
{
return Array.Empty<SyntaxToken>();
}
var result = new List<SyntaxToken>();
do
{