3
3
4
4
using System ;
5
5
using System . Buffers ;
6
+ using System . Diagnostics ;
6
7
using System . IO ;
7
8
using System . Threading ;
8
9
using System . Threading . Tasks ;
@@ -19,30 +20,23 @@ namespace Microsoft.AspNetCore.WebUtilities
19
20
/// a temporary file on disk.
20
21
/// </para>
21
22
/// <para>
22
- /// The <see cref="FileBufferingWriteStream"/> performs opportunistic writes to the wrapping stream
23
- /// when asychronous operation such as <see cref="WriteAsync(byte[], int, int, CancellationToken)"/> or <see cref="FlushAsync(CancellationToken)"/>
24
- /// are performed.
23
+ /// Consumers of this API can invoke <see cref="M:Stream.CopyToAsync" /> to copy the results to the HTTP Response Stream.
25
24
/// </para>
26
25
/// </summary>
27
26
public sealed class FileBufferingWriteStream : Stream
28
27
{
29
- private const int MaxRentedBufferSize = 1024 * 1024 ; // 1MB
30
- private const int DefaultMemoryThreshold = 30 * 1024 ; // 30k
28
+ private const int DefaultMemoryThreshold = 32 * 1024 ; // 32k
31
29
32
- private readonly Stream _writeStream ;
33
30
private readonly int _memoryThreshold ;
34
31
private readonly long ? _bufferLimit ;
35
32
private readonly Func < string > _tempFileDirectoryAccessor ;
36
- private readonly ArrayPool < byte > _bytePool ;
37
- private readonly byte [ ] _rentedBuffer ;
38
33
39
34
/// <summary>
40
35
/// Initializes a new instance of <see cref="FileBufferingWriteStream"/>.
41
36
/// </summary>
42
- /// <param name="writeStream">The <see cref="Stream"/> to write buffered contents to.</param>
43
37
/// <param name="memoryThreshold">
44
38
/// The maximum amount of memory in bytes to allocate before switching to a file on disk.
45
- /// Defaults to 30kb .
39
+ /// Defaults to 32kb .
46
40
/// </param>
47
41
/// <param name="bufferLimit">
48
42
/// The maximum amouont of bytes that the <see cref="FileBufferingWriteStream"/> is allowed to buffer.
@@ -52,24 +46,10 @@ public sealed class FileBufferingWriteStream : Stream
52
46
/// uses the value returned by <see cref="Path.GetTempPath"/>.
53
47
/// </param>
54
48
public FileBufferingWriteStream (
55
- Stream writeStream ,
56
49
int memoryThreshold = DefaultMemoryThreshold ,
57
50
long ? bufferLimit = null ,
58
51
Func < string > tempFileDirectoryAccessor = null )
59
- : this ( writeStream , memoryThreshold , bufferLimit , tempFileDirectoryAccessor , ArrayPool < byte > . Shared )
60
52
{
61
-
62
- }
63
-
64
- internal FileBufferingWriteStream (
65
- Stream writeStream ,
66
- int memoryThreshold ,
67
- long ? bufferLimit ,
68
- Func < string > tempFileDirectoryAccessor ,
69
- ArrayPool < byte > bytePool )
70
- {
71
- _writeStream = writeStream ?? throw new ArgumentNullException ( nameof ( writeStream ) ) ;
72
-
73
53
if ( memoryThreshold < 0 )
74
54
{
75
55
throw new ArgumentOutOfRangeException ( nameof ( memoryThreshold ) ) ;
@@ -84,18 +64,7 @@ internal FileBufferingWriteStream(
84
64
_memoryThreshold = memoryThreshold ;
85
65
_bufferLimit = bufferLimit ;
86
66
_tempFileDirectoryAccessor = tempFileDirectoryAccessor ?? AspNetCoreTempDirectory . TempDirectoryFactory ;
87
- _bytePool = bytePool ;
88
-
89
- if ( memoryThreshold < MaxRentedBufferSize )
90
- {
91
- _rentedBuffer = bytePool . Rent ( memoryThreshold ) ;
92
- MemoryStream = new MemoryStream ( _rentedBuffer ) ;
93
- MemoryStream . SetLength ( 0 ) ;
94
- }
95
- else
96
- {
97
- MemoryStream = new MemoryStream ( ) ;
98
- }
67
+ PagedByteBuffer = new PagedByteBuffer ( ArrayPool < byte > . Shared ) ;
99
68
}
100
69
101
70
/// <inheritdoc />
@@ -117,9 +86,9 @@ public override long Position
117
86
set => throw new NotSupportedException ( ) ;
118
87
}
119
88
120
- internal long BufferedLength => MemoryStream . Length + ( FileStream ? . Length ?? 0 ) ;
89
+ internal long BufferedLength => PagedByteBuffer . Length + ( FileStream ? . Length ?? 0 ) ;
121
90
122
- internal MemoryStream MemoryStream { get ; }
91
+ internal PagedByteBuffer PagedByteBuffer { get ; }
123
92
124
93
internal FileStream FileStream { get ; private set ; }
125
94
@@ -144,22 +113,27 @@ public override void Write(byte[] buffer, int offset, int count)
144
113
145
114
if ( _bufferLimit . HasValue && _bufferLimit - BufferedLength < count )
146
115
{
147
- DiposeInternal ( ) ;
116
+ Dispose ( ) ;
148
117
throw new IOException ( "Buffer limit exceeded." ) ;
149
118
}
150
119
151
- var availableMemory = _memoryThreshold - MemoryStream . Position ;
152
- if ( count <= availableMemory )
120
+ // Allow buffering in memory if we're below the memory threshold once the current buffer is written.
121
+ var allowMemoryBuffer = ( _memoryThreshold - count ) >= PagedByteBuffer . Length ;
122
+ if ( allowMemoryBuffer )
153
123
{
154
124
// Buffer content in the MemoryStream if it has capacity.
155
- MemoryStream . Write ( buffer , offset , count ) ;
125
+ PagedByteBuffer . Add ( buffer , offset , count ) ;
126
+ Debug . Assert ( PagedByteBuffer . Length <= _memoryThreshold ) ;
156
127
}
157
128
else
158
129
{
159
130
// If the MemoryStream is incapable of accomodating the content to be written
160
131
// spool to disk.
161
132
EnsureFileStream ( ) ;
162
- CopyContent ( MemoryStream , FileStream ) ;
133
+
134
+ // Spool memory content to disk and clear in memory buffers. We no longer need to hold on to it
135
+ PagedByteBuffer . CopyTo ( FileStream , clearBuffers : true ) ;
136
+
163
137
FileStream . Write ( buffer , offset , count ) ;
164
138
}
165
139
}
@@ -170,36 +144,86 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc
170
144
ThrowArgumentException ( buffer , offset , count ) ;
171
145
ThrowIfDisposed ( ) ;
172
146
173
- // If we have the opportunity to go async, write the buffered content to the response.
174
- await FlushAsync ( cancellationToken ) ;
175
- await _writeStream . WriteAsync ( buffer , offset , count , cancellationToken ) ;
147
+ if ( _bufferLimit . HasValue && _bufferLimit - BufferedLength < count )
148
+ {
149
+ Dispose ( ) ;
150
+ throw new IOException ( "Buffer limit exceeded." ) ;
151
+ }
152
+
153
+ // Allow buffering in memory if we're below the memory threshold once the current buffer is written.
154
+ var allowMemoryBuffer = ( _memoryThreshold - count ) >= PagedByteBuffer . Length ;
155
+ if ( allowMemoryBuffer )
156
+ {
157
+ // Buffer content in the MemoryStream if it has capacity.
158
+ PagedByteBuffer . Add ( buffer , offset , count ) ;
159
+ Debug . Assert ( PagedByteBuffer . Length <= _memoryThreshold ) ;
160
+ }
161
+ else
162
+ {
163
+ // If the MemoryStream is incapable of accomodating the content to be written
164
+ // spool to disk.
165
+ EnsureFileStream ( ) ;
166
+
167
+ // Spool memory content to disk and clear in memory buffers. We no longer need to hold on to it
168
+ await PagedByteBuffer . CopyToAsync ( FileStream , clearBuffers : true , cancellationToken ) ;
169
+
170
+ await FileStream . WriteAsync ( buffer , offset , count , cancellationToken ) ;
171
+ }
172
+ }
173
+
174
+ public override void Flush ( )
175
+ {
176
+ // Do nothing.
176
177
}
177
178
178
179
/// <inheritdoc />
179
180
public override void SetLength ( long value ) => throw new NotSupportedException ( ) ;
180
181
181
- /// <inheritdoc />
182
- // In the ordinary case, we expect this to throw if the target is the HttpResponse Body
183
- // and disallows synchronous writes. We do not need to optimize for this.
184
- public override void Flush ( )
182
+ /// <summary>
183
+ /// Copies buffered content to <paramref name="destination"/>.
184
+ /// </summary>
185
+ /// <param name="destination">The <see cref="Stream" /> to copy to.</param>
186
+ /// <param name="bufferSize">The size of the buffer.</param>
187
+ /// <param name="cancellationToken">The <see cref="CancellationToken" />.</param>
188
+ /// <returns>A <see cref="Task" /> that represents the asynchronous copy operation.</returns>
189
+ /// <remarks>
190
+ /// Users of this API do not need to reset the <see cref="Position" /> of this instance, prior to copying content.
191
+ /// </remarks>
192
+ public override async Task CopyToAsync ( Stream destination , int bufferSize , CancellationToken cancellationToken )
185
193
{
194
+ // When not null, FileStream always has "older" spooled content. The PagedByteBuffer always has "newer"
195
+ // unspooled content. Copy the FileStream content first when available.
186
196
if ( FileStream != null )
187
197
{
188
- CopyContent ( FileStream , _writeStream ) ;
198
+ FileStream . Position = 0 ;
199
+ await FileStream . CopyToAsync ( destination , bufferSize , cancellationToken ) ;
189
200
}
190
201
191
- CopyContent ( MemoryStream , _writeStream ) ;
202
+ // Copy memory content, but do not clear the buffers. We want multiple invocations of CopyTo \ CopyToAsync
203
+ // on this instance to behave the same.
204
+ await PagedByteBuffer . CopyToAsync ( destination , clearBuffers : false , cancellationToken ) ;
192
205
}
193
206
194
- /// <inheritdoc />
195
- public override async Task FlushAsync ( CancellationToken cancellationToken )
207
+ /// <summary>
208
+ /// Copies buffered content to <paramref name="destination"/>.
209
+ /// </summary>
210
+ /// <param name="destination">The <see cref="Stream" /> to copy to.</param>
211
+ /// <param name="bufferSize">The size of the buffer.</param>
212
+ /// <remarks>
213
+ /// Users of this API do not need to reset the <see cref="Position" /> of this instance, prior to copying content.
214
+ /// </remarks>
215
+ public override void CopyTo ( Stream destination , int bufferSize )
196
216
{
217
+ // See comments under CopyToAsync for an explanation for the order of execution.
197
218
if ( FileStream != null )
198
219
{
199
- await CopyContentAsync ( FileStream , _writeStream , cancellationToken ) ;
220
+ FileStream . Position = 0 ;
221
+ FileStream . CopyTo ( destination , bufferSize ) ;
200
222
}
201
223
202
- await CopyContentAsync ( MemoryStream , _writeStream , cancellationToken ) ;
224
+ // Copy memory content, but do not clear the buffers. We want multiple invocations of CopyTo \ CopyToAsync
225
+ // on this instance to behave the same.
226
+ PagedByteBuffer . CopyTo ( destination , clearBuffers : false ) ;
203
227
}
204
228
205
229
/// <inheritdoc />
@@ -208,39 +232,21 @@ protected override void Dispose(bool disposing)
208
232
if ( ! Disposed )
209
233
{
210
234
Disposed = true ;
211
- Flush ( ) ;
212
235
213
- DiposeInternal ( ) ;
236
+ PagedByteBuffer . Dispose ( ) ;
237
+ FileStream ? . Dispose ( ) ;
214
238
}
215
239
}
216
240
217
- private void DiposeInternal ( )
218
- {
219
- Disposed = true ;
220
- _bytePool . Return ( _rentedBuffer ) ;
221
- MemoryStream . Dispose ( ) ;
222
- FileStream ? . Dispose ( ) ;
223
- }
224
-
225
241
/// <inheritdoc />
226
242
public override async ValueTask DisposeAsync ( )
227
243
{
228
244
if ( ! Disposed )
229
245
{
230
246
Disposed = true ;
231
- try
232
- {
233
- await FlushAsync ( ) ;
234
- }
235
- finally
236
- {
237
- if ( _rentedBuffer != null )
238
- {
239
- _bytePool . Return ( _rentedBuffer ) ;
240
- }
241
- await MemoryStream . DisposeAsync ( ) ;
242
- await ( FileStream ? . DisposeAsync ( ) ?? default ) ;
243
- }
247
+
248
+ PagedByteBuffer . Dispose ( ) ;
249
+ await ( FileStream ? . DisposeAsync ( ) ?? default ) ;
244
250
}
245
251
}
246
252
@@ -290,19 +296,5 @@ private static void ThrowArgumentException(byte[] buffer, int offset, int count)
290
296
throw new ArgumentOutOfRangeException ( nameof ( offset ) ) ;
291
297
}
292
298
}
293
-
294
- private static void CopyContent ( Stream source , Stream destination )
295
- {
296
- source . Position = 0 ;
297
- source . CopyTo ( destination ) ;
298
- source . SetLength ( 0 ) ;
299
- }
300
-
301
- private static async Task CopyContentAsync ( Stream source , Stream destination , CancellationToken cancellationToken )
302
- {
303
- source . Position = 0 ;
304
- await source . CopyToAsync ( destination , cancellationToken ) ;
305
- source . SetLength ( 0 ) ;
306
- }
307
299
}
308
300
}
0 commit comments