Skip to content

Commit 5c6f97b

Browse files
authored
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
1 parent 127c10d commit 5c6f97b

File tree

8 files changed

+60
-11
lines changed

8 files changed

+60
-11
lines changed

src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ protected override void Dispose(bool disposing) { }
7777
[System.Diagnostics.DebuggerStepThroughAttribute]
7878
public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
7979
[System.Diagnostics.DebuggerStepThroughAttribute]
80+
public System.Threading.Tasks.Task DrainBufferAsync(System.IO.Pipelines.PipeWriter destination, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
81+
[System.Diagnostics.DebuggerStepThroughAttribute]
8082
public System.Threading.Tasks.Task DrainBufferAsync(System.IO.Stream destination, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
8183
public override void Flush() { }
8284
public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { throw null; }

src/Http/WebUtilities/src/FileBufferingWriteStream.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Buffers;
66
using System.Diagnostics;
77
using System.IO;
8+
using System.IO.Pipelines;
9+
using System.Security.Cryptography;
810
using System.Threading;
911
using System.Threading.Tasks;
1012
using Microsoft.AspNetCore.Internal;
@@ -184,9 +186,31 @@ public async Task DrainBufferAsync(Stream destination, CancellationToken cancell
184186
// unspooled content. Copy the FileStream content first when available.
185187
if (FileStream != null)
186188
{
187-
FileStream.Position = 0;
188-
await FileStream.CopyToAsync(destination, cancellationToken);
189+
// We make a new stream for async reads from disk and async writes to the destination
190+
await using var readStream = new FileStream(FileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, useAsync: true);
189191

192+
await readStream.CopyToAsync(destination, cancellationToken);
193+
194+
// This is created with delete on close
195+
await FileStream.DisposeAsync();
196+
FileStream = null;
197+
}
198+
199+
await PagedByteBuffer.MoveToAsync(destination, cancellationToken);
200+
}
201+
202+
public async Task DrainBufferAsync(PipeWriter destination, CancellationToken cancellationToken = default)
203+
{
204+
// When not null, FileStream always has "older" spooled content. The PagedByteBuffer always has "newer"
205+
// unspooled content. Copy the FileStream content first when available.
206+
if (FileStream != null)
207+
{
208+
// We make a new stream for async reads from disk and async writes to the destination
209+
await using var readStream = new FileStream(FileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, useAsync: true);
210+
211+
await readStream.CopyToAsync(destination, cancellationToken);
212+
213+
// This is created with delete on close
190214
await FileStream.DisposeAsync();
191215
FileStream = null;
192216
}
@@ -227,10 +251,10 @@ private void EnsureFileStream()
227251
FileStream = new FileStream(
228252
tempFileName,
229253
FileMode.Create,
230-
FileAccess.ReadWrite,
231-
FileShare.Delete,
254+
FileAccess.Write,
255+
FileShare.Delete | FileShare.ReadWrite,
232256
bufferSize: 1,
233-
FileOptions.Asynchronous | FileOptions.SequentialScan | FileOptions.DeleteOnClose);
257+
FileOptions.SequentialScan | FileOptions.DeleteOnClose);
234258
}
235259
}
236260

src/Http/WebUtilities/src/PagedByteBuffer.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Buffers;
66
using System.Collections.Generic;
77
using System.IO;
8+
using System.IO.Pipelines;
89
using System.Threading;
910
using System.Threading.Tasks;
1011

@@ -84,6 +85,23 @@ public void MoveTo(Stream stream)
8485
ClearBuffers();
8586
}
8687

88+
public async Task MoveToAsync(PipeWriter writer, CancellationToken cancellationToken)
89+
{
90+
ThrowIfDisposed();
91+
92+
for (var i = 0; i < Pages.Count; i++)
93+
{
94+
var page = Pages[i];
95+
var length = (i == Pages.Count - 1) ?
96+
_currentPageIndex :
97+
page.Length;
98+
99+
await writer.WriteAsync(page.AsMemory(0, length), cancellationToken);
100+
}
101+
102+
ClearBuffers();
103+
}
104+
87105
public async Task MoveToAsync(Stream stream, CancellationToken cancellationToken)
88106
{
89107
ThrowIfDisposed();

src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Text;
99
using System.Threading.Tasks;
10+
using Microsoft.Extensions.Configuration;
1011
using Xunit;
1112

1213
namespace Microsoft.AspNetCore.WebUtilities
@@ -383,9 +384,9 @@ public void Dispose()
383384

384385
private static byte[] ReadFileContent(FileStream fileStream)
385386
{
386-
fileStream.Position = 0;
387+
var fs = new FileStream(fileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite);
387388
using var memoryStream = new MemoryStream();
388-
fileStream.CopyTo(memoryStream);
389+
fs.CopyTo(memoryStream);
389390

390391
return memoryStream.ToArray();
391392
}

src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerOutputFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext co
261261
if (fileBufferingWriteStream != null)
262262
{
263263
response.ContentLength = fileBufferingWriteStream.Length;
264-
await fileBufferingWriteStream.DrainBufferAsync(response.Body);
264+
await fileBufferingWriteStream.DrainBufferAsync(response.BodyWriter);
265265
}
266266
}
267267
finally

src/Mvc/Mvc.Formatters.Xml/test/XmlSerializerOutputFormatterTest.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,18 +403,20 @@ public void CanWriteResult_ReturnsExpectedValueForMediaType(
403403
}
404404

405405
[Fact]
406-
public async Task XmlSerializerOutputFormatterDoesntFlushOutputStream()
406+
public async Task XmlSerializerOutputFormatterWritesContentLengthResponse()
407407
{
408408
// Arrange
409409
var sampleInput = new DummyClass { SampleInt = 10 };
410410
var formatter = new XmlSerializerOutputFormatter();
411411
var outputFormatterContext = GetOutputFormatterContext(sampleInput, sampleInput.GetType());
412412

413413
var response = outputFormatterContext.HttpContext.Response;
414-
response.Body = FlushReportingStream.GetThrowingStream();
414+
response.Body = Stream.Null;
415415

416416
// Act & Assert
417417
await formatter.WriteAsync(outputFormatterContext);
418+
419+
Assert.NotNull(outputFormatterContext.HttpContext.Response.ContentLength);
418420
}
419421

420422
public static IEnumerable<object[]> TypesForGetSupportedContentTypes

src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonOutputFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext co
155155
if (fileBufferingWriteStream != null)
156156
{
157157
response.ContentLength = fileBufferingWriteStream.Length;
158-
await fileBufferingWriteStream.DrainBufferAsync(response.Body);
158+
await fileBufferingWriteStream.DrainBufferAsync(response.BodyWriter);
159159
}
160160
}
161161
finally

src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonOutputFormatterTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ public async Task WriteToStreamAsync_LargePayload_DoesNotPerformSynchronousWrite
305305
var stream = new Mock<Stream> { CallBase = true };
306306
stream.Setup(v => v.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
307307
.Returns(Task.CompletedTask);
308+
stream.Setup(v => v.FlushAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
308309
stream.SetupGet(s => s.CanWrite).Returns(true);
309310

310311
var formatter = new NewtonsoftJsonOutputFormatter(new JsonSerializerSettings(), ArrayPool<char>.Shared, new MvcOptions());
@@ -322,6 +323,7 @@ public async Task WriteToStreamAsync_LargePayload_DoesNotPerformSynchronousWrite
322323

323324
stream.Verify(v => v.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
324325
stream.Verify(v => v.Flush(), Times.Never());
326+
Assert.NotNull(outputFormatterContext.HttpContext.Response.ContentLength);
325327
}
326328

327329
private class TestableJsonOutputFormatter : NewtonsoftJsonOutputFormatter

0 commit comments

Comments
 (0)