From 6a0a24481a07fc34fd44bd410bfa57d7fe9d1c61 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 22 Sep 2015 11:29:09 -0700 Subject: [PATCH] Asynchronously flush the HttpResponseStreamWriter after a view has been rendered. This solves a perf issue for views which produce content that is smaller than the buffer size of HttpResponseStreamWriter. In this case, the writer ends up synchronously writing to the Response as part of Dispose which affects perf. --- .../HttpResponseStreamWriter.cs | 5 +- .../ViewFeatures/ViewExecutor.cs | 4 ++ .../ViewFeatures/ViewExecutorTest.cs | 56 ++++++++++++++++--- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs b/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs index 25b197d48d..c195bbb289 100644 --- a/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs @@ -15,7 +15,10 @@ namespace Microsoft.AspNet.Mvc /// public class HttpResponseStreamWriter : TextWriter { - private const int DefaultBufferSize = 1024; + /// + /// Default buffer size. + /// + public const int DefaultBufferSize = 1024; private readonly Stream _stream; private Encoder _encoder; private byte[] _byteBuffer; diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs index 6b0536d9e4..43036f9765 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs @@ -62,6 +62,10 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures htmlHelperOptions); await view.RenderAsync(viewContext); + // Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying + // response. In the absence of this line, the buffer gets synchronously written to the response + // as part of the Dispose which has a perf impact. + await writer.FlushAsync(); } } } diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs index 5fec6b2aa0..3dec9fd8f2 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.Abstractions; @@ -89,7 +90,6 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures view.Setup(v => v.RenderAsync(It.IsAny())) .Callback((ViewContext v) => { - view.ToString(); v.Writer.Write("abcd"); }) .Returns(Task.FromResult(0)); @@ -99,9 +99,10 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures context.Response.Body = memoryStream; context.Response.ContentType = responseContentType; - var actionContext = new ActionContext(context, - new RouteData(), - new ActionDescriptor()); + var actionContext = new ActionContext( + context, + new RouteData(), + new ActionDescriptor()); var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); // Act @@ -135,9 +136,10 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures var memoryStream = new MemoryStream(); context.Response.Body = memoryStream; - var actionContext = new ActionContext(context, - new RouteData(), - new ActionDescriptor()); + var actionContext = new ActionContext( + context, + new RouteData(), + new ActionDescriptor()); var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); // Act @@ -153,5 +155,45 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures // Assert Assert.Equal(expectedLength, memoryStream.Length); } + + [Theory] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize - 1)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1)] + [InlineData(2 * HttpResponseStreamWriter.DefaultBufferSize + 4)] + public async Task ExecuteAsync_AsynchronouslyFlushesToTheResponseStream_PriorToDispose(int writeLength) + { + // Arrange + var view = new Mock(); + view.Setup(v => v.RenderAsync(It.IsAny())) + .Returns((ViewContext v) => + Task.Run(async () => + { + var text = new string('a', writeLength); + await v.Writer.WriteAsync(text); + })); + + var context = new DefaultHttpContext(); + var stream = new Mock(); + context.Response.Body = stream.Object; + + var actionContext = new ActionContext( + context, + new RouteData(), + new ActionDescriptor()); + var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); + + // Act + await ViewExecutor.ExecuteAsync( + view.Object, + actionContext, + viewData, + Mock.Of(), + new HtmlHelperOptions(), + ViewExecutor.DefaultContentType); + + // Assert + stream.Verify(s => s.FlushAsync(It.IsAny()), Times.Once()); + stream.Verify(s => s.Write(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + } } } \ No newline at end of file