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:
parent
127c10d49a
commit
5c6f97b9ef
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue