From a1dd89899438a090742a7adf2465f7ef0c52b2aa Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Fri, 12 Jun 2020 17:22:24 -0700 Subject: [PATCH] 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. --- .../src/Legacy/LineTrackingStringBuffer.cs | 216 ------------------ .../src/Legacy/ParserContext.cs | 4 +- .../src/Legacy/SeekableTextReader.cs | 85 +++++-- .../Legacy/LineTrackingStringBufferTest.cs | 26 --- 4 files changed, 71 insertions(+), 260 deletions(-) delete mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LineTrackingStringBuffer.cs delete mode 100644 src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/LineTrackingStringBufferTest.cs diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LineTrackingStringBuffer.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LineTrackingStringBuffer.cs deleted file mode 100644 index a235bb482b..0000000000 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/LineTrackingStringBuffer.cs +++ /dev/null @@ -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 _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(); - - 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; - } - } - } -} diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/ParserContext.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/ParserContext.cs index e976191f92..5b35faa1d6 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/ParserContext.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/ParserContext.cs @@ -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; diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/SeekableTextReader.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/SeekableTextReader.cs index 85057cf9fb..d50a6efd4a 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/SeekableTextReader.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/SeekableTextReader.cs @@ -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; } } } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/LineTrackingStringBufferTest.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/LineTrackingStringBufferTest.cs deleted file mode 100644 index 27179a9f6d..0000000000 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/LineTrackingStringBufferTest.cs +++ /dev/null @@ -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); - } - } -}