From 4e08eda58d52a74ae883a66e3719188246d70c82 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sun, 13 Sep 2015 16:31:52 -0700 Subject: [PATCH] Use pooled memory for the streamwriter --- .../MvcCoreServiceCollectionExtensions.cs | 4 + .../HttpResponseStreamWriter.cs | 103 ++++++++++++--- .../IHttpResponseStreamWriterFactory.cs | 22 ++++ ...moryPoolHttpResponseStreamWriterFactory.cs | 90 +++++++++++++ src/Microsoft.AspNet.Mvc.Core/project.json | 1 + .../ViewFeatures/PartialViewResultExecutor.cs | 5 +- .../ViewFeatures/ViewExecutor.cs | 16 ++- .../ViewFeatures/ViewResultExecutor.cs | 5 +- .../HttpResponseStreamWriterTest.cs | 124 ++++++++++++++++++ ...PoolHttpResponseStreamWriterFactoryTest.cs | 50 +++++++ .../TestHelper.cs | 3 + .../TestHttpResponseStreamWriterFactory.cs | 17 +++ .../PartialViewResultTest.cs | 1 + .../PartialViewResultExecutorTest.cs | 2 +- .../ViewFeatures/ViewExecutorTest.cs | 1 + .../ViewFeatures/ViewResultExecutorTest.cs | 1 + .../ViewResultTest.cs | 2 +- 17 files changed, 426 insertions(+), 21 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Infrastructure/IHttpResponseStreamWriterFactory.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Infrastructure/MemoryPoolHttpResponseStreamWriterFactory.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MemoryPoolHttpResponseStreamWriterFactoryTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.TestCommon/TestHttpResponseStreamWriterFactory.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 433d49d707..5765c1f892 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -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(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton, DefaultArraySegmentPool>(); + services.TryAddSingleton, DefaultArraySegmentPool>(); } private static void ConfigureDefaultServices(IServiceCollection services) diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs b/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs index d9b49c9c62..33adaff958 100644 --- a/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/HttpResponseStreamWriter.cs @@ -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. /// public const int DefaultBufferSize = 1024; + private readonly Stream _stream; private Encoder _encoder; - private byte[] _byteBuffer; - private char[] _charBuffer; + private LeasedArraySegment _leasedByteBuffer; + private LeasedArraySegment _leasedCharBuffer; + private ArraySegment _byteBuffer; + private ArraySegment _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(new byte[encoding.GetMaxByteCount(bufferSize)]); + _charBuffer = new ArraySegment(new char[bufferSize]); + } + + public HttpResponseStreamWriter( + Stream stream, + Encoding encoding, + LeasedArraySegment leasedByteBuffer, + LeasedArraySegment 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 } } } - diff --git a/src/Microsoft.AspNet.Mvc.Core/Infrastructure/IHttpResponseStreamWriterFactory.cs b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/IHttpResponseStreamWriterFactory.cs new file mode 100644 index 0000000000..1ec51d2cfb --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/IHttpResponseStreamWriterFactory.cs @@ -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 +{ + /// + /// Creates instances for writing to . + /// + public interface IHttpResponseStreamWriterFactory + { + /// + /// Creates a new . + /// + /// The , usually . + /// The , usually . + /// A . + TextWriter CreateWriter(Stream stream, Encoding encoding); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Infrastructure/MemoryPoolHttpResponseStreamWriterFactory.cs b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/MemoryPoolHttpResponseStreamWriterFactory.cs new file mode 100644 index 0000000000..21bad011c6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Infrastructure/MemoryPoolHttpResponseStreamWriterFactory.cs @@ -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 +{ + /// + /// An that uses pooled buffers. + /// + public class MemoryPoolHttpResponseStreamWriterFactory : IHttpResponseStreamWriterFactory + { + /// + /// The default size of created buffers. + /// + public static readonly int DefaultBufferSize = 4 * 1024; // 4KB + + private readonly IArraySegmentPool _bytePool; + private readonly IArraySegmentPool _charPool; + + /// + /// Creates a new . + /// + /// + /// The for creating buffers. + /// + /// + /// The for creating buffers. + /// + public MemoryPoolHttpResponseStreamWriterFactory( + IArraySegmentPool bytePool, + IArraySegmentPool charPool) + { + if (bytePool == null) + { + throw new ArgumentNullException(nameof(bytePool)); + } + + if (charPool == null) + { + throw new ArgumentNullException(nameof(charPool)); + } + + _bytePool = bytePool; + _charPool = charPool; + } + + /// + public TextWriter CreateWriter(Stream stream, Encoding encoding) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + LeasedArraySegment bytes = null; + LeasedArraySegment 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; + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index 05c4f8db4c..d669a8a1f1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -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" diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs index a8c8a9014c..b0b66efd27 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs @@ -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 . /// /// The . + /// The . /// The . /// The . /// The . public PartialViewResultExecutor( IOptions viewOptions, + IHttpResponseStreamWriterFactory writerFactory, ICompositeViewEngine viewEngine, TelemetrySource telemetry, ILoggerFactory loggerFactory) - : base(viewOptions, viewEngine, telemetry) + : base(viewOptions, writerFactory, viewEngine, telemetry) { if (loggerFactory == null) { diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs index 5487e254d6..38d5573b87 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs @@ -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 . /// /// The . + /// The . /// The . /// The . public ViewExecutor( IOptions 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 /// protected MvcViewOptions ViewOptions { get; } + /// + /// Gets the . + /// + protected IHttpResponseStreamWriterFactory WriterFactory { get; } + /// /// Executes a view asynchronously. /// @@ -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, diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs index 9058add058..2ac519e4b6 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs @@ -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 . /// /// The . + /// The . /// The . /// The . /// The . public ViewResultExecutor( IOptions viewOptions, + IHttpResponseStreamWriterFactory writerFactory, ICompositeViewEngine viewEngine, TelemetrySource telemetry, ILoggerFactory loggerFactory) - : base(viewOptions, viewEngine, telemetry) + : base(viewOptions, writerFactory, viewEngine, telemetry) { if (loggerFactory == null) { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/HttpResponseStreamWriterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/HttpResponseStreamWriterTest.cs index 9f02a6de7c..3294380eba 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/HttpResponseStreamWriterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/HttpResponseStreamWriterTest.cs @@ -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()) + { + using (var charPool = new DefaultArraySegmentPool()) + { + LeasedArraySegment bytes = null; + LeasedArraySegment 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()) + { + using (var charPool = new DefaultArraySegmentPool()) + { + LeasedArraySegment bytes = null; + LeasedArraySegment 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; diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MemoryPoolHttpResponseStreamWriterFactoryTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MemoryPoolHttpResponseStreamWriterFactoryTest.cs new file mode 100644 index 0000000000..2446dd909c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Infrastructure/MemoryPoolHttpResponseStreamWriterFactoryTest.cs @@ -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>(MockBehavior.Strict); + bytePool + .Setup(p => p.Lease(MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize)) + .Returns(new LeasedArraySegment(new ArraySegment(new byte[0]), bytePool.Object)); + bytePool + .Setup(p => p.Return(It.IsAny>())) + .Verifiable(); + + var charPool = new Mock>(MockBehavior.Strict); + charPool + .Setup(p => p.Lease(MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize)) + .Returns(new LeasedArraySegment(new ArraySegment(new char[0]), charPool.Object)); + charPool + .Setup(p => p.Return(It.IsAny>())) + .Verifiable(); + + var encoding = new Mock(MockBehavior.Strict); + encoding + .Setup(e => e.GetEncoder()) + .Throws(new InvalidOperationException()); + + var factory = new MemoryPoolHttpResponseStreamWriterFactory(bytePool.Object, charPool.Object); + + // Act + Assert.Throws(() => factory.CreateWriter(new MemoryStream(), encoding.Object)); + + // Assert + bytePool.Verify(); + charPool.Verify(); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs index 94dfed7d1e..db118110b2 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs @@ -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(new TestHttpResponseStreamWriterFactory()); + if (configureServices != null) { configureServices(services); diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/TestHttpResponseStreamWriterFactory.cs b/test/Microsoft.AspNet.Mvc.TestCommon/TestHttpResponseStreamWriterFactory.cs new file mode 100644 index 0000000000..4bb4cd05ab --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TestCommon/TestHttpResponseStreamWriterFactory.cs @@ -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); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs index c0b2db148d..08ece0033e 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs @@ -106,6 +106,7 @@ namespace Microsoft.AspNet.Mvc var options = new TestOptionsManager(); var viewExecutor = new PartialViewResultExecutor( options, + new TestHttpResponseStreamWriterFactory(), new CompositeViewEngine(options), new TelemetryListener("Microsoft.AspNet"), NullLoggerFactory.Instance); diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs index d2f1531832..110b9b925c 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs @@ -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); diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs index b514bb5eec..14a802b69d 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs @@ -286,6 +286,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures return new ViewExecutor( new TestOptionsManager(), + new TestHttpResponseStreamWriterFactory(), new Mock(MockBehavior.Strict).Object, listener); } diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs index 6c0b569a34..123c922791 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs @@ -216,6 +216,7 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures var viewExecutor = new ViewResultExecutor( options, + new TestHttpResponseStreamWriterFactory(), new CompositeViewEngine(options), telemetry, NullLoggerFactory.Instance); diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs index 052e00b914..89c77e43b9 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs @@ -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(); var viewExecutor = new ViewResultExecutor( options, + new TestHttpResponseStreamWriterFactory(), new CompositeViewEngine(options), new TelemetryListener("Microsoft.AspNet"), NullLoggerFactory.Instance);