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:
parent
073cd0aa40
commit
a1dd898994
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue