Skip to content

Commit

Permalink
585: Optimize Razor output rendering to avoid LOH allocations
Browse files Browse the repository at this point in the history
Instead of creating a string from the StringWriter (_tempWriter)
we will copy the underlying buffer in chunks to the output text buffer.
For large pages this both reduces the total allocations to 1KB as well
as avoid getting into the large object heap.
  • Loading branch information
Yishai Galatzer committed Jul 14, 2014
1 parent a3dcd16 commit 3fe0d34
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 9 deletions.
35 changes: 35 additions & 0 deletions src/System.Web.WebPages/StringWriterExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.IO;
using System.Text;

namespace System.Web.WebPages
{
internal static class StringWriterExtensions
{
public const int BufferSize = 1024;

// Used to copy data from a string writer to avoid allocating the full string
// which can end up on LOH (and cause memory fragmentation).
public static void CopyTo(this StringWriter input, TextWriter output)
{
StringBuilder builder = input.GetStringBuilder();

int remainingChars = builder.Length;
int bufferSize = Math.Min(builder.Length, BufferSize);

char[] buffer = new char[bufferSize];
int currentPosition = 0;

while (remainingChars > 0)
{
int copyLen = Math.Min(bufferSize, remainingChars);

builder.CopyTo(currentPosition, buffer, 0, copyLen);

output.Write(buffer, 0, copyLen);

currentPosition += copyLen;
remainingChars -= copyLen;
}
}
}
}
1 change: 1 addition & 0 deletions src/System.Web.WebPages/System.Web.WebPages.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
<Compile Include="Mvc\ModelClientValidationStringLengthRule.cs" />
<Compile Include="Mvc\UnobtrusiveValidationAttributesGenerator.cs" />
<Compile Include="RequestBrowserOverrideStore.cs" />
<Compile Include="StringWriterExtensions.cs" />
<Compile Include="Utils\HtmlAttributePropertyHelper.cs" />
<Compile Include="..\Common\PropertyHelper.cs" />
<Compile Include="Utils\SessionStateUtil.cs" />
Expand Down
18 changes: 9 additions & 9 deletions src/System.Web.WebPages/WebPageBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,17 @@

namespace System.Web.WebPages
{
// TODO(elipton): Clean this up and remove the suppression
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "This is temporary (elipton)")]
public abstract class WebPageBase : WebPageRenderingBase
{
// Keep track of which sections RenderSection has already been called on
private HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Keep track of whether RenderBody has been called
private bool _renderedBody = false;
// Action for rendering the body within a layout page
private Action<TextWriter> _body;

// TODO(elipton): Figure out if we still need these two writers
private TextWriter _tempWriter;
private StringWriter _tempWriter;
private TextWriter _currentWriter;

private DynamicPageDataDictionary<dynamic> _dynamicPageData;
Expand Down Expand Up @@ -267,23 +265,25 @@ public bool IsSectionDefined(string name)

public void PopContext()
{
string renderedContent = _tempWriter.ToString();
// Using the CopyTo extension method on the _tempWriter instead of .ToString()
// to avoid allocating large strings that then end up on the Large object heap.
OutputStack.Pop();

if (!String.IsNullOrEmpty(Layout))
{
string layoutPagePath = NormalizeLayoutPagePath(Layout);
// If a layout file was specified, render it passing our page content

// If a layout file was specified, render it passing our page content.
OutputStack.Push(_currentWriter);
RenderSurrounding(
layoutPagePath,
w => w.Write(renderedContent));
_tempWriter.CopyTo);
OutputStack.Pop();
}
else
{
// Otherwise, just render the page
_currentWriter.Write(renderedContent);
// Otherwise, just render the page.
_tempWriter.CopyTo(_currentWriter);
}

VerifyRenderedBodyOrSections();
Expand Down
113 changes: 113 additions & 0 deletions test/System.Web.WebPages.Test/Extensions/StringWriterExtensionsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.TestCommon;
using Moq;

namespace System.Web.WebPages.Test
{
public class StringWriterExtensionsTest
{
[Fact]
public void CopiesResult()
{
// Note that a preable is not expected on the generated stream.
string text = "Hello world";
Byte[] textInBytes = Encoding.UTF8.GetBytes(text);
string outputText;

Byte[] buffer = new Byte[1024];

using (MemoryStream stream = new MemoryStream(buffer))
using (StringWriter writer = new StringWriter())
using (StreamWriter outputWriter = new StreamWriter(stream))
{
writer.Write(text);
writer.CopyTo(outputWriter);

outputText = writer.ToString();
}

Assert.Equal(text, outputText, StringComparer.Ordinal);

for (int i = 0; i < textInBytes.Length; i++)
{
Assert.Equal(textInBytes[i], buffer[i]);
}
}

[Theory]
[InlineData(1)]
[InlineData(1023)]
[InlineData(1024)]
[InlineData(1025)]
[InlineData(20000)]
[InlineData(100000)]
public void OnlyUsesBufferUpToSize(int count)
{
string text = new string('a', count);
Byte[] textInBytes = Encoding.UTF8.GetBytes(text);

Mock<StreamWriter> mock;

Byte[] buffer = new Byte[textInBytes.Length + 100];

using (MemoryStream stream = new MemoryStream(buffer))
{
StringWriter writer = new StringWriter();

mock = new Mock<StreamWriter>(MockBehavior.Strict, stream) { CallBase = true };
mock.Setup(sw
=> sw.Write(It.IsAny<char[]>(),
It.IsAny<int>(),
It.Is<int>(c => c == StringWriterExtensions.BufferSize ||
c == textInBytes.Length % StringWriterExtensions.BufferSize))).
Verifiable();

StreamWriter outputWriter = mock.Object;
writer.Write(text);
writer.CopyTo(outputWriter);

mock.Verify();
}
}

[Theory]
[InlineData(1)]
[InlineData(1023/7)]
[InlineData(1024/7)]
[InlineData(1025/7)]
[InlineData(20000/7)]
[InlineData(100000/7)]
public void ProperlyCopiesLargeSetsOfText(int count)
{
// The char א turns into a two byte sequence so we end up with a
// 7 byte sequence that is not a divider or 1024.
string text = string.Join(string.Empty, Enumerable.Repeat("abcdeא", count));

Byte[] textInBytes = Encoding.UTF8.GetBytes(text);
string outputText;

Byte[] buffer = new Byte[textInBytes.Length + 100];

using (MemoryStream stream = new MemoryStream(buffer))
using (StringWriter writer = new StringWriter())
{
using (StreamWriter outputWriter = new StreamWriter(stream))
{
writer.Write(text);
writer.CopyTo(outputWriter);

outputText = writer.ToString();
}
}

Assert.Equal(text, outputText, StringComparer.Ordinal);

for (int i = 0; i < textInBytes.Length; i++)
{
Assert.Equal(textInBytes[i], buffer[i]);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<Compile Include="ApplicationParts\MimeMappingTest.cs" />
<Compile Include="ApplicationParts\ResourceHandlerTest.cs" />
<Compile Include="ApplicationParts\TestResourceAssembly.cs" />
<Compile Include="Extensions\StringWriterExtensionsTest.cs" />
<Compile Include="Helpers\AntiXsrf\AntiForgeryTokenStoreTest.cs" />
<Compile Include="Helpers\AntiXsrf\MachineKey45CryptoSystemTest.cs" />
<Compile Include="Helpers\AntiXsrf\MockableTokenStore.cs" />
Expand Down

0 comments on commit 3fe0d34

Please sign in to comment.