Fix for Razor #84 - Optimize GetSourceLocation

GetSourceLocation is frequently called to determine the location mappings
between the original document and the generated code.

The old implementation did a number of ToString and replace operations to
simplify the math on tracking the position - which put it front and center
in our performance measurements - about 25% of all execution time in a
sampling profile of our perf test.

The new code tracks position as code is written, and avoids allocations.
After these changes GetSourceLocation doesn't show up in the profile.
This commit is contained in:
Ryan Nowak 2014-08-18 10:29:04 -07:00
parent 896cce5b51
commit ff944e5948
2 changed files with 347 additions and 11 deletions

View File

@ -9,11 +9,17 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler
{
public class CodeWriter : IDisposable
{
private static readonly char[] NewLineCharacters = new char[] { '\r', '\n' };
private StringWriter _writer = new StringWriter();
private bool _newLine;
private string _cache = string.Empty;
private bool _dirty = false;
private int _absoluteIndex;
private int _currentLineIndex;
private int _currentLineCharacterIndex;
public string LastWrite { get; private set; }
public int CurrentIndent { get; private set; }
@ -49,6 +55,10 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler
{
_writer.Write(new string(' ', size));
Flush();
_currentLineCharacterIndex += size;
_absoluteIndex += size;
_dirty = true;
_newLine = false;
}
@ -61,24 +71,90 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler
Indent(CurrentIndent);
_writer.Write(data);
Flush();
LastWrite = data;
_dirty = true;
_newLine = false;
if (data == null || data.Length == 0)
{
return this;
}
_absoluteIndex += data.Length;
// The data string might contain a partial newline where the previously
// written string has part of the newline.
var i = 0;
int? trailingPartStart = null;
var builder = _writer.GetStringBuilder();
if (
// Check the last character of the previous write operation.
builder.Length - data.Length - 1 >= 0 &&
builder[builder.Length - data.Length - 1] == '\r' &&
// Check the first character of the current write operation.
builder[builder.Length - data.Length] == '\n')
{
// This is newline that's spread across two writes. Skip the first character of the
// current write operation.
//
// We don't need to increment our newline counter because we already did that when we
// saw the \r.
i += 1;
trailingPartStart = 1;
}
// Iterate the string, stopping at each occurrence of a newline character. This lets us count the
// newline occurrences and keep the index of the last one.
while ((i = data.IndexOfAny(NewLineCharacters, i)) >= 0)
{
// Newline found.
_currentLineIndex++;
_currentLineCharacterIndex = 0;
i++;
// We might have stopped at a \r, so check if it's followed by \n and then advance the index to
// start the next search after it.
if (data.Length > i &&
data[i - 1] == '\r' &&
data[i] == '\n')
{
i++;
}
// The 'suffix' of the current line starts after this newline token.
trailingPartStart = i;
}
if (trailingPartStart == null)
{
// No newlines, just add the length of the data buffer
_currentLineCharacterIndex += data.Length;
}
else
{
// Newlines found, add the trailing part of 'data'
_currentLineCharacterIndex += (data.Length - trailingPartStart.Value);
}
return this;
}
public CodeWriter WriteLine()
{
LastWrite = Environment.NewLine;
LastWrite = _writer.NewLine;
_writer.WriteLine();
Flush();
_currentLineIndex++;
_currentLineCharacterIndex = 0;
_absoluteIndex += _writer.NewLine.Length;
_dirty = true;
_newLine = true;
@ -110,18 +186,12 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler
public SourceLocation GetCurrentSourceLocation()
{
string output = GenerateCode();
string unescapedOutput = output.Replace("\\r", String.Empty).Replace("\\n", String.Empty);
return new SourceLocation(
absoluteIndex: output.Length,
lineIndex: (unescapedOutput.Length - unescapedOutput.Replace(Environment.NewLine, String.Empty).Length) / Environment.NewLine.Length,
characterIndex: unescapedOutput.Length - (unescapedOutput.LastIndexOf(Environment.NewLine) + Environment.NewLine.Length));
return new SourceLocation(_absoluteIndex, _currentLineIndex, _currentLineCharacterIndex);
}
protected virtual void Dispose(bool disposing)
{
if(disposing)
if (disposing)
{
_writer.Dispose();
}

View File

@ -0,0 +1,266 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Razor.Text;
using Xunit;
using System.Collections.Generic;
namespace Microsoft.AspNet.Razor.Generator.Compiler
{
public class CodeWriterTest
{
// The length of the newline string written by writer.WriteLine.
private static readonly int WriterNewLineLength = Environment.NewLine.Length;
public static IEnumerable<object[]> NewLines
{
get
{
return new object[][]
{
new object[] { "\r" },
new object[] { "\n" },
new object[] { "\r\n" },
};
}
}
[Fact]
public void CodeWriter_TracksPosition_WithWrite()
{
// Arrange
var writer = new CodeWriter();
// Act
writer.Write("1234");
// Assert
var location = writer.GetCurrentSourceLocation();
var expected = new SourceLocation(absoluteIndex: 4, lineIndex: 0, characterIndex: 4);
Assert.Equal(expected, location);
}
[Fact]
public void CodeWriter_TracksPosition_WithIndent()
{
// Arrange
var writer = new CodeWriter();
// Act
writer.WriteLine();
writer.Indent(size: 3);
// Assert
var location = writer.GetCurrentSourceLocation();
var expected = new SourceLocation(absoluteIndex: 3 + WriterNewLineLength, lineIndex: 1, characterIndex: 3);
Assert.Equal(expected, location);
}
[Fact]
public void CodeWriter_TracksPosition_WithWriteLine()
{
// Arrange
var writer = new CodeWriter();
// Act
writer.WriteLine("1234");
// Assert
var location = writer.GetCurrentSourceLocation();
var expected = new SourceLocation(absoluteIndex: 4 + WriterNewLineLength, lineIndex: 1, characterIndex: 0);
Assert.Equal(expected, location);
}
[Theory]
[MemberData("NewLines")]
public void CodeWriter_TracksPosition_WithWriteLine_WithNewLineInContent(string newLine)
{
// Arrange
var writer = new CodeWriter();
// Act
writer.WriteLine("1234" + newLine + "12");
// Assert
var location = writer.GetCurrentSourceLocation();
var expected = new SourceLocation(
absoluteIndex: 6 + newLine.Length + WriterNewLineLength,
lineIndex: 2,
characterIndex: 0);
Assert.Equal(expected, location);
}
[Theory]
[MemberData("NewLines")]
public void CodeWriter_TracksPosition_WithWrite_WithNewlineInContent(string newLine)
{
// Arrange
var writer = new CodeWriter();
// Act
writer.Write("1234" + newLine + "123" + newLine + "12");
// Assert
var location = writer.GetCurrentSourceLocation();
var expected = new SourceLocation(
absoluteIndex: 9 + newLine.Length + newLine.Length,
lineIndex: 2,
characterIndex: 2);
Assert.Equal(expected, location);
}
[Fact]
public void CodeWriter_TracksPosition_WithWrite_WithNewlineInContent_RepeatedN()
{
// Arrange
var writer = new CodeWriter();
// Act
writer.Write("1234\n\n123");
// Assert
var location = writer.GetCurrentSourceLocation();
var expected = new SourceLocation(
absoluteIndex: 9,
lineIndex: 2,
characterIndex: 3);
Assert.Equal(expected, location);
}
[Fact]
public void CodeWriter_TracksPosition_WithWrite_WithMixedNewlineInContent()
{
// Arrange
var writer = new CodeWriter();
// Act
writer.Write("1234\r123\r\n12\n1");
// Assert
var location = writer.GetCurrentSourceLocation();
var expected = new SourceLocation(
absoluteIndex: 14,
lineIndex: 3,
characterIndex: 1);
Assert.Equal(expected, location);
}
[Fact]
public void CodeWriter_TracksPosition_WithNewline_SplitAcrossWrites()
{
// Arrange
var writer = new CodeWriter();
// Act
writer.Write("1234\r");
var location1 = writer.GetCurrentSourceLocation();
writer.Write("\n");
var location2 = writer.GetCurrentSourceLocation();
// Assert
var expected1 = new SourceLocation(absoluteIndex: 5, lineIndex: 1, characterIndex: 0);
Assert.Equal(expected1, location1);
var expected2 = new SourceLocation(absoluteIndex: 6, lineIndex: 1, characterIndex: 0);
Assert.Equal(expected2, location2);
}
[Fact]
public void CodeWriter_TracksPosition_WithTwoNewline_SplitAcrossWrites_R()
{
// Arrange
var writer = new CodeWriter();
// Act
writer.Write("1234\r");
var location1 = writer.GetCurrentSourceLocation();
writer.Write("\r");
var location2 = writer.GetCurrentSourceLocation();
// Assert
var expected1 = new SourceLocation(absoluteIndex: 5, lineIndex: 1, characterIndex: 0);
Assert.Equal(expected1, location1);
var expected2 = new SourceLocation(absoluteIndex: 6, lineIndex: 2, characterIndex: 0);
Assert.Equal(expected2, location2);
}
[Fact]
public void CodeWriter_TracksPosition_WithTwoNewline_SplitAcrossWrites_N()
{
// Arrange
var writer = new CodeWriter();
// Act
writer.Write("1234\n");
var location1 = writer.GetCurrentSourceLocation();
writer.Write("\n");
var location2 = writer.GetCurrentSourceLocation();
// Assert
var expected1 = new SourceLocation(absoluteIndex: 5, lineIndex: 1, characterIndex: 0);
Assert.Equal(expected1, location1);
var expected2 = new SourceLocation(absoluteIndex: 6, lineIndex: 2, characterIndex: 0);
Assert.Equal(expected2, location2);
}
[Fact]
public void CodeWriter_TracksPosition_WithTwoNewline_SplitAcrossWrites_Reversed()
{
// Arrange
var writer = new CodeWriter();
// Act
writer.Write("1234\n");
var location1 = writer.GetCurrentSourceLocation();
writer.Write("\r");
var location2 = writer.GetCurrentSourceLocation();
// Assert
var expected1 = new SourceLocation(absoluteIndex: 5, lineIndex: 1, characterIndex: 0);
Assert.Equal(expected1, location1);
var expected2 = new SourceLocation(absoluteIndex: 6, lineIndex: 2, characterIndex: 0);
Assert.Equal(expected2, location2);
}
[Fact]
public void CodeWriter_TracksPosition_WithNewline_SplitAcrossWrites_AtBeginning()
{
// Arrange
var writer = new CodeWriter();
// Act
writer.Write("\r");
var location1 = writer.GetCurrentSourceLocation();
writer.Write("\n");
var location2 = writer.GetCurrentSourceLocation();
// Assert
var expected1 = new SourceLocation(absoluteIndex: 1, lineIndex: 1, characterIndex: 0);
Assert.Equal(expected1, location1);
var expected2 = new SourceLocation(absoluteIndex: 2, lineIndex: 1, characterIndex: 0);
Assert.Equal(expected2, location2);
}
}
}