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]
public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
[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 override void Flush() { }
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.Diagnostics;
using System.IO;
using System.IO.Pipelines;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Internal;
@ -184,9 +186,31 @@ namespace Microsoft.AspNetCore.WebUtilities
// unspooled content. Copy the FileStream content first when available.
if (FileStream != null)
{
FileStream.Position = 0;
await FileStream.CopyToAsync(destination, cancellationToken);
// 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();
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();
FileStream = null;
}
@ -227,10 +251,10 @@ namespace Microsoft.AspNetCore.WebUtilities
FileStream = new FileStream(
tempFileName,
FileMode.Create,
FileAccess.ReadWrite,
FileShare.Delete,
FileAccess.Write,
FileShare.Delete | FileShare.ReadWrite,
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.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
@ -84,6 +85,23 @@ namespace Microsoft.AspNetCore.WebUtilities
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)
{
ThrowIfDisposed();

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Xunit;
namespace Microsoft.AspNetCore.WebUtilities
@ -383,9 +384,9 @@ namespace Microsoft.AspNetCore.WebUtilities
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();
fileStream.CopyTo(memoryStream);
fs.CopyTo(memoryStream);
return memoryStream.ToArray();
}

View File

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

View File

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

View File

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

View File

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