Made changes to FileBufferWriteStream (#21223)

* Made changes to FileBufferWriteStream
- Make the internal FileStream write only
- Make a new readable stream over the same file in DrainBufferAsync to copy data to the buffer.
- Added an overload to DrainBufferAsync into a PipeWriter and use this overload in the 2 formatters in MVC. This should reduce the amount of copying from the internal buffer and reduces pinning (since these buffers are already pinned)
- Improved formatter tests
This commit is contained in:
David Fowler 2020-04-28 23:22:39 -07:00 committed by GitHub
parent 127c10d49a
commit 5c6f97b9ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 60 additions and 11 deletions

View File

@ -77,6 +77,8 @@ namespace Microsoft.AspNetCore.WebUtilities
[System.Diagnostics.DebuggerStepThroughAttribute] [System.Diagnostics.DebuggerStepThroughAttribute]
public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute] [System.Diagnostics.DebuggerStepThroughAttribute]
public System.Threading.Tasks.Task DrainBufferAsync(System.IO.Pipelines.PipeWriter destination, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public System.Threading.Tasks.Task DrainBufferAsync(System.IO.Stream destination, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task DrainBufferAsync(System.IO.Stream destination, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public override void Flush() { } public override void Flush() { }
public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { throw null; }

View File

@ -5,6 +5,8 @@ using System;
using System.Buffers; using System.Buffers;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Pipelines;
using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Internal;
@ -184,9 +186,31 @@ namespace Microsoft.AspNetCore.WebUtilities
// unspooled content. Copy the FileStream content first when available. // unspooled content. Copy the FileStream content first when available.
if (FileStream != null) if (FileStream != null)
{ {
FileStream.Position = 0; // We make a new stream for async reads from disk and async writes to the destination
await FileStream.CopyToAsync(destination, cancellationToken); await using var readStream = new FileStream(FileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, useAsync: true);
await readStream.CopyToAsync(destination, cancellationToken);
// This is created with delete on close
await FileStream.DisposeAsync();
FileStream = null;
}
await PagedByteBuffer.MoveToAsync(destination, cancellationToken);
}
public async Task DrainBufferAsync(PipeWriter destination, CancellationToken cancellationToken = default)
{
// When not null, FileStream always has "older" spooled content. The PagedByteBuffer always has "newer"
// unspooled content. Copy the FileStream content first when available.
if (FileStream != null)
{
// We make a new stream for async reads from disk and async writes to the destination
await using var readStream = new FileStream(FileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, useAsync: true);
await readStream.CopyToAsync(destination, cancellationToken);
// This is created with delete on close
await FileStream.DisposeAsync(); await FileStream.DisposeAsync();
FileStream = null; FileStream = null;
} }
@ -227,10 +251,10 @@ namespace Microsoft.AspNetCore.WebUtilities
FileStream = new FileStream( FileStream = new FileStream(
tempFileName, tempFileName,
FileMode.Create, FileMode.Create,
FileAccess.ReadWrite, FileAccess.Write,
FileShare.Delete, FileShare.Delete | FileShare.ReadWrite,
bufferSize: 1, bufferSize: 1,
FileOptions.Asynchronous | FileOptions.SequentialScan | FileOptions.DeleteOnClose); FileOptions.SequentialScan | FileOptions.DeleteOnClose);
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Pipelines;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -84,6 +85,23 @@ namespace Microsoft.AspNetCore.WebUtilities
ClearBuffers(); ClearBuffers();
} }
public async Task MoveToAsync(PipeWriter writer, CancellationToken cancellationToken)
{
ThrowIfDisposed();
for (var i = 0; i < Pages.Count; i++)
{
var page = Pages[i];
var length = (i == Pages.Count - 1) ?
_currentPageIndex :
page.Length;
await writer.WriteAsync(page.AsMemory(0, length), cancellationToken);
}
ClearBuffers();
}
public async Task MoveToAsync(Stream stream, CancellationToken cancellationToken) public async Task MoveToAsync(Stream stream, CancellationToken cancellationToken)
{ {
ThrowIfDisposed(); ThrowIfDisposed();

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.WebUtilities namespace Microsoft.AspNetCore.WebUtilities
@ -383,9 +384,9 @@ namespace Microsoft.AspNetCore.WebUtilities
private static byte[] ReadFileContent(FileStream fileStream) private static byte[] ReadFileContent(FileStream fileStream)
{ {
fileStream.Position = 0; var fs = new FileStream(fileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite);
using var memoryStream = new MemoryStream(); using var memoryStream = new MemoryStream();
fileStream.CopyTo(memoryStream); fs.CopyTo(memoryStream);
return memoryStream.ToArray(); return memoryStream.ToArray();
} }

View File

@ -261,7 +261,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
if (fileBufferingWriteStream != null) if (fileBufferingWriteStream != null)
{ {
response.ContentLength = fileBufferingWriteStream.Length; response.ContentLength = fileBufferingWriteStream.Length;
await fileBufferingWriteStream.DrainBufferAsync(response.Body); await fileBufferingWriteStream.DrainBufferAsync(response.BodyWriter);
} }
} }
finally finally

View File

@ -403,7 +403,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
} }
[Fact] [Fact]
public async Task XmlSerializerOutputFormatterDoesntFlushOutputStream() public async Task XmlSerializerOutputFormatterWritesContentLengthResponse()
{ {
// Arrange // Arrange
var sampleInput = new DummyClass { SampleInt = 10 }; var sampleInput = new DummyClass { SampleInt = 10 };
@ -411,10 +411,12 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType()); var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType());
var response = outputFormatterContext.HttpContext.Response; var response = outputFormatterContext.HttpContext.Response;
response.Body = FlushReportingStream.GetThrowingStream(); response.Body = Stream.Null;
// Act & Assert // Act & Assert
await formatter.WriteAsync(outputFormatterContext); await formatter.WriteAsync(outputFormatterContext);
Assert.NotNull(outputFormatterContext.HttpContext.Response.ContentLength);
} }
public static IEnumerable<object[]> TypesForGetSupportedContentTypes public static IEnumerable<object[]> TypesForGetSupportedContentTypes

View File

@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
if (fileBufferingWriteStream != null) if (fileBufferingWriteStream != null)
{ {
response.ContentLength = fileBufferingWriteStream.Length; response.ContentLength = fileBufferingWriteStream.Length;
await fileBufferingWriteStream.DrainBufferAsync(response.Body); await fileBufferingWriteStream.DrainBufferAsync(response.BodyWriter);
} }
} }
finally finally

View File

@ -305,6 +305,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var stream = new Mock<Stream> { CallBase = true }; var stream = new Mock<Stream> { CallBase = true };
stream.Setup(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>())) stream.Setup(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
stream.Setup(v => v.FlushAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
stream.SetupGet(s => s.CanWrite).Returns(true); stream.SetupGet(s => s.CanWrite).Returns(true);
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions()); var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions());
@ -322,6 +323,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
stream.Verify(v => v.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never()); stream.Verify(v => v.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
stream.Verify(v => v.Flush(), Times.Never()); stream.Verify(v => v.Flush(), Times.Never());
Assert.NotNull(outputFormatterContext.HttpContext.Response.ContentLength);
} }
private class TestableJsonOutputFormatter : NewtonsoftJsonOutputFormatter private class TestableJsonOutputFormatter : NewtonsoftJsonOutputFormatter