From 0ee149c8dc0106bd84175f4a3a5376545c9b219c Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Sun, 7 Feb 2016 09:40:49 +0000 Subject: [PATCH] Use ValueTask in FormReading Addressing Task allocations in #553 Unfortunately this has a return type split between in public api net451 and dotnet5.4 --- .../FormReader.cs | 150 ++++++++++++++---- .../project.json | 6 +- 2 files changed, 127 insertions(+), 29 deletions(-) diff --git a/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs b/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs index 81a99728..26fc861e 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/FormReader.cs @@ -90,14 +90,50 @@ public FormReader(Stream stream, Encoding encoding, ArrayPool bytePool, Ar /// /// /// The next key value pair, or null when the end of the form is reached. - public async Task?> ReadNextPairAsync(CancellationToken cancellationToken) + public ValueTask?> ReadNextPairAsync(CancellationToken cancellationToken) { - var key = await ReadWordAsync('=', cancellationToken); + var keyTask = ReadWordAsync('=', cancellationToken); + if (!keyTask.IsCompletedSuccessfully) + { + return ReadNextPairAsyncKeyAwaited(keyTask.AsTask(), cancellationToken); + } + var key = keyTask.Result; + if (string.IsNullOrEmpty(key) && _bufferCount == 0) + { + return (KeyValuePair?)null; + } + + var valueTask = ReadWordAsync('&', cancellationToken); + if (!valueTask.IsCompletedSuccessfully) + { + return ReadNextPairAsyncValueAwaited(key, valueTask.AsTask(), cancellationToken); + } + + var value = valueTask.Result; + return new KeyValuePair(key, value); + } + + private async Task?> ReadNextPairAsyncKeyAwaited(Task keyTask, CancellationToken cancellationToken) + { + var key = await keyTask; if (string.IsNullOrEmpty(key) && _bufferCount == 0) { return null; } - var value = await ReadWordAsync('&', cancellationToken); + + var valueTask = ReadWordAsync('&', cancellationToken); + if (!valueTask.IsCompletedSuccessfully) + { + var value = await valueTask; + return new KeyValuePair(key, value); + } + + return new KeyValuePair(key, valueTask.Result); + } + + private async Task?> ReadNextPairAsyncValueAwaited(string key, Task valueTask, CancellationToken cancellationToken) + { + var value = await valueTask; return new KeyValuePair(key, value); } @@ -112,24 +148,33 @@ private string ReadWord(char seperator) Buffer(); } - // End - if (_bufferCount == 0) + var word = ReadWordImpl(seperator); + if (word != null) { - return BuildWord(); + return word; } + } + } - var c = _buffer[_bufferOffset++]; - _bufferCount--; + private ValueTask ReadWordAsync(char seperator, CancellationToken cancellationToken) + { + // TODO: Configurable value size limit + while (true) + { + // Empty + if (_bufferCount == 0) + { + return ReadWordAsyncAwaited(seperator, cancellationToken); + } - if (c == seperator) + var word = ReadWordImpl(seperator); + if (word != null) { - return BuildWord(); + return word; } - _builder.Append(c); } } - - private async Task ReadWordAsync(char seperator, CancellationToken cancellationToken) + private async Task ReadWordAsyncAwaited(char seperator, CancellationToken cancellationToken) { // TODO: Configurable value size limit while (true) @@ -140,21 +185,32 @@ private async Task ReadWordAsync(char seperator, CancellationToken cance await BufferAsync(cancellationToken); } - // End - if (_bufferCount == 0) + var word = ReadWordImpl(seperator); + if (word != null) { - return BuildWord(); + return word; } + } + } + + private string ReadWordImpl(char seperator) + { + // End + if (_bufferCount == 0) + { + return BuildWord(); + } - var c = _buffer[_bufferOffset++]; - _bufferCount--; + var c = _buffer[_bufferOffset++]; + _bufferCount--; - if (c == seperator) - { - return BuildWord(); - } - _builder.Append(c); + if (c == seperator) + { + return BuildWord(); } + _builder.Append(c); + + return null; } // '+' un-escapes to ' ', %HH un-escapes as ASCII (or utf-8?) @@ -209,7 +265,7 @@ public static Dictionary ReadForm(string text) /// /// The HTTP form body to parse. /// The collection containing the parsed HTTP form body. - public static Task> ReadFormAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken()) + public static ValueTask> ReadFormAsync(Stream stream, CancellationToken cancellationToken = default(CancellationToken)) { return ReadFormAsync(stream, Encoding.UTF8, cancellationToken); } @@ -219,12 +275,52 @@ public static Dictionary ReadForm(string text) /// /// The HTTP form body to parse. /// The collection containing the parsed HTTP form body. - public static async Task> ReadFormAsync(Stream stream, Encoding encoding, CancellationToken cancellationToken = new CancellationToken()) + public static ValueTask> ReadFormAsync(Stream stream, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) { - using (var reader = new FormReader(stream, encoding)) + var isAsync = false; + var reader = new FormReader(stream, encoding); + try { var accumulator = new KeyValueAccumulator(); - var pair = await reader.ReadNextPairAsync(cancellationToken); + + var pairTask = reader.ReadNextPairAsync(cancellationToken); + if (!pairTask.IsCompletedSuccessfully) + { + isAsync = true; + return ReadFormAsyncAwaited(pairTask.AsTask(), reader, accumulator, cancellationToken); + } + + var pair = pairTask.Result; + while (pair.HasValue) + { + accumulator.Append(pair.Value.Key, pair.Value.Value); + + pairTask = reader.ReadNextPairAsync(cancellationToken); + if (!pairTask.IsCompletedSuccessfully) + { + isAsync = true; + return ReadFormAsyncAwaited(pairTask.AsTask(), reader, accumulator, cancellationToken); + } + + pair = pairTask.Result; + } + + return accumulator.GetResults(); + } + finally + { + if (!isAsync) + { + reader.Dispose(); + } + } + } + + private static async Task> ReadFormAsyncAwaited(Task?> pairTask, FormReader reader, KeyValueAccumulator accumulator, CancellationToken cancellationToken) + { + using (reader) + { + var pair = await pairTask; while (pair.HasValue) { accumulator.Append(pair.Value.Key, pair.Value.Value); diff --git a/src/Microsoft.AspNetCore.WebUtilities/project.json b/src/Microsoft.AspNetCore.WebUtilities/project.json index 9ff39f43..ab27c3b1 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/project.json +++ b/src/Microsoft.AspNetCore.WebUtilities/project.json @@ -12,12 +12,14 @@ "dependencies": { "Microsoft.Extensions.Primitives": "1.0.0-*", "System.Buffers": "4.0.0-*", - "System.Text.Encodings.Web": "4.0.0-*" + "System.Text.Encodings.Web": "4.0.0-*", + "System.Threading.Tasks.Extensions": "4.0.0-*" }, "frameworks": { "net451": { "frameworkAssemblies": { - "System.Runtime": "" + "System.Runtime": "", + "System.Threading.Tasks": "4.0.0.0" } }, "dotnet5.4": {