From 65f70fb5c3a3ab4b85a6410312eae3c45fbe35d5 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 02:17:24 +0000 Subject: [PATCH 01/15] System.Buffers in BufferedReadStream --- src/Microsoft.AspNetCore.Http/project.json | 3 ++- .../BufferedReadStream.cs | 14 ++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.Http/project.json b/src/Microsoft.AspNetCore.Http/project.json index 728e38eb..229661bb 100644 --- a/src/Microsoft.AspNetCore.Http/project.json +++ b/src/Microsoft.AspNetCore.Http/project.json @@ -13,7 +13,8 @@ "dependencies": { "Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*", "Microsoft.AspNetCore.WebUtilities": "1.0.0-*", - "Microsoft.Net.Http.Headers": "1.0.0-*" + "Microsoft.Net.Http.Headers": "1.0.0-*", + "System.Buffers": "4.0.0-*" }, "frameworks": { "net451": {}, diff --git a/src/Microsoft.AspNetCore.WebUtilities/BufferedReadStream.cs b/src/Microsoft.AspNetCore.WebUtilities/BufferedReadStream.cs index 7a1e2448..1726378b 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/BufferedReadStream.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/BufferedReadStream.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.IO; using System.Text; using System.Threading; @@ -28,7 +29,7 @@ public BufferedReadStream(Stream inner, int bufferSize) } _inner = inner; - _buffer = new byte[bufferSize]; + _buffer = ArrayPool.Shared.Rent(bufferSize); } public ArraySegment BufferedData @@ -128,10 +129,15 @@ public override void SetLength(long value) protected override void Dispose(bool disposing) { - _disposed = true; - if (disposing) + if (!_disposed) { - _inner.Dispose(); + _disposed = true; + ArrayPool.Shared.Return(_buffer); + + if (disposing) + { + _inner.Dispose(); + } } } From 6fdfbf0d8039554d1a7fc0b23bc399828c126038 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 02:21:04 +0000 Subject: [PATCH 02/15] System.Buffers in FormReader --- .../FormReader.cs | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs b/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs index a78e56c7..778fd8d7 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Collections.Generic; using System.IO; using System.Text; @@ -14,13 +15,14 @@ namespace Microsoft.AspNetCore.WebUtilities /// /// Used to read an 'application/x-www-form-urlencoded' form. /// - public class FormReader + public class FormReader : IDisposable { private readonly TextReader _reader; - private readonly char[] _buffer = new char[1024]; + private readonly char[] _buffer; private readonly StringBuilder _builder = new StringBuilder(); private int _bufferOffset; private int _bufferCount; + private bool _disposed; public FormReader(string data) { @@ -29,6 +31,7 @@ public FormReader(string data) throw new ArgumentNullException(nameof(data)); } + _buffer = ArrayPool.Shared.Rent(1024); _reader = new StringReader(data); } @@ -44,6 +47,7 @@ public FormReader(Stream stream, Encoding encoding) throw new ArgumentNullException(nameof(encoding)); } + _buffer = ArrayPool.Shared.Rent(1024); _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); } @@ -167,17 +171,18 @@ private async Task BufferAsync(CancellationToken cancellationToken) /// The collection containing the parsed HTTP form body. public static Dictionary ReadForm(string text) { - var reader = new FormReader(text); - - var accumulator = new KeyValueAccumulator(); - var pair = reader.ReadNextPair(); - while (pair.HasValue) + using (var reader = new FormReader(text)) { - accumulator.Append(pair.Value.Key, pair.Value.Value); - pair = reader.ReadNextPair(); - } + var accumulator = new KeyValueAccumulator(); + var pair = reader.ReadNextPair(); + while (pair.HasValue) + { + accumulator.Append(pair.Value.Key, pair.Value.Value); + pair = reader.ReadNextPair(); + } - return accumulator.GetResults(); + return accumulator.GetResults(); + } } /// @@ -197,17 +202,27 @@ public static Dictionary ReadForm(string text) /// The collection containing the parsed HTTP form body. public static async Task> ReadFormAsync(Stream stream, Encoding encoding, CancellationToken cancellationToken = new CancellationToken()) { - var reader = new FormReader(stream, encoding); - - var accumulator = new KeyValueAccumulator(); - var pair = await reader.ReadNextPairAsync(cancellationToken); - while (pair.HasValue) + using (var reader = new FormReader(stream, encoding)) { - accumulator.Append(pair.Value.Key, pair.Value.Value); - pair = await reader.ReadNextPairAsync(cancellationToken); + var accumulator = new KeyValueAccumulator(); + var pair = await reader.ReadNextPairAsync(cancellationToken); + while (pair.HasValue) + { + accumulator.Append(pair.Value.Key, pair.Value.Value); + pair = await reader.ReadNextPairAsync(cancellationToken); + } + + return accumulator.GetResults(); } + } - return accumulator.GetResults(); + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + ArrayPool.Shared.Return(_buffer); + } } } } \ No newline at end of file From 35d840b7c4994ae6313b5a6e4f518aad0f382a19 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 02:25:07 +0000 Subject: [PATCH 03/15] System.Buffers in StreamHelperExtensions --- .../StreamHelperExtensions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs b/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs index ac5d4c43..4c805597 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Buffers; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -11,13 +12,14 @@ public static class StreamHelperExtensions { public static async Task DrainAsync(this Stream stream, CancellationToken cancellationToken) { - byte[] buffer = new byte[1024]; + var buffer = ArrayPool.Shared.Rent(1024); cancellationToken.ThrowIfCancellationRequested(); while (await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken) > 0) { // Not all streams support cancellation directly. cancellationToken.ThrowIfCancellationRequested(); } + ArrayPool.Shared.Return(buffer); } } } \ No newline at end of file From 00d3d060cd04041909c9f6043640ead2a755df34 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 02:32:07 +0000 Subject: [PATCH 04/15] System.Buffers in HttpRequestStreamReader --- .../HttpRequestStreamReader.cs | 31 ++----------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/HttpRequestStreamReader.cs b/src/Microsoft.AspNetCore.WebUtilities/HttpRequestStreamReader.cs index 15e98d7e..67977ca6 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/HttpRequestStreamReader.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/HttpRequestStreamReader.cs @@ -34,40 +34,13 @@ public class HttpRequestStreamReader : TextReader private bool _isBlocked; public HttpRequestStreamReader(Stream stream, Encoding encoding) - : this(stream, encoding, DefaultBufferSize) + : this(stream, encoding, DefaultBufferSize, ArrayPool.Shared, ArrayPool.Shared) { } public HttpRequestStreamReader(Stream stream, Encoding encoding, int bufferSize) + : this(stream, encoding, bufferSize, ArrayPool.Shared, ArrayPool.Shared) { - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } - - if (!stream.CanRead) - { - throw new ArgumentException(Resources.HttpRequestStreamReader_StreamNotReadable, nameof(stream)); - } - - if (encoding == null) - { - throw new ArgumentNullException(nameof(encoding)); - } - - _stream = stream; - _encoding = encoding; - _decoder = encoding.GetDecoder(); - - if (bufferSize < MinBufferSize) - { - bufferSize = MinBufferSize; - } - - _byteBufferSize = bufferSize; - _byteBuffer = new byte[bufferSize]; - var maxCharsPerBuffer = encoding.GetMaxCharCount(bufferSize); - _charBuffer = new char[maxCharsPerBuffer]; } public HttpRequestStreamReader( From 05f460fa5884b2d00d9c4e31f93b80cf146e6c20 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 02:34:55 +0000 Subject: [PATCH 05/15] System.Buffers in HttpResponseStreamWriter --- .../HttpResponseStreamWriter.cs | 30 ++----------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/HttpResponseStreamWriter.cs b/src/Microsoft.AspNetCore.WebUtilities/HttpResponseStreamWriter.cs index 30f6423f..9ec8da23 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/HttpResponseStreamWriter.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/HttpResponseStreamWriter.cs @@ -34,39 +34,13 @@ public class HttpResponseStreamWriter : TextWriter private int _charBufferCount; public HttpResponseStreamWriter(Stream stream, Encoding encoding) - : this(stream, encoding, DefaultBufferSize) + : this(stream, encoding, DefaultBufferSize, ArrayPool.Shared, ArrayPool.Shared) { } public HttpResponseStreamWriter(Stream stream, Encoding encoding, int bufferSize) + : this(stream, encoding, bufferSize, ArrayPool.Shared, ArrayPool.Shared) { - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } - - if (!stream.CanWrite) - { - throw new ArgumentException(Resources.HttpResponseStreamWriter_StreamNotWritable, nameof(stream)); - } - - if (encoding == null) - { - throw new ArgumentNullException(nameof(encoding)); - } - - _stream = stream; - Encoding = encoding; - _charBufferSize = bufferSize; - - if (bufferSize < MinBufferSize) - { - bufferSize = MinBufferSize; - } - - _encoder = encoding.GetEncoder(); - _byteBuffer = new byte[encoding.GetMaxByteCount(bufferSize)]; - _charBuffer = new char[bufferSize]; } public HttpResponseStreamWriter( From a1af9017486a75553b30d84b5a630a046735f4fa Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 02:36:59 +0000 Subject: [PATCH 06/15] StreamHelperExtensions return buffer in finally --- .../StreamHelperExtensions.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs b/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs index 4c805597..679ca98b 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs @@ -12,14 +12,20 @@ public static class StreamHelperExtensions { public static async Task DrainAsync(this Stream stream, CancellationToken cancellationToken) { - var buffer = ArrayPool.Shared.Rent(1024); cancellationToken.ThrowIfCancellationRequested(); - while (await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken) > 0) + var buffer = ArrayPool.Shared.Rent(1024); + try + { + while (await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken) > 0) + { + // Not all streams support cancellation directly. + cancellationToken.ThrowIfCancellationRequested(); + } + } + finally { - // Not all streams support cancellation directly. - cancellationToken.ThrowIfCancellationRequested(); + ArrayPool.Shared.Return(buffer); } - ArrayPool.Shared.Return(buffer); } } } \ No newline at end of file From 8930b0f8b6c0471af0b30adbc561908f10e7836f Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 02:43:18 +0000 Subject: [PATCH 07/15] System.Buffers in MultipartReaderStream --- .../MultipartReaderStream.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs b/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs index 22836ee3..0c7a1714 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Diagnostics; using System.IO; using System.Text; @@ -227,17 +228,17 @@ public override int Read(byte[] buffer, int offset, int count) // "The boundary may be followed by zero or more characters of // linear whitespace. It is then terminated by either another CRLF" // or -- for the final boundary. - byte[] boundary = new byte[_boundaryBytes.Length]; - read = _innerStream.Read(boundary, 0, boundary.Length); - Debug.Assert(read == boundary.Length); // It should have all been buffered + var boundary = ArrayPool.Shared.Rent(_boundaryBytes.Length); + read = _innerStream.Read(boundary, 0, _boundaryBytes.Length); + Debug.Assert(read == _boundaryBytes.Length); // It should have all been buffered var remainder = _innerStream.ReadLine(lengthLimit: 100); // Whitespace may exceed the buffer. remainder = remainder.Trim(); if (string.Equals("--", remainder, StringComparison.Ordinal)) { FinalBoundaryFound = true; } + ArrayPool.Shared.Return(boundary); Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); - _finished = true; return 0; } @@ -279,15 +280,16 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, // "The boundary may be followed by zero or more characters of // linear whitespace. It is then terminated by either another CRLF" // or -- for the final boundary. - byte[] boundary = new byte[_boundaryBytes.Length]; - read = _innerStream.Read(boundary, 0, boundary.Length); - Debug.Assert(read == boundary.Length); // It should have all been buffered + var boundary = ArrayPool.Shared.Rent(_boundaryBytes.Length); + read = _innerStream.Read(boundary, 0, _boundaryBytes.Length); + Debug.Assert(read == _boundaryBytes.Length); // It should have all been buffered var remainder = await _innerStream.ReadLineAsync(lengthLimit: 100, cancellationToken: cancellationToken); // Whitespace may exceed the buffer. remainder = remainder.Trim(); if (string.Equals("--", remainder, StringComparison.Ordinal)) { FinalBoundaryFound = true; } + ArrayPool.Shared.Return(boundary); Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); _finished = true; From e92afc46ea31529649faaadb254c44ad85a623a5 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 02:49:48 +0000 Subject: [PATCH 08/15] System.Buffers in WebEncoders --- .../WebEncoders.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/WebEncoders.cs b/src/Microsoft.AspNetCore.WebUtilities/WebEncoders.cs index dc462135..31f0fc4f 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/WebEncoders.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/WebEncoders.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Diagnostics; namespace Microsoft.AspNetCore.WebUtilities @@ -59,17 +60,18 @@ public static byte[] Base64UrlDecode(string input, int offset, int count) // Assumption: input is base64url encoded without padding and contains no whitespace. // First, we need to add the padding characters back. - int numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); - char[] completeBase64Array = new char[checked(count + numPaddingCharsToAdd)]; + var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); + var length = checked(count + numPaddingCharsToAdd); + var completeBase64Array = ArrayPool.Shared.Rent(length); Debug.Assert(completeBase64Array.Length % 4 == 0, "Invariant: Array length must be a multiple of 4."); input.CopyTo(offset, completeBase64Array, 0, count); - for (int i = 1; i <= numPaddingCharsToAdd; i++) + for (var i = 1; i <= numPaddingCharsToAdd; i++) { - completeBase64Array[completeBase64Array.Length - i] = '='; + completeBase64Array[length - i] = '='; } // Next, fix up '-' -> '+' and '_' -> '/' - for (int i = 0; i < completeBase64Array.Length; i++) + for (int i = 0; i < length; i++) { char c = completeBase64Array[i]; if (c == '-') @@ -84,7 +86,9 @@ public static byte[] Base64UrlDecode(string input, int offset, int count) // Finally, decode. // If the caller provided invalid base64 chars, they'll be caught here. - return Convert.FromBase64CharArray(completeBase64Array, 0, completeBase64Array.Length); + var decoded = Convert.FromBase64CharArray(completeBase64Array, 0, length); + ArrayPool.Shared.Return(completeBase64Array); + return decoded; } /// @@ -126,7 +130,7 @@ public static string Base64UrlEncode(byte[] input, int offset, int count) // We're going to use base64url encoding with no padding characters. // See RFC 4648, Sec. 5. - char[] buffer = new char[GetNumBase64CharsRequiredForInput(count)]; + var buffer = ArrayPool.Shared.Rent(GetNumBase64CharsRequiredForInput(count)); int numBase64Chars = Convert.ToBase64CharArray(input, offset, count, buffer, 0); // Fix up '+' -> '-' and '/' -> '_' @@ -144,13 +148,17 @@ public static string Base64UrlEncode(byte[] input, int offset, int count) else if (ch == '=') { // We've reached a padding character: truncate the string from this point - return new String(buffer, 0, i); + var encodedPadded = new String(buffer, 0, i); + ArrayPool.Shared.Return(buffer); + return encodedPadded; } } // If we got this far, the buffer didn't contain any padding chars, so turn // it directly into a string. - return new String(buffer, 0, numBase64Chars); + var encoded = new String(buffer, 0, numBase64Chars); + ArrayPool.Shared.Return(buffer); + return encoded; } private static int GetNumBase64CharsRequiredForInput(int inputLength) From ca3104beb1f59fd31bdd90b500e97bfe9b00bec5 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 02:58:35 +0000 Subject: [PATCH 09/15] System.Buffers in ContentDispositionHeaderValue --- .../ContentDispositionHeaderValue.cs | 12 +++++++++++- src/Microsoft.Net.Http.Headers/project.json | 10 ++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs index 6a3cb271..5eb393fe 100644 --- a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; @@ -578,12 +579,13 @@ private bool TryDecode5987(string input, out string output) } var decoded = new StringBuilder(); + byte[] unescapedBytes = null; try { var encoding = Encoding.GetEncoding(parts[0]); var dataString = parts[2]; - var unescapedBytes = new byte[dataString.Length]; + unescapedBytes = ArrayPool.Shared.Rent(dataString.Length); var unescapedBytesCount = 0; for (var index = 0; index < dataString.Length; index++) { @@ -613,9 +615,17 @@ private bool TryDecode5987(string input, out string output) } catch (ArgumentException) { + if (unescapedBytes != null) + { + ArrayPool.Shared.Return(unescapedBytes); + } return false; // Unknown encoding or bad characters } + if (unescapedBytes != null) + { + ArrayPool.Shared.Return(unescapedBytes); + } output = decoded.ToString(); return true; } diff --git a/src/Microsoft.Net.Http.Headers/project.json b/src/Microsoft.Net.Http.Headers/project.json index 4dddd21e..b85adc85 100644 --- a/src/Microsoft.Net.Http.Headers/project.json +++ b/src/Microsoft.Net.Http.Headers/project.json @@ -9,9 +9,15 @@ "warningsAsErrors": true, "keyFile": "../../tools/Key.snk" }, - "dependencies": {}, + "dependencies": { + "System.Buffers": "4.0.0-*" + }, "frameworks": { - "net451": {}, + "net451": { + "frameworkAssemblies": { + "System.Runtime": "" + } + }, "dotnet5.4": { "dependencies": { "System.Collections": "4.0.11-*", From 887d784123f672f1e856973d9cd7d20b49648706 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 03:03:30 +0000 Subject: [PATCH 10/15] System.Buffers in OwinWebSocketAdapter --- .../WebSockets/OwinWebSocketAdapter.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.AspNetCore.Owin/WebSockets/OwinWebSocketAdapter.cs b/src/Microsoft.AspNetCore.Owin/WebSockets/OwinWebSocketAdapter.cs index 17bd2ede..f5919206 100644 --- a/src/Microsoft.AspNetCore.Owin/WebSockets/OwinWebSocketAdapter.cs +++ b/src/Microsoft.AspNetCore.Owin/WebSockets/OwinWebSocketAdapter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -126,11 +127,18 @@ public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string s await CloseOutputAsync(closeStatus, statusDescription, cancellationToken); } - byte[] buffer = new byte[1024]; - while (State == WebSocketState.CloseSent) + var buffer = ArrayPool.Shared.Rent(1024); + try { - // Drain until close received - await ReceiveAsync(new ArraySegment(buffer), cancellationToken); + while (State == WebSocketState.CloseSent) + { + // Drain until close received + await ReceiveAsync(new ArraySegment(buffer), cancellationToken); + } + } + finally + { + ArrayPool.Shared.Return(buffer); } } From bc8c6f6edc18dffe356d4e7a8509a530b4681e09 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 04:41:49 +0000 Subject: [PATCH 11/15] Allow Shared ArrayPool to be overridden --- .../BufferedReadStream.cs | 11 ++++++++-- .../FormReader.cs | 19 +++++++++++++++--- .../MultipartReaderStream.cs | 20 +++++++++++++++---- .../StreamHelperExtensions.cs | 10 +++++++--- .../WebEncoders.cs | 20 ++++++++++++++----- 5 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/BufferedReadStream.cs b/src/Microsoft.AspNetCore.WebUtilities/BufferedReadStream.cs index 1726378b..f9ea281d 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/BufferedReadStream.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/BufferedReadStream.cs @@ -17,11 +17,17 @@ internal class BufferedReadStream : Stream private readonly Stream _inner; private readonly byte[] _buffer; + private readonly ArrayPool _bytePool; private int _bufferOffset = 0; private int _bufferCount = 0; private bool _disposed; public BufferedReadStream(Stream inner, int bufferSize) + : this(inner, bufferSize, ArrayPool.Shared) + { + } + + public BufferedReadStream(Stream inner, int bufferSize, ArrayPool bytePool) { if (inner == null) { @@ -29,7 +35,8 @@ public BufferedReadStream(Stream inner, int bufferSize) } _inner = inner; - _buffer = ArrayPool.Shared.Rent(bufferSize); + _bytePool = bytePool; + _buffer = bytePool.Rent(bufferSize); } public ArraySegment BufferedData @@ -132,7 +139,7 @@ protected override void Dispose(bool disposing) if (!_disposed) { _disposed = true; - ArrayPool.Shared.Return(_buffer); + _bytePool.Return(_buffer); if (disposing) { diff --git a/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs b/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs index 778fd8d7..be1efbf3 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs @@ -19,23 +19,35 @@ public class FormReader : IDisposable { private readonly TextReader _reader; private readonly char[] _buffer; + private readonly ArrayPool _charPool; private readonly StringBuilder _builder = new StringBuilder(); private int _bufferOffset; private int _bufferCount; private bool _disposed; public FormReader(string data) + : this(data, ArrayPool.Shared) + { + } + + public FormReader(string data, ArrayPool charPool) { if (data == null) { throw new ArgumentNullException(nameof(data)); } - _buffer = ArrayPool.Shared.Rent(1024); + _buffer = charPool.Rent(1024); + _charPool = charPool; _reader = new StringReader(data); } public FormReader(Stream stream, Encoding encoding) + : this(stream, encoding, ArrayPool.Shared) + { + } + + public FormReader(Stream stream, Encoding encoding, ArrayPool charPool) { if (stream == null) { @@ -47,7 +59,8 @@ public FormReader(Stream stream, Encoding encoding) throw new ArgumentNullException(nameof(encoding)); } - _buffer = ArrayPool.Shared.Rent(1024); + _buffer = charPool.Rent(1024); + _charPool = charPool; _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); } @@ -221,7 +234,7 @@ public void Dispose() if (!_disposed) { _disposed = true; - ArrayPool.Shared.Return(_buffer); + _charPool.Return(_buffer); } } } diff --git a/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs b/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs index 0c7a1714..3715120e 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs @@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.WebUtilities internal class MultipartReaderStream : Stream { private readonly BufferedReadStream _innerStream; + private readonly ArrayPool _bytePool; private readonly byte[] _boundaryBytes; private readonly int _finalBoundaryLength; private readonly long _innerOffset; @@ -27,6 +28,16 @@ internal class MultipartReaderStream : Stream /// /// public MultipartReaderStream(BufferedReadStream stream, string boundary, bool expectLeadingCrlf = true) + : this(stream, boundary, expectLeadingCrlf, ArrayPool.Shared) + { + } + + /// + /// Creates a stream that reads until it reaches the given boundary pattern. + /// + /// + /// + public MultipartReaderStream(BufferedReadStream stream, string boundary, bool expectLeadingCrlf, ArrayPool bytePool) { if (stream == null) { @@ -38,6 +49,7 @@ public MultipartReaderStream(BufferedReadStream stream, string boundary, bool ex throw new ArgumentNullException(nameof(boundary)); } + _bytePool = bytePool; _innerStream = stream; _innerOffset = _innerStream.CanSeek ? _innerStream.Position : 0; if (expectLeadingCrlf) @@ -228,7 +240,7 @@ public override int Read(byte[] buffer, int offset, int count) // "The boundary may be followed by zero or more characters of // linear whitespace. It is then terminated by either another CRLF" // or -- for the final boundary. - var boundary = ArrayPool.Shared.Rent(_boundaryBytes.Length); + var boundary = _bytePool.Rent(_boundaryBytes.Length); read = _innerStream.Read(boundary, 0, _boundaryBytes.Length); Debug.Assert(read == _boundaryBytes.Length); // It should have all been buffered var remainder = _innerStream.ReadLine(lengthLimit: 100); // Whitespace may exceed the buffer. @@ -237,7 +249,7 @@ public override int Read(byte[] buffer, int offset, int count) { FinalBoundaryFound = true; } - ArrayPool.Shared.Return(boundary); + _bytePool.Return(boundary); Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); _finished = true; return 0; @@ -280,7 +292,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, // "The boundary may be followed by zero or more characters of // linear whitespace. It is then terminated by either another CRLF" // or -- for the final boundary. - var boundary = ArrayPool.Shared.Rent(_boundaryBytes.Length); + var boundary = _bytePool.Rent(_boundaryBytes.Length); read = _innerStream.Read(boundary, 0, _boundaryBytes.Length); Debug.Assert(read == _boundaryBytes.Length); // It should have all been buffered var remainder = await _innerStream.ReadLineAsync(lengthLimit: 100, cancellationToken: cancellationToken); // Whitespace may exceed the buffer. @@ -289,7 +301,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, { FinalBoundaryFound = true; } - ArrayPool.Shared.Return(boundary); + _bytePool.Return(boundary); Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); _finished = true; diff --git a/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs b/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs index 679ca98b..4c8b4af8 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs @@ -10,10 +10,14 @@ namespace Microsoft.AspNetCore.WebUtilities { public static class StreamHelperExtensions { - public static async Task DrainAsync(this Stream stream, CancellationToken cancellationToken) + public static Task DrainAsync(this Stream stream, CancellationToken cancellationToken) + { + return stream.DrainAsync(cancellationToken, ArrayPool.Shared); + } + public static async Task DrainAsync(this Stream stream, CancellationToken cancellationToken, ArrayPool bytePool) { cancellationToken.ThrowIfCancellationRequested(); - var buffer = ArrayPool.Shared.Rent(1024); + var buffer = bytePool.Rent(1024); try { while (await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken) > 0) @@ -24,7 +28,7 @@ public static async Task DrainAsync(this Stream stream, CancellationToken cancel } finally { - ArrayPool.Shared.Return(buffer); + bytePool.Return(buffer); } } } diff --git a/src/Microsoft.AspNetCore.WebUtilities/WebEncoders.cs b/src/Microsoft.AspNetCore.WebUtilities/WebEncoders.cs index 31f0fc4f..180298b2 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/WebEncoders.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/WebEncoders.cs @@ -43,6 +43,11 @@ public static byte[] Base64UrlDecode(string input) /// Throws FormatException if the input is malformed. /// public static byte[] Base64UrlDecode(string input, int offset, int count) + { + return Base64UrlDecode(input, offset, count, ArrayPool.Shared); + } + + public static byte[] Base64UrlDecode(string input, int offset, int count, ArrayPool charPool) { if (input == null) { @@ -62,7 +67,7 @@ public static byte[] Base64UrlDecode(string input, int offset, int count) // First, we need to add the padding characters back. var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); var length = checked(count + numPaddingCharsToAdd); - var completeBase64Array = ArrayPool.Shared.Rent(length); + var completeBase64Array = charPool.Rent(length); Debug.Assert(completeBase64Array.Length % 4 == 0, "Invariant: Array length must be a multiple of 4."); input.CopyTo(offset, completeBase64Array, 0, count); for (var i = 1; i <= numPaddingCharsToAdd; i++) @@ -87,7 +92,7 @@ public static byte[] Base64UrlDecode(string input, int offset, int count) // Finally, decode. // If the caller provided invalid base64 chars, they'll be caught here. var decoded = Convert.FromBase64CharArray(completeBase64Array, 0, length); - ArrayPool.Shared.Return(completeBase64Array); + charPool.Return(completeBase64Array); return decoded; } @@ -114,6 +119,11 @@ public static string Base64UrlEncode(byte[] input) /// The number of bytes of to encode. /// The base64url-encoded form of the input. public static string Base64UrlEncode(byte[] input, int offset, int count) + { + return Base64UrlEncode(input, offset, count, ArrayPool.Shared); + } + + public static string Base64UrlEncode(byte[] input, int offset, int count, ArrayPool charPool) { if (input == null) { @@ -130,7 +140,7 @@ public static string Base64UrlEncode(byte[] input, int offset, int count) // We're going to use base64url encoding with no padding characters. // See RFC 4648, Sec. 5. - var buffer = ArrayPool.Shared.Rent(GetNumBase64CharsRequiredForInput(count)); + var buffer = charPool.Rent(GetNumBase64CharsRequiredForInput(count)); int numBase64Chars = Convert.ToBase64CharArray(input, offset, count, buffer, 0); // Fix up '+' -> '-' and '/' -> '_' @@ -149,7 +159,7 @@ public static string Base64UrlEncode(byte[] input, int offset, int count) { // We've reached a padding character: truncate the string from this point var encodedPadded = new String(buffer, 0, i); - ArrayPool.Shared.Return(buffer); + charPool.Return(buffer); return encodedPadded; } } @@ -157,7 +167,7 @@ public static string Base64UrlEncode(byte[] input, int offset, int count) // If we got this far, the buffer didn't contain any padding chars, so turn // it directly into a string. var encoded = new String(buffer, 0, numBase64Chars); - ArrayPool.Shared.Return(buffer); + charPool.Return(buffer); return encoded; } From 592af43beabad50b71960eca045681c53af0cbe0 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 3 Feb 2016 08:05:46 +0000 Subject: [PATCH 12/15] System.Buffers in FileBufferingReadStream --- .../FileBufferingReadStream.cs | 73 ++++++++++++++++--- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs b/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs index a80f02a5..47525dc6 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Diagnostics; using System.IO; using System.Threading; @@ -17,11 +18,13 @@ namespace Microsoft.AspNetCore.WebUtilities public class FileBufferingReadStream : Stream { private readonly Stream _inner; + private readonly ArrayPool _bytePool; private readonly int _memoryThreshold; private string _tempFileDirectory; private readonly Func _tempFileDirectoryAccessor; - private Stream _buffer = new MemoryStream(); // TODO: We could have a more efficiently expanding buffer stream. + private Stream _buffer; + private byte[] _rentedBuffer; private bool _inMemory = true; private bool _completelyBuffered; @@ -32,6 +35,15 @@ public FileBufferingReadStream( Stream inner, int memoryThreshold, Func tempFileDirectoryAccessor) + : this(inner, memoryThreshold, tempFileDirectoryAccessor, ArrayPool.Shared) + { + } + + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + Func tempFileDirectoryAccessor, + ArrayPool bytePool) { if (inner == null) { @@ -43,6 +55,17 @@ public FileBufferingReadStream( throw new ArgumentNullException(nameof(tempFileDirectoryAccessor)); } + _bytePool = bytePool; + if (memoryThreshold < 1024 * 1024) + { + _rentedBuffer = bytePool.Rent(memoryThreshold); + _buffer = new MemoryStream(_rentedBuffer); + } + else + { + _buffer = new MemoryStream(); + } + _inner = inner; _memoryThreshold = memoryThreshold; _tempFileDirectoryAccessor = tempFileDirectoryAccessor; @@ -50,6 +73,14 @@ public FileBufferingReadStream( // TODO: allow for an optional buffer size limit to prevent filling hard disks. 1gb? public FileBufferingReadStream(Stream inner, int memoryThreshold, string tempFileDirectory) + { + } + + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + string tempFileDirectory, + ArrayPool bytePool) { if (inner == null) { @@ -61,6 +92,17 @@ public FileBufferingReadStream(Stream inner, int memoryThreshold, string tempFil throw new ArgumentNullException(nameof(tempFileDirectory)); } + _bytePool = bytePool; + if (memoryThreshold < 1024 * 1024) + { + _rentedBuffer = bytePool.Rent(memoryThreshold); + _buffer = new MemoryStream(_rentedBuffer); + } + else + { + _buffer = new MemoryStream(); + } + _inner = inner; _memoryThreshold = memoryThreshold; _tempFileDirectory = tempFileDirectory; @@ -145,11 +187,15 @@ public override int Read(byte[] buffer, int offset, int count) if (_inMemory && _buffer.Length + read > _memoryThreshold) { - var oldBuffer = _buffer; - _buffer = CreateTempFile(); _inMemory = false; - oldBuffer.Position = 0; - oldBuffer.CopyTo(_buffer, 1024 * 16); + checked + { + int length = (int)_buffer.Length; + _buffer = CreateTempFile(); + _buffer.Write(_rentedBuffer, 0, length); + _bytePool.Return(_rentedBuffer); + _rentedBuffer = null; + } } if (read > 0) @@ -216,11 +262,15 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, if (_inMemory && _buffer.Length + read > _memoryThreshold) { - var oldBuffer = _buffer; - _buffer = CreateTempFile(); _inMemory = false; - oldBuffer.Position = 0; - await oldBuffer.CopyToAsync(_buffer, 1024 * 16, cancellationToken); + checked + { + int length = (int)_buffer.Length; + _buffer = CreateTempFile(); + await _buffer.WriteAsync(_rentedBuffer, 0, length, cancellationToken); + _bytePool.Return(_rentedBuffer); + _rentedBuffer = null; + } } if (read > 0) @@ -270,6 +320,11 @@ protected override void Dispose(bool disposing) if (!_disposed) { _disposed = true; + if (_rentedBuffer != null) + { + _bytePool.Return(_rentedBuffer); + } + if (disposing) { _buffer.Dispose(); From 660fd32cb40845b732716cf8b83165109fcf9c8b Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 4 Feb 2016 00:24:22 +0000 Subject: [PATCH 13/15] Halve string creations when form reading --- .../FormReader.cs | 178 +++++++++++++++++- 1 file changed, 171 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs b/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs index be1efbf3..81a99728 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs @@ -19,6 +19,7 @@ public class FormReader : IDisposable { private readonly TextReader _reader; private readonly char[] _buffer; + private readonly ArrayPool _bytePool; private readonly ArrayPool _charPool; private readonly StringBuilder _builder = new StringBuilder(); private int _bufferOffset; @@ -26,11 +27,11 @@ public class FormReader : IDisposable private bool _disposed; public FormReader(string data) - : this(data, ArrayPool.Shared) + : this(data, ArrayPool.Shared, ArrayPool.Shared) { } - public FormReader(string data, ArrayPool charPool) + public FormReader(string data, ArrayPool bytePool, ArrayPool charPool) { if (data == null) { @@ -39,15 +40,16 @@ public FormReader(string data, ArrayPool charPool) _buffer = charPool.Rent(1024); _charPool = charPool; + _bytePool = bytePool; _reader = new StringReader(data); } public FormReader(Stream stream, Encoding encoding) - : this(stream, encoding, ArrayPool.Shared) + : this(stream, encoding, ArrayPool.Shared, ArrayPool.Shared) { } - public FormReader(Stream stream, Encoding encoding, ArrayPool charPool) + public FormReader(Stream stream, Encoding encoding, ArrayPool bytePool, ArrayPool charPool) { if (stream == null) { @@ -61,6 +63,7 @@ public FormReader(Stream stream, Encoding encoding, ArrayPool charPool) _buffer = charPool.Rent(1024); _charPool = charPool; + _bytePool = bytePool; _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); } @@ -157,10 +160,13 @@ private async Task ReadWordAsync(char seperator, CancellationToken cance // '+' un-escapes to ' ', %HH un-escapes as ASCII (or utf-8?) private string BuildWord() { - _builder.Replace('+', ' '); - var result = _builder.ToString(); + if (_builder.Length == 0) + { + return string.Empty; + } + var result = UrlDecode(_builder, _bytePool, _charPool); _builder.Clear(); - return Uri.UnescapeDataString(result); // TODO: Replace this, it's not completely accurate. + return result; } private void Buffer() @@ -237,5 +243,163 @@ public void Dispose() _charPool.Return(_buffer); } } + + // Algorithm from https://github.com/dotnet/corefx/blob/ac67ffac987d0c27236c4a6cf1255c2bcbc7fe7d/src/System.Runtime.Extensions/src/System/Net/WebUtility.cs#L378 + // but accepting StringBuilder + private string UrlDecode(StringBuilder value, ArrayPool bytePool, ArrayPool charPool) + { + if (value == null) + { + return null; + } + + var count = value.Length; + UrlDecoder helper = new UrlDecoder(count, Encoding.UTF8, bytePool, charPool); + + // go through the string's chars collapsing %XX and + // appending each char as char, with exception of %XX constructs + // that are appended as bytes + + for (var pos = 0; pos < count; pos++) + { + var ch = value[pos]; + + if (ch == '+') + { + ch = ' '; + } + else if (ch == '%' && pos < count - 2) + { + var h1 = HexToInt(value[pos + 1]); + var h2 = HexToInt(value[pos + 2]); + + if (h1 >= 0 && h2 >= 0) + { // valid 2 hex chars + var b = (byte)((h1 << 4) | h2); + pos += 2; + + // don't add as char + helper.AddByte(b); + continue; + } + } + + if ((ch & 0xFF80) == 0) + { + helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode + } + else + { + helper.AddChar(ch); + } + } + + var decodedString = helper.GetString(); + helper.Dispose(); + + return decodedString; + } + + // from https://github.com/dotnet/corefx/blob/ac67ffac987d0c27236c4a6cf1255c2bcbc7fe7d/src/System.Runtime.Extensions/src/System/Net/WebUtility.cs#L529 + // as private static there + private static int HexToInt(char h) + { + return (h >= '0' && h <= '9') ? h - '0' : + (h >= 'a' && h <= 'f') ? h - 'a' + 10 : + (h >= 'A' && h <= 'F') ? h - 'A' + 10 : + -1; + } + + // from https://github.com/dotnet/corefx/blob/ac67ffac987d0c27236c4a6cf1255c2bcbc7fe7d/src/System.Runtime.Extensions/src/System/Net/WebUtility.cs#L610 + // but lower allocation struct + private struct UrlDecoder + { + private int _bufferSize; + + // Accumulate characters in a special array + private int _numChars; + private char[] _charBuffer; + + // Accumulate bytes for decoding into characters in a special array + private int _numBytes; + private byte[] _byteBuffer; + + // Encoding to convert chars to bytes + private Encoding _encoding; + + private ArrayPool _bytePool; + private ArrayPool _charPool; + + public UrlDecoder(int bufferSize, Encoding encoding, ArrayPool bytePool, ArrayPool charPool) + { + _bufferSize = bufferSize; + _encoding = encoding; + _numChars = 0; + _numBytes = 0; + + _bytePool = bytePool; + _charPool = charPool; + + _byteBuffer = null; + _charBuffer = charPool.Rent(bufferSize); + // byte buffer created on demand + } + + private void FlushBytes() + { + if (_numBytes > 0) + { + _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars); + _numBytes = 0; + } + } + + public void AddChar(char ch) + { + if (_numBytes > 0) + { + FlushBytes(); + } + + _charBuffer[_numChars++] = ch; + } + + public void AddByte(byte b) + { + if (_byteBuffer == null) + { + _byteBuffer = _bytePool.Rent(_bufferSize); + } + + _byteBuffer[_numBytes++] = b; + } + + public string GetString() + { + if (_numBytes > 0) + { + FlushBytes(); + } + + if (_numChars > 0) + { + return new string(_charBuffer, 0, _numChars); + } + else + { + return string.Empty; + } + } + + public void Dispose() + { + _charPool.Return(_charBuffer); + + if (_byteBuffer != null) + { + _bytePool.Return(_byteBuffer); + } + } + } } } \ No newline at end of file From c30e9f4efd921a1bc1ad2f30788b6ec781ee256f Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Fri, 5 Feb 2016 00:26:01 +0000 Subject: [PATCH 14/15] cr feedback --- .../FileBufferingReadStream.cs | 1 + .../MultipartReaderStream.cs | 4 ++-- .../StreamHelperExtensions.cs | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs b/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs index 47525dc6..07deca41 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs @@ -73,6 +73,7 @@ public FileBufferingReadStream( // TODO: allow for an optional buffer size limit to prevent filling hard disks. 1gb? public FileBufferingReadStream(Stream inner, int memoryThreshold, string tempFileDirectory) + : this (inner, memoryThreshold, tempFileDirectory, ArrayPool.Shared) { } diff --git a/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs b/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs index 3715120e..02738e43 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs @@ -242,6 +242,7 @@ public override int Read(byte[] buffer, int offset, int count) // or -- for the final boundary. var boundary = _bytePool.Rent(_boundaryBytes.Length); read = _innerStream.Read(boundary, 0, _boundaryBytes.Length); + _bytePool.Return(boundary); Debug.Assert(read == _boundaryBytes.Length); // It should have all been buffered var remainder = _innerStream.ReadLine(lengthLimit: 100); // Whitespace may exceed the buffer. remainder = remainder.Trim(); @@ -249,7 +250,6 @@ public override int Read(byte[] buffer, int offset, int count) { FinalBoundaryFound = true; } - _bytePool.Return(boundary); Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); _finished = true; return 0; @@ -294,6 +294,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, // or -- for the final boundary. var boundary = _bytePool.Rent(_boundaryBytes.Length); read = _innerStream.Read(boundary, 0, _boundaryBytes.Length); + _bytePool.Return(boundary); Debug.Assert(read == _boundaryBytes.Length); // It should have all been buffered var remainder = await _innerStream.ReadLineAsync(lengthLimit: 100, cancellationToken: cancellationToken); // Whitespace may exceed the buffer. remainder = remainder.Trim(); @@ -301,7 +302,6 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, { FinalBoundaryFound = true; } - _bytePool.Return(boundary); Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); _finished = true; diff --git a/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs b/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs index 4c8b4af8..ffd8435c 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs @@ -12,9 +12,9 @@ public static class StreamHelperExtensions { public static Task DrainAsync(this Stream stream, CancellationToken cancellationToken) { - return stream.DrainAsync(cancellationToken, ArrayPool.Shared); + return stream.DrainAsync(ArrayPool.Shared, cancellationToken); } - public static async Task DrainAsync(this Stream stream, CancellationToken cancellationToken, ArrayPool bytePool) + public static async Task DrainAsync(this Stream stream, ArrayPool bytePool, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var buffer = bytePool.Rent(1024); From a4103fca1379eb91956558b9d45df682d1256f77 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 11 Feb 2016 10:17:09 +0000 Subject: [PATCH 15/15] Bufferpool, Perf-testing --- .../BufferingHelper.cs | 2 +- .../FileBufferingReadStream.cs | 27 +-- .../FormReader.cs | 171 +----------------- 3 files changed, 21 insertions(+), 179 deletions(-) diff --git a/src/Microsoft.AspNetCore.Http/BufferingHelper.cs b/src/Microsoft.AspNetCore.Http/BufferingHelper.cs index 80f95ecb..8fb39dab 100644 --- a/src/Microsoft.AspNetCore.Http/BufferingHelper.cs +++ b/src/Microsoft.AspNetCore.Http/BufferingHelper.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Http.Internal { public static class BufferingHelper { - internal const int DefaultBufferThreshold = 1024 * 30; + internal const int DefaultBufferThreshold = 32768; private readonly static Func _getTempDirectory = () => TempDirectory; diff --git a/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs b/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs index 07deca41..dd4719d8 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/FileBufferingReadStream.cs @@ -25,6 +25,7 @@ public class FileBufferingReadStream : Stream private Stream _buffer; private byte[] _rentedBuffer; + private int _readIntoBuffer = 0; private bool _inMemory = true; private bool _completelyBuffered; @@ -73,13 +74,13 @@ public FileBufferingReadStream( // TODO: allow for an optional buffer size limit to prevent filling hard disks. 1gb? public FileBufferingReadStream(Stream inner, int memoryThreshold, string tempFileDirectory) - : this (inner, memoryThreshold, tempFileDirectory, ArrayPool.Shared) + : this(inner, memoryThreshold, tempFileDirectory, ArrayPool.Shared) { } public FileBufferingReadStream( - Stream inner, - int memoryThreshold, + Stream inner, + int memoryThreshold, string tempFileDirectory, ArrayPool bytePool) { @@ -178,26 +179,27 @@ private Stream CreateTempFile() public override int Read(byte[] buffer, int offset, int count) { ThrowIfDisposed(); - if (_buffer.Position < _buffer.Length || _completelyBuffered) + if (_buffer.Position < _readIntoBuffer || _completelyBuffered) { // Just read from the buffer - return _buffer.Read(buffer, offset, (int)Math.Min(count, _buffer.Length - _buffer.Position)); + return _buffer.Read(buffer, offset, (int)Math.Min(count, _readIntoBuffer - _buffer.Position)); } int read = _inner.Read(buffer, offset, count); - if (_inMemory && _buffer.Length + read > _memoryThreshold) + if (_inMemory && _readIntoBuffer + read > _memoryThreshold) { _inMemory = false; checked { - int length = (int)_buffer.Length; + int length = _readIntoBuffer; _buffer = CreateTempFile(); _buffer.Write(_rentedBuffer, 0, length); _bytePool.Return(_rentedBuffer); _rentedBuffer = null; } } + _readIntoBuffer += read; if (read > 0) { @@ -253,26 +255,27 @@ public override int EndRead(IAsyncResult asyncResult) public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ThrowIfDisposed(); - if (_buffer.Position < _buffer.Length || _completelyBuffered) + if (_buffer.Position < _readIntoBuffer || _completelyBuffered) { // Just read from the buffer - return await _buffer.ReadAsync(buffer, offset, (int)Math.Min(count, _buffer.Length - _buffer.Position), cancellationToken); + return await _buffer.ReadAsync(buffer, offset, (int)Math.Min(count, _readIntoBuffer - _buffer.Position), cancellationToken); } int read = await _inner.ReadAsync(buffer, offset, count, cancellationToken); - if (_inMemory && _buffer.Length + read > _memoryThreshold) + if (_inMemory && _readIntoBuffer + read > _memoryThreshold) { _inMemory = false; checked { - int length = (int)_buffer.Length; + int length = _readIntoBuffer; _buffer = CreateTempFile(); await _buffer.WriteAsync(_rentedBuffer, 0, length, cancellationToken); _bytePool.Return(_rentedBuffer); _rentedBuffer = null; } } + _readIntoBuffer += read; if (read > 0) { @@ -341,4 +344,4 @@ private void ThrowIfDisposed() } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs b/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs index 81a99728..e9dee2ff 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs @@ -38,7 +38,7 @@ public FormReader(string data, ArrayPool bytePool, ArrayPool charPoo throw new ArgumentNullException(nameof(data)); } - _buffer = charPool.Rent(1024); + _buffer = charPool.Rent(8192); _charPool = charPool; _bytePool = bytePool; _reader = new StringReader(data); @@ -61,7 +61,7 @@ public FormReader(Stream stream, Encoding encoding, ArrayPool bytePool, Ar throw new ArgumentNullException(nameof(encoding)); } - _buffer = charPool.Rent(1024); + _buffer = charPool.Rent(8192); _charPool = charPool; _bytePool = bytePool; _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); @@ -160,13 +160,10 @@ private async Task ReadWordAsync(char seperator, CancellationToken cance // '+' un-escapes to ' ', %HH un-escapes as ASCII (or utf-8?) private string BuildWord() { - if (_builder.Length == 0) - { - return string.Empty; - } - var result = UrlDecode(_builder, _bytePool, _charPool); + _builder.Replace('+', ' '); + var result = _builder.ToString(); _builder.Clear(); - return result; + return Uri.UnescapeDataString(result); // TODO: Replace this, it's not completely accurate. } private void Buffer() @@ -243,163 +240,5 @@ public void Dispose() _charPool.Return(_buffer); } } - - // Algorithm from https://github.com/dotnet/corefx/blob/ac67ffac987d0c27236c4a6cf1255c2bcbc7fe7d/src/System.Runtime.Extensions/src/System/Net/WebUtility.cs#L378 - // but accepting StringBuilder - private string UrlDecode(StringBuilder value, ArrayPool bytePool, ArrayPool charPool) - { - if (value == null) - { - return null; - } - - var count = value.Length; - UrlDecoder helper = new UrlDecoder(count, Encoding.UTF8, bytePool, charPool); - - // go through the string's chars collapsing %XX and - // appending each char as char, with exception of %XX constructs - // that are appended as bytes - - for (var pos = 0; pos < count; pos++) - { - var ch = value[pos]; - - if (ch == '+') - { - ch = ' '; - } - else if (ch == '%' && pos < count - 2) - { - var h1 = HexToInt(value[pos + 1]); - var h2 = HexToInt(value[pos + 2]); - - if (h1 >= 0 && h2 >= 0) - { // valid 2 hex chars - var b = (byte)((h1 << 4) | h2); - pos += 2; - - // don't add as char - helper.AddByte(b); - continue; - } - } - - if ((ch & 0xFF80) == 0) - { - helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode - } - else - { - helper.AddChar(ch); - } - } - - var decodedString = helper.GetString(); - helper.Dispose(); - - return decodedString; - } - - // from https://github.com/dotnet/corefx/blob/ac67ffac987d0c27236c4a6cf1255c2bcbc7fe7d/src/System.Runtime.Extensions/src/System/Net/WebUtility.cs#L529 - // as private static there - private static int HexToInt(char h) - { - return (h >= '0' && h <= '9') ? h - '0' : - (h >= 'a' && h <= 'f') ? h - 'a' + 10 : - (h >= 'A' && h <= 'F') ? h - 'A' + 10 : - -1; - } - - // from https://github.com/dotnet/corefx/blob/ac67ffac987d0c27236c4a6cf1255c2bcbc7fe7d/src/System.Runtime.Extensions/src/System/Net/WebUtility.cs#L610 - // but lower allocation struct - private struct UrlDecoder - { - private int _bufferSize; - - // Accumulate characters in a special array - private int _numChars; - private char[] _charBuffer; - - // Accumulate bytes for decoding into characters in a special array - private int _numBytes; - private byte[] _byteBuffer; - - // Encoding to convert chars to bytes - private Encoding _encoding; - - private ArrayPool _bytePool; - private ArrayPool _charPool; - - public UrlDecoder(int bufferSize, Encoding encoding, ArrayPool bytePool, ArrayPool charPool) - { - _bufferSize = bufferSize; - _encoding = encoding; - _numChars = 0; - _numBytes = 0; - - _bytePool = bytePool; - _charPool = charPool; - - _byteBuffer = null; - _charBuffer = charPool.Rent(bufferSize); - // byte buffer created on demand - } - - private void FlushBytes() - { - if (_numBytes > 0) - { - _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars); - _numBytes = 0; - } - } - - public void AddChar(char ch) - { - if (_numBytes > 0) - { - FlushBytes(); - } - - _charBuffer[_numChars++] = ch; - } - - public void AddByte(byte b) - { - if (_byteBuffer == null) - { - _byteBuffer = _bytePool.Rent(_bufferSize); - } - - _byteBuffer[_numBytes++] = b; - } - - public string GetString() - { - if (_numBytes > 0) - { - FlushBytes(); - } - - if (_numChars > 0) - { - return new string(_charBuffer, 0, _numChars); - } - else - { - return string.Empty; - } - } - - public void Dispose() - { - _charPool.Return(_charBuffer); - - if (_byteBuffer != null) - { - _bytePool.Return(_byteBuffer); - } - } - } } } \ No newline at end of file