From 18909506e1690779056ac7de1da3d9492adf9011 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 21 Jan 2016 17:29:35 -0800 Subject: [PATCH] Flatten TagHelperOutput when writing This change 'flattens' a TagHelperOutput when writing it out to an HtmlTextWriter. This is a beneficial perf change because more often than not the thing being written to is a ViewBuffer in Razor, which uses pooled memory. This allows us to 'join' islands of pooled ViewBuffer memory back into the main buffer instead of keeping them wrapped up in a TagHelperOutput or TagHelperContent. --- .../TagHelpers/TagHelperOutput.cs | 11 ++-- .../TagHelpers/TagHelperOutputTest.cs | 54 +++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs index 8eaa47bfe6..e1e5bea4ac 100644 --- a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs +++ b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs @@ -283,14 +283,14 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers if (!isTagNameNullOrWhitespace) { - writer.Write('<'); + writer.Write("<"); writer.Write(TagName); // Perf: Avoid allocating enumerator for (var i = 0; i < Attributes.Count; i++) { var attribute = Attributes[i]; - writer.Write(' '); + writer.Write(" "); writer.Write(attribute.Name); if (attribute.Minimized) @@ -309,10 +309,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers using (var stringWriter = new StringWriter()) { htmlContent.WriteTo(stringWriter, encoder); + stringWriter.GetStringBuilder().Replace("\"", """); var stringValue = stringWriter.ToString(); - stringValue = stringValue.Replace("\"", """); - writer.Write(stringValue); } } @@ -321,7 +320,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers encoder.Encode(writer, value.ToString()); } - writer.Write('"'); + writer.Write("\""); } if (TagMode == TagMode.SelfClosing) @@ -329,7 +328,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers writer.Write(" /"); } - writer.Write('>'); + writer.Write(">"); } if (isTagNameNullOrWhitespace || TagMode == TagMode.StartTagAndEndTag) diff --git a/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs b/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs index aacd53708d..02d02a5012 100644 --- a/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Razor.Runtime.TagHelpers; using Microsoft.Extensions.WebEncoders.Testing; using Xunit; @@ -990,6 +992,34 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers Assert.Equal(expected, writer.ToString(), StringComparer.Ordinal); } + // This tests a separate code path that's used by THO when the writer is an HtmlTextWriter. + // The output should be the same, but we do some specific perf optimizations on this path. + [Theory] + [MemberData(nameof(WriteTagHelper_InputData))] + public void WriteTo_WritesFormattedTagHelper_HtmlTextWriter(TagHelperOutput output, string expected) + { + // Arrange + var inner = new StringWriter(); + var testEncoder = new HtmlTestEncoder(); + var writer = new MockHtmlTextWriter(inner, testEncoder); + + var tagHelperExecutionContext = new TagHelperExecutionContext( + tagName: output.TagName, + tagMode: output.TagMode, + items: new Dictionary(), + uniqueId: string.Empty, + executeChildContentAsync: () => Task.FromResult(result: true), + startTagHelperWritingScope: _ => { }, + endTagHelperWritingScope: () => new DefaultTagHelperContent()); + tagHelperExecutionContext.Output = output; + + // Act + output.WriteTo(writer, testEncoder); + + // Assert + Assert.Equal(expected, inner.ToString(), StringComparer.Ordinal); + } + private static TagHelperOutput GetTagHelperOutput( string tagName, TagHelperAttributeList attributes, @@ -1036,5 +1066,29 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers return output; } + + private class MockHtmlTextWriter : HtmlTextWriter + { + private readonly HtmlEncoder _encoder; + private readonly TextWriter _inner; + + public MockHtmlTextWriter(TextWriter inner, HtmlEncoder encoder) + { + _inner = inner; + _encoder = encoder; + } + + public override Encoding Encoding => _inner.Encoding; + + public override void Write(IHtmlContent value) + { + value.WriteTo(_inner, _encoder); + } + + public override void Write(char value) + { + _inner.Write(value); + } + } } } \ No newline at end of file