Use pooled memory for the streamwriter

This commit is contained in:
Ryan Nowak 2015-09-13 16:31:52 -07:00
parent 306776ff63
commit 4e08eda58d
17 changed files with 426 additions and 21 deletions

View File

@ -16,6 +16,7 @@ using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection.Extensions;
using Microsoft.Framework.MemoryPool;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.Framework.DependencyInjection
@ -137,6 +138,9 @@ namespace Microsoft.Framework.DependencyInjection
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.TryAddSingleton<IActionBindingContextAccessor, ActionBindingContextAccessor>();
services.TryAddSingleton<IUrlHelper, UrlHelper>();
services.TryAddSingleton<IHttpResponseStreamWriterFactory, MemoryPoolHttpResponseStreamWriterFactory>();
services.TryAddSingleton<IArraySegmentPool<byte>, DefaultArraySegmentPool<byte>>();
services.TryAddSingleton<IArraySegmentPool<char>, DefaultArraySegmentPool<char>>();
}
private static void ConfigureDefaultServices(IServiceCollection services)

View File

@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Framework.MemoryPool;
namespace Microsoft.AspNet.Mvc
{
@ -18,10 +19,13 @@ namespace Microsoft.AspNet.Mvc
/// Default buffer size.
/// </summary>
public const int DefaultBufferSize = 1024;
private readonly Stream _stream;
private Encoder _encoder;
private byte[] _byteBuffer;
private char[] _charBuffer;
private LeasedArraySegment<byte> _leasedByteBuffer;
private LeasedArraySegment<char> _leasedCharBuffer;
private ArraySegment<byte> _byteBuffer;
private ArraySegment<char> _charBuffer;
private int _charBufferSize;
private int _charBufferCount;
@ -44,10 +48,54 @@ namespace Microsoft.AspNet.Mvc
_stream = stream;
Encoding = encoding;
_encoder = encoding.GetEncoder();
_charBufferSize = bufferSize;
_charBuffer = new char[bufferSize];
_byteBuffer = new byte[encoding.GetMaxByteCount(bufferSize)];
_encoder = encoding.GetEncoder();
_byteBuffer = new ArraySegment<byte>(new byte[encoding.GetMaxByteCount(bufferSize)]);
_charBuffer = new ArraySegment<char>(new char[bufferSize]);
}
public HttpResponseStreamWriter(
Stream stream,
Encoding encoding,
LeasedArraySegment<byte> leasedByteBuffer,
LeasedArraySegment<char> leasedCharBuffer)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
if (encoding == null)
{
throw new ArgumentNullException(nameof(encoding));
}
if (leasedByteBuffer == null)
{
throw new ArgumentNullException(nameof(leasedByteBuffer));
}
if (leasedCharBuffer == null)
{
throw new ArgumentNullException(nameof(leasedCharBuffer));
}
_stream = stream;
Encoding = encoding;
_leasedByteBuffer = leasedByteBuffer;
_leasedCharBuffer = leasedCharBuffer;
_encoder = encoding.GetEncoder();
_byteBuffer = leasedByteBuffer.Data;
_charBuffer = leasedCharBuffer.Data;
// We need to compute the usable size of the char buffer based on the size of the byte buffer.
// Encoder.GetBytes assumes that the entirety of the byte[] passed in can be used, and that's not the
// case with ArraySegments.
_charBufferSize = Math.Min(
leasedCharBuffer.Data.Count,
encoding.GetMaxCharCount(leasedByteBuffer.Data.Count));
}
public override Encoding Encoding { get; }
@ -59,7 +107,7 @@ namespace Microsoft.AspNet.Mvc
FlushInternal();
}
_charBuffer[_charBufferCount] = value;
_charBuffer.Array[_charBuffer.Offset + _charBufferCount] = value;
_charBufferCount++;
}
@ -108,7 +156,7 @@ namespace Microsoft.AspNet.Mvc
await FlushInternalAsync();
}
_charBuffer[_charBufferCount] = value;
_charBuffer.Array[_charBuffer.Offset + _charBufferCount] = value;
_charBufferCount++;
}
@ -168,6 +216,16 @@ namespace Microsoft.AspNet.Mvc
protected override void Dispose(bool disposing)
{
FlushInternal(flushStream: false, flushEncoder: true);
if (_leasedByteBuffer != null)
{
_leasedByteBuffer.Owner.Return(_leasedByteBuffer);
}
if (_leasedCharBuffer != null)
{
_leasedCharBuffer.Owner.Return(_leasedCharBuffer);
}
}
private void FlushInternal(bool flushStream = false, bool flushEncoder = false)
@ -177,10 +235,17 @@ namespace Microsoft.AspNet.Mvc
return;
}
var count = _encoder.GetBytes(_charBuffer, 0, _charBufferCount, _byteBuffer, 0, flushEncoder);
var count = _encoder.GetBytes(
_charBuffer.Array,
_charBuffer.Offset,
_charBufferCount,
_byteBuffer.Array,
_byteBuffer.Offset,
flushEncoder);
if (count > 0)
{
_stream.Write(_byteBuffer, 0, count);
_stream.Write(_byteBuffer.Array, _byteBuffer.Offset, count);
}
_charBufferCount = 0;
@ -198,10 +263,17 @@ namespace Microsoft.AspNet.Mvc
return;
}
var count = _encoder.GetBytes(_charBuffer, 0, _charBufferCount, _byteBuffer, 0, flushEncoder);
var count = _encoder.GetBytes(
_charBuffer.Array,
_charBuffer.Offset,
_charBufferCount,
_byteBuffer.Array,
_byteBuffer.Offset,
flushEncoder);
if (count > 0)
{
await _stream.WriteAsync(_byteBuffer, 0, count);
await _stream.WriteAsync(_byteBuffer.Array, _byteBuffer.Offset, count);
}
_charBufferCount = 0;
@ -218,8 +290,8 @@ namespace Microsoft.AspNet.Mvc
value.CopyTo(
sourceIndex: index,
destination: _charBuffer,
destinationIndex: _charBufferCount,
destination: _charBuffer.Array,
destinationIndex: _charBuffer.Offset + _charBufferCount,
count: remaining);
_charBufferCount += remaining;
@ -234,8 +306,8 @@ namespace Microsoft.AspNet.Mvc
Buffer.BlockCopy(
src: values,
srcOffset: index * sizeof(char),
dst: _charBuffer,
dstOffset: _charBufferCount * sizeof(char),
dst: _charBuffer.Array,
dstOffset: (_charBuffer.Offset + _charBufferCount) * sizeof(char),
count: remaining * sizeof(char));
_charBufferCount += remaining;
@ -244,4 +316,3 @@ namespace Microsoft.AspNet.Mvc
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text;
namespace Microsoft.AspNet.Mvc.Infrastructure
{
/// <summary>
/// Creates <see cref="TextWriter"/> instances for writing to <see cref="Http.HttpResponse.Body"/>.
/// </summary>
public interface IHttpResponseStreamWriterFactory
{
/// <summary>
/// Creates a new <see cref="TextWriter"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/>, usually <see cref="Http.HttpResponse.Body"/>.</param>
/// <param name="encoding">The <see cref="Encoding"/>, usually <see cref="Encoding.UTF8"/>.</param>
/// <returns>A <see cref="TextWriter"/>.</returns>
TextWriter CreateWriter(Stream stream, Encoding encoding);
}
}

View File

@ -0,0 +1,90 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using Microsoft.Framework.MemoryPool;
namespace Microsoft.AspNet.Mvc.Infrastructure
{
/// <summary>
/// An <see cref="IHttpResponseStreamWriterFactory"/> that uses pooled buffers.
/// </summary>
public class MemoryPoolHttpResponseStreamWriterFactory : IHttpResponseStreamWriterFactory
{
/// <summary>
/// The default size of created buffers.
/// </summary>
public static readonly int DefaultBufferSize = 4 * 1024; // 4KB
private readonly IArraySegmentPool<byte> _bytePool;
private readonly IArraySegmentPool<char> _charPool;
/// <summary>
/// Creates a new <see cref="MemoryPoolHttpResponseStreamWriterFactory"/>.
/// </summary>
/// <param name="bytePool">
/// The <see cref="IArraySegmentPool{byte}"/> for creating <see cref="byte"/> buffers.
/// </param>
/// <param name="charPool">
/// The <see cref="IArraySegmentPool{char}"/> for creating <see cref="char"/> buffers.
/// </param>
public MemoryPoolHttpResponseStreamWriterFactory(
IArraySegmentPool<byte> bytePool,
IArraySegmentPool<char> charPool)
{
if (bytePool == null)
{
throw new ArgumentNullException(nameof(bytePool));
}
if (charPool == null)
{
throw new ArgumentNullException(nameof(charPool));
}
_bytePool = bytePool;
_charPool = charPool;
}
/// <inheritdoc />
public TextWriter CreateWriter(Stream stream, Encoding encoding)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
if (encoding == null)
{
throw new ArgumentNullException(nameof(encoding));
}
LeasedArraySegment<byte> bytes = null;
LeasedArraySegment<char> chars = null;
try
{
bytes = _bytePool.Lease(DefaultBufferSize);
chars = _charPool.Lease(DefaultBufferSize);
return new HttpResponseStreamWriter(stream, encoding, bytes, chars);
}
catch
{
if (bytes != null)
{
bytes.Owner.Return(bytes);
}
if (chars != null)
{
chars.Owner.Return(chars);
}
throw;
}
}
}
}

View File

@ -19,6 +19,7 @@
"type": "build"
},
"Microsoft.Framework.Logging.Abstractions": "1.0.0-*",
"Microsoft.Framework.MemoryPool": "1.0.0-*",
"Microsoft.Framework.PropertyActivator.Sources": {
"version": "1.0.0-*",
"type": "build"

View File

@ -4,6 +4,7 @@
using System;
using System.Diagnostics.Tracing;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Infrastructure;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
@ -19,15 +20,17 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
/// Creates a new <see cref="PartialViewResultExecutor"/>.
/// </summary>
/// <param name="viewOptions">The <see cref="IOptions{MvcViewOptions}"/>.</param>
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
/// <param name="viewEngine">The <see cref="ICompositeViewEngine"/>.</param>
/// <param name="telemetry">The <see cref="TelemetrySource"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public PartialViewResultExecutor(
IOptions<MvcViewOptions> viewOptions,
IHttpResponseStreamWriterFactory writerFactory,
ICompositeViewEngine viewEngine,
TelemetrySource telemetry,
ILoggerFactory loggerFactory)
: base(viewOptions, viewEngine, telemetry)
: base(viewOptions, writerFactory, viewEngine, telemetry)
{
if (loggerFactory == null)
{

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics.Tracing;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Infrastructure;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.Framework.OptionsModel;
@ -29,10 +30,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
/// Creates a new <see cref="ViewExecutor"/>.
/// </summary>
/// <param name="viewOptions">The <see cref="IOptions{MvcViewOptions}"/>.</param>
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
/// <param name="viewEngine">The <see cref="ICompositeViewEngine"/>.</param>
/// <param name="telemetry">The <see cref="TelemetrySource"/>.</param>
public ViewExecutor(
IOptions<MvcViewOptions> viewOptions,
IHttpResponseStreamWriterFactory writerFactory,
ICompositeViewEngine viewEngine,
TelemetrySource telemetry)
{
@ -41,6 +44,11 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(viewOptions));
}
if (writerFactory == null)
{
throw new ArgumentNullException(nameof(writerFactory));
}
if (viewEngine == null)
{
throw new ArgumentNullException(nameof(viewEngine));
@ -52,6 +60,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
ViewOptions = viewOptions.Value;
WriterFactory = writerFactory;
ViewEngine = viewEngine;
Telemetry = telemetry;
}
@ -71,6 +80,11 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
/// </summary>
protected MvcViewOptions ViewOptions { get; }
/// <summary>
/// Gets the <see cref="IHttpResponseStreamWriterFactory"/>.
/// </summary>
protected IHttpResponseStreamWriterFactory WriterFactory { get; }
/// <summary>
/// Executes a view asynchronously.
/// </summary>
@ -134,7 +148,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
}
var encoding = contentType?.Encoding ?? DefaultContentType.Encoding;
using (var writer = new HttpResponseStreamWriter(response.Body, encoding))
using (var writer = WriterFactory.CreateWriter(response.Body, encoding))
{
var viewContext = new ViewContext(
actionContext,

View File

@ -4,6 +4,7 @@
using System;
using System.Diagnostics.Tracing;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Infrastructure;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
@ -19,15 +20,17 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
/// Creates a new <see cref="ViewResultExecutor"/>.
/// </summary>
/// <param name="viewOptions">The <see cref="IOptions{MvcViewOptions}"/>.</param>
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
/// <param name="viewEngine">The <see cref="ICompositeViewEngine"/>.</param>
/// <param name="telemetry">The <see cref="TelemetrySource"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public ViewResultExecutor(
IOptions<MvcViewOptions> viewOptions,
IHttpResponseStreamWriterFactory writerFactory,
ICompositeViewEngine viewEngine,
TelemetrySource telemetry,
ILoggerFactory loggerFactory)
: base(viewOptions, viewEngine, telemetry)
: base(viewOptions, writerFactory, viewEngine, telemetry)
{
if (loggerFactory == null)
{

View File

@ -1,10 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Framework.MemoryPool;
using Xunit;
namespace Microsoft.AspNet.Mvc
@ -355,6 +357,128 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(expectedBytes, stream.ToArray());
}
// None of the code in HttpResponseStreamWriter differs significantly when using pooled buffers.
//
// This test effectively verifies that things are correctly constructed and disposed. Pooled buffers
// throw on the finalizer thread if not disposed, so that's why it's complicated.
[Fact]
public void HttpResponseStreamWriter_UsingPooledBuffers()
{
// Arrange
var encoding = Encoding.UTF8;
var stream = new MemoryStream();
var expectedBytes = encoding.GetBytes("Hello, World!");
using (var bytePool = new DefaultArraySegmentPool<byte>())
{
using (var charPool = new DefaultArraySegmentPool<char>())
{
LeasedArraySegment<byte> bytes = null;
LeasedArraySegment<char> chars = null;
HttpResponseStreamWriter writer;
try
{
bytes = bytePool.Lease(4096);
chars = charPool.Lease(4096);
writer = new HttpResponseStreamWriter(stream, encoding, bytes, chars);
}
catch
{
if (bytes != null)
{
bytes.Owner.Return(bytes);
}
if (chars != null)
{
chars.Owner.Return(chars);
}
throw;
}
// Act
using (writer)
{
writer.Write("Hello, World!");
}
}
}
// Assert
Assert.Equal(expectedBytes, stream.ToArray());
}
// This covers the case where we need to limit the usable region of the char buffer
// based on the size of the byte buffer. See comments in the constructor.
[Fact]
public void HttpResponseStreamWriter_UsingPooledBuffers_SmallByteBuffer()
{
// Arrange
var encoding = Encoding.UTF8;
var stream = new MemoryStream();
var charBufferSize = encoding.GetMaxCharCount(1024);
// This content is bigger than the byte buffer can hold, so it will need to be split
// into two separate encoding operations.
var content = new string('a', charBufferSize + 1);
var expectedBytes = encoding.GetBytes(content);
using (var bytePool = new DefaultArraySegmentPool<byte>())
{
using (var charPool = new DefaultArraySegmentPool<char>())
{
LeasedArraySegment<byte> bytes = null;
LeasedArraySegment<char> chars = null;
HttpResponseStreamWriter writer;
try
{
bytes = bytePool.Lease(1024);
chars = charPool.Lease(4096);
writer = new HttpResponseStreamWriter(stream, encoding, bytes, chars);
}
catch
{
if (bytes != null)
{
bytes.Owner.Return(bytes);
}
if (chars != null)
{
chars.Owner.Return(chars);
}
throw;
}
// Zero the byte buffer because we're going to examine it.
Array.Clear(bytes.Data.Array, 0, bytes.Data.Array.Length);
// Act
using (writer)
{
writer.Write(content);
}
// Verify that we didn't buffer overflow 'our' region of the underlying array.
if (bytes.Data.Array.Length > bytes.Data.Count)
{
Assert.Equal((byte)0, bytes.Data.Array[bytes.Data.Count]);
}
}
}
// Assert
Assert.Equal(expectedBytes, stream.ToArray());
}
private class TestMemoryStream : MemoryStream
{
private int _flushCallCount;

View File

@ -0,0 +1,50 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text;
using Microsoft.Framework.MemoryPool;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Infrastructure
{
public class MemoryPoolHttpResponseStreamWriterFactoryTest
{
[Fact]
public void CreateWriter_BuffersReturned_OnException()
{
// Arrange
var bytePool = new Mock<IArraySegmentPool<byte>>(MockBehavior.Strict);
bytePool
.Setup(p => p.Lease(MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize))
.Returns(new LeasedArraySegment<byte>(new ArraySegment<byte>(new byte[0]), bytePool.Object));
bytePool
.Setup(p => p.Return(It.IsAny<LeasedArraySegment<byte>>()))
.Verifiable();
var charPool = new Mock<IArraySegmentPool<char>>(MockBehavior.Strict);
charPool
.Setup(p => p.Lease(MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize))
.Returns(new LeasedArraySegment<char>(new ArraySegment<char>(new char[0]), charPool.Object));
charPool
.Setup(p => p.Return(It.IsAny<LeasedArraySegment<char>>()))
.Verifiable();
var encoding = new Mock<Encoding>(MockBehavior.Strict);
encoding
.Setup(e => e.GetEncoder())
.Throws(new InvalidOperationException());
var factory = new MemoryPoolHttpResponseStreamWriterFactory(bytePool.Object, charPool.Object);
// Act
Assert.Throws<InvalidOperationException>(() => factory.CreateWriter(new MemoryStream(), encoding.Object));
// Assert
bytePool.Verify();
charPool.Verify();
}
}
}

View File

@ -86,6 +86,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var assemblyProvider = CreateAssemblyProvider(applicationWebSiteName);
services.AddInstance(assemblyProvider);
// Avoid using pooled memory, we don't have a guarantee that our services will get disposed.
services.AddInstance<IHttpResponseStreamWriterFactory>(new TestHttpResponseStreamWriterFactory());
if (configureServices != null)
{
configureServices(services);

View File

@ -0,0 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text;
using Microsoft.AspNet.Mvc.Infrastructure;
namespace Microsoft.AspNet.Mvc
{
public class TestHttpResponseStreamWriterFactory : IHttpResponseStreamWriterFactory
{
public TextWriter CreateWriter(Stream stream, Encoding encoding)
{
return new HttpResponseStreamWriter(stream, encoding);
}
}
}

View File

@ -106,6 +106,7 @@ namespace Microsoft.AspNet.Mvc
var options = new TestOptionsManager<MvcViewOptions>();
var viewExecutor = new PartialViewResultExecutor(
options,
new TestHttpResponseStreamWriterFactory(),
new CompositeViewEngine(options),
new TelemetryListener("Microsoft.AspNet"),
NullLoggerFactory.Instance);

View File

@ -8,7 +8,6 @@ using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.Logging;
using Microsoft.Framework.Logging.Testing;
using Microsoft.Net.Http.Headers;
using Moq;
@ -217,6 +216,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
var viewExecutor = new PartialViewResultExecutor(
options,
new TestHttpResponseStreamWriterFactory(),
new CompositeViewEngine(options),
telemetry,
NullLoggerFactory.Instance);

View File

@ -286,6 +286,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
return new ViewExecutor(
new TestOptionsManager<MvcViewOptions>(),
new TestHttpResponseStreamWriterFactory(),
new Mock<ICompositeViewEngine>(MockBehavior.Strict).Object,
listener);
}

View File

@ -216,6 +216,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
var viewExecutor = new ViewResultExecutor(
options,
new TestHttpResponseStreamWriterFactory(),
new CompositeViewEngine(options),
telemetry,
NullLoggerFactory.Instance);

View File

@ -13,7 +13,6 @@ using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using Microsoft.Framework.Logging.Testing;
using Moq;
using Xunit;
@ -107,6 +106,7 @@ namespace Microsoft.AspNet.Mvc
var options = new TestOptionsManager<MvcViewOptions>();
var viewExecutor = new ViewResultExecutor(
options,
new TestHttpResponseStreamWriterFactory(),
new CompositeViewEngine(options),
new TelemetryListener("Microsoft.AspNet"),
NullLoggerFactory.Instance);