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.Mvc.Routing;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection.Extensions; using Microsoft.Framework.DependencyInjection.Extensions;
using Microsoft.Framework.MemoryPool;
using Microsoft.Framework.OptionsModel; using Microsoft.Framework.OptionsModel;
namespace Microsoft.Framework.DependencyInjection namespace Microsoft.Framework.DependencyInjection
@ -137,6 +138,9 @@ namespace Microsoft.Framework.DependencyInjection
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>(); services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.TryAddSingleton<IActionBindingContextAccessor, ActionBindingContextAccessor>(); services.TryAddSingleton<IActionBindingContextAccessor, ActionBindingContextAccessor>();
services.TryAddSingleton<IUrlHelper, UrlHelper>(); 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) private static void ConfigureDefaultServices(IServiceCollection services)

View File

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Framework.MemoryPool;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
@ -18,10 +19,13 @@ namespace Microsoft.AspNet.Mvc
/// Default buffer size. /// Default buffer size.
/// </summary> /// </summary>
public const int DefaultBufferSize = 1024; public const int DefaultBufferSize = 1024;
private readonly Stream _stream; private readonly Stream _stream;
private Encoder _encoder; private Encoder _encoder;
private byte[] _byteBuffer; private LeasedArraySegment<byte> _leasedByteBuffer;
private char[] _charBuffer; private LeasedArraySegment<char> _leasedCharBuffer;
private ArraySegment<byte> _byteBuffer;
private ArraySegment<char> _charBuffer;
private int _charBufferSize; private int _charBufferSize;
private int _charBufferCount; private int _charBufferCount;
@ -44,10 +48,54 @@ namespace Microsoft.AspNet.Mvc
_stream = stream; _stream = stream;
Encoding = encoding; Encoding = encoding;
_encoder = encoding.GetEncoder();
_charBufferSize = bufferSize; _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; } public override Encoding Encoding { get; }
@ -59,7 +107,7 @@ namespace Microsoft.AspNet.Mvc
FlushInternal(); FlushInternal();
} }
_charBuffer[_charBufferCount] = value; _charBuffer.Array[_charBuffer.Offset + _charBufferCount] = value;
_charBufferCount++; _charBufferCount++;
} }
@ -108,7 +156,7 @@ namespace Microsoft.AspNet.Mvc
await FlushInternalAsync(); await FlushInternalAsync();
} }
_charBuffer[_charBufferCount] = value; _charBuffer.Array[_charBuffer.Offset + _charBufferCount] = value;
_charBufferCount++; _charBufferCount++;
} }
@ -168,6 +216,16 @@ namespace Microsoft.AspNet.Mvc
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
FlushInternal(flushStream: false, flushEncoder: true); 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) private void FlushInternal(bool flushStream = false, bool flushEncoder = false)
@ -177,10 +235,17 @@ namespace Microsoft.AspNet.Mvc
return; 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) if (count > 0)
{ {
_stream.Write(_byteBuffer, 0, count); _stream.Write(_byteBuffer.Array, _byteBuffer.Offset, count);
} }
_charBufferCount = 0; _charBufferCount = 0;
@ -198,10 +263,17 @@ namespace Microsoft.AspNet.Mvc
return; 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) if (count > 0)
{ {
await _stream.WriteAsync(_byteBuffer, 0, count); await _stream.WriteAsync(_byteBuffer.Array, _byteBuffer.Offset, count);
} }
_charBufferCount = 0; _charBufferCount = 0;
@ -218,8 +290,8 @@ namespace Microsoft.AspNet.Mvc
value.CopyTo( value.CopyTo(
sourceIndex: index, sourceIndex: index,
destination: _charBuffer, destination: _charBuffer.Array,
destinationIndex: _charBufferCount, destinationIndex: _charBuffer.Offset + _charBufferCount,
count: remaining); count: remaining);
_charBufferCount += remaining; _charBufferCount += remaining;
@ -234,8 +306,8 @@ namespace Microsoft.AspNet.Mvc
Buffer.BlockCopy( Buffer.BlockCopy(
src: values, src: values,
srcOffset: index * sizeof(char), srcOffset: index * sizeof(char),
dst: _charBuffer, dst: _charBuffer.Array,
dstOffset: _charBufferCount * sizeof(char), dstOffset: (_charBuffer.Offset + _charBufferCount) * sizeof(char),
count: remaining * sizeof(char)); count: remaining * sizeof(char));
_charBufferCount += remaining; _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" "type": "build"
}, },
"Microsoft.Framework.Logging.Abstractions": "1.0.0-*", "Microsoft.Framework.Logging.Abstractions": "1.0.0-*",
"Microsoft.Framework.MemoryPool": "1.0.0-*",
"Microsoft.Framework.PropertyActivator.Sources": { "Microsoft.Framework.PropertyActivator.Sources": {
"version": "1.0.0-*", "version": "1.0.0-*",
"type": "build" "type": "build"

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.IO;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Framework.MemoryPool;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
@ -355,6 +357,128 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(expectedBytes, stream.ToArray()); 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 class TestMemoryStream : MemoryStream
{ {
private int _flushCallCount; 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); var assemblyProvider = CreateAssemblyProvider(applicationWebSiteName);
services.AddInstance(assemblyProvider); 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) if (configureServices != null)
{ {
configureServices(services); 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 options = new TestOptionsManager<MvcViewOptions>();
var viewExecutor = new PartialViewResultExecutor( var viewExecutor = new PartialViewResultExecutor(
options, options,
new TestHttpResponseStreamWriterFactory(),
new CompositeViewEngine(options), new CompositeViewEngine(options),
new TelemetryListener("Microsoft.AspNet"), new TelemetryListener("Microsoft.AspNet"),
NullLoggerFactory.Instance); NullLoggerFactory.Instance);

View File

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

View File

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

View File

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

View File

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