Get rid of LineTrackingStringBuffer class and instead use the line information provided by RazorSourceDocument.

This additionally gets rid of an extra whole buffer allocation in the ParserContext. The most complex bit of the change is around avoiding TextLineCollection.GetLocation.

Overall, I'm seeing a pretty big win here, about 35% less time spent in RazorSyntaxTree.Parse for the typing scenario I was doing in a very large file.
This commit is contained in:
Todd Grunke 2020-06-12 17:22:24 -07:00
parent 073cd0aa40
commit a1dd898994
4 changed files with 71 additions and 260 deletions

View File

@ -1,216 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
internal class LineTrackingStringBuffer
{
private readonly IList<TextLine> _lines;
private readonly string _filePath;
private TextLine _currentLine;
public LineTrackingStringBuffer(string content, string filePath)
: this(content.ToCharArray(), filePath)
{
}
public LineTrackingStringBuffer(char[] content, string filePath)
{
_lines = new List<TextLine>();
BuildTextLines(content);
_filePath = filePath;
}
public int Length
{
get { return _lines[_lines.Count - 1].End; }
}
public SourceLocation EndLocation
{
get { return new SourceLocation(_filePath, Length, _lines.Count - 1, _lines[_lines.Count - 1].Length); }
}
public CharacterReference CharAt(int absoluteIndex)
{
var line = FindLine(absoluteIndex);
if (line.IsDefault)
{
throw new ArgumentOutOfRangeException(nameof(absoluteIndex));
}
var idx = absoluteIndex - line.Start;
return new CharacterReference(line.Content[idx], new SourceLocation(_filePath, absoluteIndex, line.Index, idx));
}
private void BuildTextLines(char[] content)
{
string lineText;
var lineStart = 0;
for (int i = 0; i < content.Length; i++)
{
if (ParserHelpers.IsNewLine(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')
{
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;
}
}
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;
if (_currentLine.IsDefault)
{
// Scan from line 0
selected = ScanLines(absoluteIndex, 0, _lines.Count);
}
else if (absoluteIndex >= _currentLine.End)
{
if (_currentLine.Index + 1 < _lines.Count)
{
// This index is after the last read line
var nextLine = _lines[_currentLine.Index + 1];
// Optimization to not search if it's the common case where the line after _currentLine is being requested.
if (nextLine.Contains(absoluteIndex))
{
selected = nextLine;
}
else
{
selected = ScanLines(absoluteIndex, _currentLine.Index, _lines.Count);
}
}
else
{
selected = default;
}
}
else if (absoluteIndex < _currentLine.Start)
{
if (_currentLine.Index > 0)
{
// This index is before the last read line
var prevLine = _lines[_currentLine.Index - 1];
// Optimization to not search if it's the common case where the line before _currentLine is being requested.
if (prevLine.Contains(absoluteIndex))
{
selected = prevLine;
}
else
{
selected = ScanLines(absoluteIndex, 0, _currentLine.Index);
}
}
else
{
selected = default;
}
}
else
{
// This index is on the last read line
selected = _currentLine;
}
Debug.Assert(selected.IsDefault || selected.Contains(absoluteIndex));
_currentLine = selected;
return selected;
}
private TextLine ScanLines(int absoluteIndex, int startLineIndex, int endLineIndex)
{
// binary search for the line containing absoluteIndex
var lowIndex = startLineIndex;
var highIndex = endLineIndex;
while (lowIndex != highIndex)
{
var midIndex = (lowIndex + highIndex) / 2;
var midLine = _lines[midIndex];
if (absoluteIndex >= midLine.End)
{
lowIndex = midIndex + 1;
}
else if (absoluteIndex < midLine.Start)
{
highIndex = midIndex;
}
else
{
return midLine;
}
}
return default;
}
internal struct CharacterReference
{
public CharacterReference(char character, SourceLocation location)
{
Character = character;
Location = location;
}
public char Character { get; }
public SourceLocation Location { get; }
}
private struct TextLine
{
public TextLine(int start, int index, string content)
{
Start = start;
Index = index;
Content = content;
}
public string Content { get; }
public bool IsDefault => Content == null;
public int Length
{
get { return Content.Length; }
}
public int Start { get; }
public int Index { get; }
public int End
{
get { return Start + Length; }
}
public bool Contains(int index)
{
return index < End && index >= Start;
}
}
}
}

View File

@ -18,10 +18,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
SourceDocument = source;
var chars = new char[source.Length];
source.CopyTo(0, chars, 0, source.Length);
Source = new SeekableTextReader(chars, source.FilePath);
Source = new SeekableTextReader(SourceDocument);
DesignTimeMode = options.DesignTime;
FeatureFlags = options.FeatureFlags;
ParseLeadingDirectives = options.ParseLeadingDirectives;

View File

@ -3,32 +3,36 @@
using System;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Razor.Language.Syntax;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
internal class SeekableTextReader : TextReader, ITextDocument
{
private readonly LineTrackingStringBuffer _buffer;
private readonly RazorSourceDocument _sourceDocument;
private int _position = 0;
private int _current;
private SourceLocation _location;
private (TextSpan Span, int LineIndex) _cachedLineInfo;
public SeekableTextReader(string source, string filePath) : this(source.ToCharArray(), filePath) { }
public SeekableTextReader(string source, string filePath) : this(new StringSourceDocument(source, Encoding.UTF8, new RazorSourceDocumentProperties(filePath, relativePath: null))) { }
public SeekableTextReader(char[] source, string filePath)
public SeekableTextReader(RazorSourceDocument source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
_buffer = new LineTrackingStringBuffer(source, filePath);
_sourceDocument = source;
_cachedLineInfo = (new TextSpan(0, _sourceDocument.Lines.GetLineLength(0)), 0);
UpdateState();
}
public SourceLocation Location => _location;
public int Length => _buffer.Length;
public int Length => _sourceDocument.Length;
public int Position
{
@ -55,22 +59,73 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
private void UpdateState()
{
if (_position < _buffer.Length)
if (_cachedLineInfo.Span.Contains(_position))
{
var chr = _buffer.CharAt(_position);
_current = chr.Character;
_location = chr.Location;
_location = new SourceLocation(_sourceDocument.FilePath, _position, _cachedLineInfo.LineIndex, _position - _cachedLineInfo.Span.Start);
_current = _sourceDocument[_location.AbsoluteIndex];
return;
}
else if (_buffer.Length == 0)
if (_position < _sourceDocument.Length)
{
if (_position >= _cachedLineInfo.Span.End)
{
// Try to avoid the GetLocation call by checking if the next line contains the position
int nextLineIndex = _cachedLineInfo.LineIndex + 1;
int nextLineLength = _sourceDocument.Lines.GetLineLength(nextLineIndex);
TextSpan nextLineSpan = new TextSpan(_cachedLineInfo.Span.End, nextLineLength);
if (nextLineSpan.Contains(_position))
{
_cachedLineInfo = (nextLineSpan, nextLineIndex);
_location = new SourceLocation(_sourceDocument.FilePath, _position, nextLineIndex, _position - nextLineSpan.Start);
_current = _sourceDocument[_location.AbsoluteIndex];
return;
}
}
else
{
// Try to avoid the GetLocation call by checking if the previous line contains the position
int prevLineIndex = _cachedLineInfo.LineIndex - 1;
int prevLineLength = _sourceDocument.Lines.GetLineLength(prevLineIndex);
TextSpan prevLineSpan = new TextSpan(_cachedLineInfo.Span.Start - prevLineLength, prevLineLength);
if (prevLineSpan.Contains(_position))
{
_cachedLineInfo = (prevLineSpan, prevLineIndex);
_location = new SourceLocation(_sourceDocument.FilePath, _position, prevLineIndex, _position - prevLineSpan.Start);
_current = _sourceDocument[_location.AbsoluteIndex];
return;
}
}
// The call to GetLocation is expensive
_location = _sourceDocument.Lines.GetLocation(_position);
int lineLength = _sourceDocument.Lines.GetLineLength(_location.LineIndex);
TextSpan lineSpan = new TextSpan(_position - _location.CharacterIndex, lineLength);
_cachedLineInfo = (lineSpan, _location.LineIndex);
_current = _sourceDocument[_location.AbsoluteIndex];
return;
}
if (_sourceDocument.Length == 0)
{
_current = -1;
_location = SourceLocation.Zero;
}
else
{
_current = -1;
_location = _buffer.EndLocation;
return;
}
var lineNumber = _sourceDocument.Lines.Count - 1;
_location = new SourceLocation(_sourceDocument.FilePath, Length, lineNumber, _sourceDocument.Lines.GetLineLength(lineNumber));
_current = -1;
}
}
}

View File

@ -1,26 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
public class LineTrackingStringBufferTest
{
[Fact]
public void CtorInitializesProperties()
{
var buffer = new LineTrackingStringBuffer(string.Empty, "test.cshtml");
Assert.Equal(0, buffer.Length);
}
[Fact]
public void CharAtCorrectlyReturnsLocation()
{
var buffer = new LineTrackingStringBuffer("foo\rbar\nbaz\r\nbiz", "test.cshtml");
var chr = buffer.CharAt(14);
Assert.Equal('i', chr.Character);
Assert.Equal(new SourceLocation("test.cshtml", 14, 3, 1), chr.Location);
}
}
}