From ff944e59481a7dbf7cea5f009b014ad12c56a02a Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 18 Aug 2014 10:29:04 -0700 Subject: [PATCH] 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. --- .../Compiler/CodeBuilder/CodeWriter.cs | 92 +++++- .../Generator/Compiler/CodeWriterTest.cs | 266 ++++++++++++++++++ 2 files changed, 347 insertions(+), 11 deletions(-) create mode 100644 test/Microsoft.AspNet.Razor.Test/Generator/Compiler/CodeWriterTest.cs diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeWriter.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeWriter.cs index 07c64a1136..66b49a52be 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeWriter.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeWriter.cs @@ -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(); } diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/Compiler/CodeWriterTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/Compiler/CodeWriterTest.cs new file mode 100644 index 0000000000..276e0172b4 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/Generator/Compiler/CodeWriterTest.cs @@ -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 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); + } + } +} \ No newline at end of file