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.
This commit is contained in:
Pranav K 2015-09-22 11:29:09 -07:00
parent 8a502dbe5d
commit 6a0a24481a
3 changed files with 57 additions and 8 deletions

View File

@ -15,7 +15,10 @@ namespace Microsoft.AspNet.Mvc
/// </summary>
public class HttpResponseStreamWriter : TextWriter
{
private const int DefaultBufferSize = 1024;
/// <summary>
/// Default buffer size.
/// </summary>
public const int DefaultBufferSize = 1024;
private readonly Stream _stream;
private Encoder _encoder;
private byte[] _byteBuffer;

View File

@ -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();
}
}
}

View File

@ -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<ViewContext>()))
.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<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.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<Stream>();
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<ITempDataDictionary>(),
new HtmlHelperOptions(),
ViewExecutor.DefaultContentType);
// Assert
stream.Verify(s => s.FlushAsync(It.IsAny<CancellationToken>()), Times.Once());
stream.Verify(s => s.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
}
}
}