From 1c2e94568552c31d26333180c15392409ee4404e Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 18 Jan 2022 12:57:53 -0800 Subject: [PATCH] Use view buffers during pre-rendering (#39465) * Use view buffers during rendering This change removes about 8kb of string[] allocations per request during pre-rendering and replaces them with a ViewBuffer that uses array pooling. The allocations come from list resizing as part of HtmlRenderer operations (such as https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs#L133-L134). Also includes a couple of other clean up items: * Uses ValueTask instead of `Task` * Moves top-level types to a separate file. * Some formatting cleanup --- ....AspNetCore.Components.Server.Tests.csproj | 3 +- .../test/ComponentTagHelperTest.cs | 2 +- .../PersistComponentStateTagHelperTest.cs | 4 +- .../Infrastructure/ComponentHtmlContent.cs | 29 ------- .../RazorComponents/ComponentRenderedText.cs | 8 +- .../src/RazorComponents/ComponentRenderer.cs | 68 +++++++--------- .../src/RazorComponents/HtmlRenderer.cs | 77 ++++++++++--------- .../src/RazorComponents/IComponentRenderer.cs | 4 +- .../src/RazorComponents/InvokedRenderModes.cs | 31 ++++++++ .../StaticComponentRenderer.cs | 7 +- .../HtmlHelperComponentExtensions.cs | 6 +- .../src/ServerComponentSerializer.cs | 48 ++++-------- .../src/WebAssemblyComponentSerializer.cs | 43 +++-------- .../RazorComponents/ComponentRendererTest.cs | 43 ++++++----- .../test/RazorComponents/HtmlRendererTest.cs | 66 ++++++++++------ .../HtmlHelperComponentExtensionsTest.cs | 4 +- 16 files changed, 210 insertions(+), 233 deletions(-) delete mode 100644 src/Mvc/Mvc.ViewFeatures/src/Infrastructure/ComponentHtmlContent.cs create mode 100644 src/Mvc/Mvc.ViewFeatures/src/RazorComponents/InvokedRenderModes.cs diff --git a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj index 2463622ac4a3..69c2897fdb6d 100644 --- a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj +++ b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -6,6 +6,7 @@ + diff --git a/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs index 41d5c4456703..d4778092498e 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs @@ -75,7 +75,7 @@ private ViewContext GetViewContext() { var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world"); var renderer = Mock.Of(c => - c.RenderComponentAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == Task.FromResult(htmlContent)); + c.RenderComponentAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == new ValueTask(htmlContent)); var httpContext = new DefaultHttpContext { diff --git a/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs index 81b309cdd13f..2df13c0ceb10 100644 --- a/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -180,7 +181,7 @@ private ViewContext GetViewContext() { var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world"); var renderer = Mock.Of(c => - c.RenderComponentAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == Task.FromResult(htmlContent)); + c.RenderComponentAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == new ValueTask(htmlContent)); var httpContext = new DefaultHttpContext { @@ -191,6 +192,7 @@ private ViewContext GetViewContext() .AddSingleton(_ephemeralProvider) .AddSingleton(NullLoggerFactory.Instance) .AddSingleton(HtmlEncoder.Default) + .AddScoped() .BuildServiceProvider(), }; diff --git a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/ComponentHtmlContent.cs b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/ComponentHtmlContent.cs deleted file mode 100644 index be2eca6bb73b..000000000000 --- a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/ComponentHtmlContent.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Linq; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Html; - -namespace Microsoft.AspNetCore.Mvc.ViewFeatures; - -internal class ComponentHtmlContent : IHtmlContent -{ - private readonly IEnumerable _preamble; - private readonly IEnumerable _componentResult; - private readonly IEnumerable _epilogue; - - public ComponentHtmlContent(IEnumerable componentResult) - : this(Array.Empty(), componentResult, Array.Empty()) { } - - public ComponentHtmlContent(IEnumerable preamble, IEnumerable componentResult, IEnumerable epilogue) => - (_preamble, _componentResult, _epilogue) = (preamble, componentResult, epilogue); - - public void WriteTo(TextWriter writer, HtmlEncoder encoder) - { - foreach (var element in _preamble.Concat(_componentResult).Concat(_epilogue)) - { - writer.Write(element); - } - } -} diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderedText.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderedText.cs index 806a58c3ed6c..2f3f3cf7fe71 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderedText.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderedText.cs @@ -1,17 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Html; + namespace Microsoft.AspNetCore.Components.Rendering; internal readonly struct ComponentRenderedText { - public ComponentRenderedText(int componentId, IEnumerable tokens) + public ComponentRenderedText(int componentId, IHtmlContent htmlContent) { ComponentId = componentId; - Tokens = tokens; + HtmlContent = htmlContent; } public int ComponentId { get; } - public IEnumerable Tokens { get; } + public IHtmlContent HtmlContent { get; } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs index 6f143397464f..8ff7c8af3c9f 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs @@ -5,29 +5,30 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; -internal class ComponentRenderer : IComponentRenderer +internal sealed class ComponentRenderer : IComponentRenderer { private static readonly object ComponentSequenceKey = new object(); private static readonly object InvokedRenderModesKey = new object(); private readonly StaticComponentRenderer _staticComponentRenderer; private readonly ServerComponentSerializer _serverComponentSerializer; - private readonly WebAssemblyComponentSerializer _WebAssemblyComponentSerializer; + private readonly IViewBufferScope _viewBufferScope; public ComponentRenderer( StaticComponentRenderer staticComponentRenderer, ServerComponentSerializer serverComponentSerializer, - WebAssemblyComponentSerializer WebAssemblyComponentSerializer) + IViewBufferScope viewBufferScope) { _staticComponentRenderer = staticComponentRenderer; _serverComponentSerializer = serverComponentSerializer; - _WebAssemblyComponentSerializer = WebAssemblyComponentSerializer; + _viewBufferScope = viewBufferScope; } - public async Task RenderComponentAsync( + public async ValueTask RenderComponentAsync( ViewContext viewContext, Type componentType, RenderMode renderMode, @@ -117,14 +118,12 @@ internal static InvokedRenderModes.Mode GetPersistStateRenderMode(ViewContext vi } } - private async Task StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) + private ValueTask StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) { - var result = await _staticComponentRenderer.PrerenderComponentAsync( + return _staticComponentRenderer.PrerenderComponentAsync( parametersCollection, context, type); - - return new ComponentHtmlContent(result); } private async Task PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) @@ -145,13 +144,15 @@ private async Task PrerenderedServerComponentAsync(HttpContext con context, type); - return new ComponentHtmlContent( - ServerComponentSerializer.GetPreamble(currentInvocation), - result, - ServerComponentSerializer.GetEpilogue(currentInvocation)); + var viewBuffer = new ViewBuffer(_viewBufferScope, nameof(ComponentRenderer), ViewBuffer.ViewPageSize); + ServerComponentSerializer.AppendPreamble(viewBuffer, currentInvocation); + viewBuffer.AppendHtml(result); + ServerComponentSerializer.AppendEpilogue(viewBuffer, currentInvocation); + + return viewBuffer; } - private async Task PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) + private async ValueTask PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) { var currentInvocation = WebAssemblyComponentSerializer.SerializeInvocation( type, @@ -163,10 +164,12 @@ private async Task PrerenderedWebAssemblyComponentAsync(HttpContex context, type); - return new ComponentHtmlContent( - WebAssemblyComponentSerializer.GetPreamble(currentInvocation), - result, - WebAssemblyComponentSerializer.GetEpilogue(currentInvocation)); + var viewBuffer = new ViewBuffer(_viewBufferScope, nameof(ComponentRenderer), ViewBuffer.ViewPageSize); + WebAssemblyComponentSerializer.AppendPreamble(viewBuffer, currentInvocation); + viewBuffer.AppendHtml(result); + WebAssemblyComponentSerializer.AppendEpilogue(viewBuffer, currentInvocation); + + return viewBuffer; } private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) @@ -178,31 +181,16 @@ private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerCo var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false); - return new ComponentHtmlContent(ServerComponentSerializer.GetPreamble(currentInvocation)); + var viewBuffer = new ViewBuffer(_viewBufferScope, nameof(ComponentRenderer), ServerComponentSerializer.PreambleBufferSize); + ServerComponentSerializer.AppendPreamble(viewBuffer, currentInvocation); + return viewBuffer; } - private static IHtmlContent NonPrerenderedWebAssemblyComponent(HttpContext context, Type type, ParameterView parametersCollection) + private IHtmlContent NonPrerenderedWebAssemblyComponent(HttpContext context, Type type, ParameterView parametersCollection) { var currentInvocation = WebAssemblyComponentSerializer.SerializeInvocation(type, parametersCollection, prerendered: false); - - return new ComponentHtmlContent(WebAssemblyComponentSerializer.GetPreamble(currentInvocation)); - } -} - -internal class InvokedRenderModes -{ - public InvokedRenderModes(Mode mode) - { - Value = mode; - } - - public Mode Value { get; set; } - - internal enum Mode - { - None, - Server, - WebAssembly, - ServerAndWebAssembly + var viewBuffer = new ViewBuffer(_viewBufferScope, nameof(ComponentRenderer), ServerComponentSerializer.PreambleBufferSize); + WebAssemblyComponentSerializer.AppendPreamble(viewBuffer, currentInvocation); + return viewBuffer; } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs index f629976b7f02..2fa943a3497a 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs @@ -3,27 +3,26 @@ using System.Diagnostics; using System.Runtime.ExceptionServices; -using System.Text.Encodings.Web; using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Rendering; -internal class HtmlRenderer : Renderer +internal sealed class HtmlRenderer : Renderer { private static readonly HashSet SelfClosingElements = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" - }; + { + "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" + }; private static readonly Task CanceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true)); + private readonly IViewBufferScope _viewBufferScope; - private readonly Func _htmlEncoder; - - public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, HtmlEncoder htmlEncoder) + public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IViewBufferScope viewBufferScope) : base(serviceProvider, loggerFactory) { - _htmlEncoder = htmlEncoder.Encode; + _viewBufferScope = viewBufferScope; } public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); @@ -50,10 +49,12 @@ public async Task RenderComponentAsync(Type componentType { var (componentId, frames) = await CreateInitialRenderAsync(componentType, initialParameters); - var context = new HtmlRenderingContext(); + var viewBuffer = new ViewBuffer(_viewBufferScope, nameof(HtmlRenderer), ViewBuffer.ViewPageSize); + + var context = new HtmlRenderingContext { HtmlContentBuilder = viewBuffer, }; var newPosition = RenderFrames(context, frames, 0, frames.Count); Debug.Assert(newPosition == frames.Count); - return new ComponentRenderedText(componentId, context.Result); + return new ComponentRenderedText(componentId, context.HtmlContentBuilder); } public Task RenderComponentAsync(ParameterView initialParameters) where TComponent : IComponent @@ -95,10 +96,10 @@ private int RenderCore( case RenderTreeFrameType.Attribute: throw new InvalidOperationException($"Attributes should only be encountered within {nameof(RenderElement)}"); case RenderTreeFrameType.Text: - context.Result.Add(_htmlEncoder(frame.TextContent)); + context.HtmlContentBuilder.Append(frame.TextContent); return ++position; case RenderTreeFrameType.Markup: - context.Result.Add(frame.MarkupContent); + context.HtmlContentBuilder.AppendHtml(frame.MarkupContent); return ++position; case RenderTreeFrameType.Component: return RenderChildComponent(context, frames, position); @@ -129,9 +130,9 @@ private int RenderElement( int position) { ref var frame = ref frames.Array[position]; - var result = context.Result; - result.Add("<"); - result.Add(frame.ElementName); + var result = context.HtmlContentBuilder; + result.AppendHtml("<"); + result.AppendHtml(frame.ElementName); var afterAttributes = RenderAttributes(context, frames, position + 1, frame.ElementSubtreeLength - 1, out var capturedValueAttribute); // When we see an