Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Commit c63f02c

Browse files
committed
Optimize form reader allocations
1 parent 485e2e8 commit c63f02c

File tree

4 files changed

+168
-50
lines changed

4 files changed

+168
-50
lines changed

src/Microsoft.AspNetCore.WebUtilities/FormReader.cs

Lines changed: 88 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ public class FormReader : IDisposable
2828
private readonly StringBuilder _builder = new StringBuilder();
2929
private int _bufferOffset;
3030
private int _bufferCount;
31+
private string _currentKey;
32+
private string _currentValue;
33+
private bool _endOfStream;
3134
private bool _disposed;
3235

3336
public FormReader(string data)
@@ -97,13 +100,29 @@ public FormReader(Stream stream, Encoding encoding, ArrayPool<char> charPool)
97100
/// <returns>The next key value pair, or null when the end of the form is reached.</returns>
98101
public KeyValuePair<string, string>? ReadNextPair()
99102
{
100-
var key = ReadWord('=', KeyLengthLimit);
101-
if (string.IsNullOrEmpty(key) && _bufferCount == 0)
103+
ReadNextPairImpl();
104+
if (ReadSucceded())
102105
{
103-
return null;
106+
return new KeyValuePair<string, string>(_currentKey, _currentValue);
107+
}
108+
return null;
109+
}
110+
111+
private void ReadNextPairImpl()
112+
{
113+
StartReadNextPair();
114+
while (!_endOfStream)
115+
{
116+
// Empty
117+
if (_bufferCount == 0)
118+
{
119+
Buffer();
120+
}
121+
if (TryReadNextPair())
122+
{
123+
break;
124+
}
104125
}
105-
var value = ReadWord('&', ValueLengthLimit);
106-
return new KeyValuePair<string, string>(key, value);
107126
}
108127

109128
// Format: key1=value1&key2=value2
@@ -114,49 +133,72 @@ public FormReader(Stream stream, Encoding encoding, ArrayPool<char> charPool)
114133
/// <returns>The next key value pair, or null when the end of the form is reached.</returns>
115134
public async Task<KeyValuePair<string, string>?> ReadNextPairAsync(CancellationToken cancellationToken = new CancellationToken())
116135
{
117-
var key = await ReadWordAsync('=', KeyLengthLimit, cancellationToken);
118-
if (string.IsNullOrEmpty(key) && _bufferCount == 0)
136+
await ReadNextPairAsyncImpl(cancellationToken);
137+
if (ReadSucceded())
119138
{
120-
return null;
139+
return new KeyValuePair<string, string>(_currentKey, _currentValue);
121140
}
122-
var value = await ReadWordAsync('&', ValueLengthLimit, cancellationToken);
123-
return new KeyValuePair<string, string>(key, value);
141+
return null;
124142
}
125143

126-
private string ReadWord(char seperator, int limit)
144+
private async Task ReadNextPairAsyncImpl(CancellationToken cancellationToken = new CancellationToken())
127145
{
128-
while (true)
146+
StartReadNextPair();
147+
while (!_endOfStream)
129148
{
130149
// Empty
131150
if (_bufferCount == 0)
132151
{
133-
Buffer();
152+
await BufferAsync(cancellationToken);
134153
}
135-
136-
string word;
137-
if (ReadChar(seperator, limit, out word))
154+
if (TryReadNextPair())
138155
{
139-
return word;
156+
break;
140157
}
141158
}
142159
}
143160

144-
private async Task<string> ReadWordAsync(char seperator, int limit, CancellationToken cancellationToken)
161+
private void StartReadNextPair()
162+
{
163+
_currentKey = null;
164+
_currentValue = null;
165+
}
166+
167+
private bool TryReadNextPair()
145168
{
146-
while (true)
169+
if (_currentKey == null)
147170
{
148-
// Empty
171+
if (!TryReadWord('=', KeyLengthLimit, out _currentKey))
172+
{
173+
return false;
174+
}
175+
149176
if (_bufferCount == 0)
150177
{
151-
await BufferAsync(cancellationToken);
178+
return false;
152179
}
180+
}
153181

154-
string word;
155-
if (ReadChar(seperator, limit, out word))
182+
if (_currentValue == null)
183+
{
184+
if (!TryReadWord('&', ValueLengthLimit, out _currentValue))
156185
{
157-
return word;
186+
return false;
158187
}
159188
}
189+
return true;
190+
}
191+
192+
private bool TryReadWord(char seperator, int limit, out string value)
193+
{
194+
do
195+
{
196+
if (ReadChar(seperator, limit, out value))
197+
{
198+
return true;
199+
}
200+
} while (_bufferCount > 0);
201+
return false;
160202
}
161203

162204
private bool ReadChar(char seperator, int limit, out string word)
@@ -198,6 +240,7 @@ private void Buffer()
198240
{
199241
_bufferOffset = 0;
200242
_bufferCount = _reader.Read(_buffer, 0, _buffer.Length);
243+
_endOfStream = _bufferCount == 0;
201244
}
202245

203246
private async Task BufferAsync(CancellationToken cancellationToken)
@@ -206,6 +249,7 @@ private async Task BufferAsync(CancellationToken cancellationToken)
206249
cancellationToken.ThrowIfCancellationRequested();
207250
_bufferOffset = 0;
208251
_bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length);
252+
_endOfStream = _bufferCount == 0;
209253
}
210254

211255
/// <summary>
@@ -215,17 +259,11 @@ private async Task BufferAsync(CancellationToken cancellationToken)
215259
public Dictionary<string, StringValues> ReadForm()
216260
{
217261
var accumulator = new KeyValueAccumulator();
218-
var pair = ReadNextPair();
219-
while (pair.HasValue)
262+
while (!_endOfStream)
220263
{
221-
accumulator.Append(pair.Value.Key, pair.Value.Value);
222-
if (accumulator.Count > KeyCountLimit)
223-
{
224-
throw new InvalidDataException($"Form key count limit {KeyCountLimit} exceeded.");
225-
}
226-
pair = ReadNextPair();
264+
ReadNextPairImpl();
265+
Append(ref accumulator);
227266
}
228-
229267
return accumulator.GetResults();
230268
}
231269

@@ -237,18 +275,29 @@ public Dictionary<string, StringValues> ReadForm()
237275
public async Task<Dictionary<string, StringValues>> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken())
238276
{
239277
var accumulator = new KeyValueAccumulator();
240-
var pair = await ReadNextPairAsync(cancellationToken);
241-
while (pair.HasValue)
278+
while (!_endOfStream)
279+
{
280+
await ReadNextPairAsyncImpl(cancellationToken);
281+
Append(ref accumulator);
282+
}
283+
return accumulator.GetResults();
284+
}
285+
286+
private bool ReadSucceded()
287+
{
288+
return _currentKey != null && _currentValue != null;
289+
}
290+
291+
private void Append(ref KeyValueAccumulator accumulator)
292+
{
293+
if (ReadSucceded())
242294
{
243-
accumulator.Append(pair.Value.Key, pair.Value.Value);
295+
accumulator.Append(_currentKey, _currentValue);
244296
if (accumulator.Count > KeyCountLimit)
245297
{
246298
throw new InvalidDataException($"Form key count limit {KeyCountLimit} exceeded.");
247299
}
248-
pair = await ReadNextPairAsync(cancellationToken);
249300
}
250-
251-
return accumulator.GetResults();
252301
}
253302

254303
public void Dispose()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Threading.Tasks;
6+
using Microsoft.Extensions.Primitives;
7+
8+
namespace Microsoft.AspNetCore.WebUtilities
9+
{
10+
public class FormReaderAsyncTest : FormReaderTests
11+
{
12+
protected override async Task<Dictionary<string, StringValues>> ReadFormAsync(FormReader reader)
13+
{
14+
return await reader.ReadFormAsync();
15+
}
16+
17+
protected override async Task<KeyValuePair<string, string>?> ReadPair(FormReader reader)
18+
{
19+
return await reader.ReadNextPairAsync();
20+
}
21+
}
22+
}

test/Microsoft.AspNetCore.WebUtilities.Tests/FormReaderTests.cs

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System.Collections.Generic;
45
using System.IO;
5-
using System.Linq;
66
using System.Text;
77
using System.Threading.Tasks;
8+
using Microsoft.Extensions.Primitives;
89
using Xunit;
910

1011
namespace Microsoft.AspNetCore.WebUtilities
@@ -18,7 +19,7 @@ public async Task ReadFormAsync_EmptyKeyAtEndAllowed(bool bufferRequest)
1819
{
1920
var body = MakeStream(bufferRequest, "=bar");
2021

21-
var formCollection = await new FormReader(body).ReadFormAsync();
22+
var formCollection = await ReadFormAsync(new FormReader(body));
2223

2324
Assert.Equal("bar", formCollection[""].ToString());
2425
}
@@ -30,7 +31,7 @@ public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed(bool bufferRe
3031
{
3132
var body = MakeStream(bufferRequest, "=bar&baz=2");
3233

33-
var formCollection = await new FormReader(body).ReadFormAsync();
34+
var formCollection = await ReadFormAsync(new FormReader(body));
3435

3536
Assert.Equal("bar", formCollection[""].ToString());
3637
Assert.Equal("2", formCollection["baz"].ToString());
@@ -43,7 +44,7 @@ public async Task ReadFormAsync_EmptyValuedAtEndAllowed(bool bufferRequest)
4344
{
4445
var body = MakeStream(bufferRequest, "foo=");
4546

46-
var formCollection = await new FormReader(body).ReadFormAsync();
47+
var formCollection = await ReadFormAsync(new FormReader(body));
4748

4849
Assert.Equal("", formCollection["foo"].ToString());
4950
}
@@ -55,7 +56,7 @@ public async Task ReadFormAsync_EmptyValuedWithAdditionalEntryAllowed(bool buffe
5556
{
5657
var body = MakeStream(bufferRequest, "foo=&baz=2");
5758

58-
var formCollection = await new FormReader(body).ReadFormAsync();
59+
var formCollection = await ReadFormAsync(new FormReader(body));
5960

6061
Assert.Equal("", formCollection["foo"].ToString());
6162
Assert.Equal("2", formCollection["baz"].ToString());
@@ -68,7 +69,7 @@ public async Task ReadFormAsync_KeyCountLimitMet_Success(bool bufferRequest)
6869
{
6970
var body = MakeStream(bufferRequest, "foo=1&bar=2&baz=3&baz=4");
7071

71-
var formCollection = await new FormReader(body) { KeyCountLimit = 3 }.ReadFormAsync();
72+
var formCollection = await ReadFormAsync(new FormReader(body) { KeyCountLimit = 3 });
7273

7374
Assert.Equal("1", formCollection["foo"].ToString());
7475
Assert.Equal("2", formCollection["bar"].ToString());
@@ -84,7 +85,7 @@ public async Task ReadFormAsync_KeyCountLimitExceeded_Throw(bool bufferRequest)
8485
var body = MakeStream(bufferRequest, "foo=1&baz=2&bar=3&baz=4&baf=5");
8586

8687
var exception = await Assert.ThrowsAsync<InvalidDataException>(
87-
() => new FormReader(body) { KeyCountLimit = 3 }.ReadFormAsync());
88+
() => ReadFormAsync(new FormReader(body) { KeyCountLimit = 3 }));
8889
Assert.Equal("Form key count limit 3 exceeded.", exception.Message);
8990
}
9091

@@ -95,7 +96,7 @@ public async Task ReadFormAsync_KeyLengthLimitMet_Success(bool bufferRequest)
9596
{
9697
var body = MakeStream(bufferRequest, "foo=1&bar=2&baz=3&baz=4");
9798

98-
var formCollection = await new FormReader(body) { KeyLengthLimit = 10 }.ReadFormAsync();
99+
var formCollection = await ReadFormAsync(new FormReader(body) { KeyLengthLimit = 10 });
99100

100101
Assert.Equal("1", formCollection["foo"].ToString());
101102
Assert.Equal("2", formCollection["bar"].ToString());
@@ -111,7 +112,7 @@ public async Task ReadFormAsync_KeyLengthLimitExceeded_Throw(bool bufferRequest)
111112
var body = MakeStream(bufferRequest, "foo=1&baz1234567890=2");
112113

113114
var exception = await Assert.ThrowsAsync<InvalidDataException>(
114-
() => new FormReader(body) { KeyLengthLimit = 10 }.ReadFormAsync());
115+
() => ReadFormAsync(new FormReader(body) { KeyLengthLimit = 10 }));
115116
Assert.Equal("Form key or value length limit 10 exceeded.", exception.Message);
116117
}
117118

@@ -122,7 +123,7 @@ public async Task ReadFormAsync_ValueLengthLimitMet_Success(bool bufferRequest)
122123
{
123124
var body = MakeStream(bufferRequest, "foo=1&bar=1234567890&baz=3&baz=4");
124125

125-
var formCollection = await new FormReader(body) { ValueLengthLimit = 10 }.ReadFormAsync();
126+
var formCollection = await ReadFormAsync(new FormReader(body) { ValueLengthLimit = 10 });
126127

127128
Assert.Equal("1", formCollection["foo"].ToString());
128129
Assert.Equal("1234567890", formCollection["bar"].ToString());
@@ -138,10 +139,54 @@ public async Task ReadFormAsync_ValueLengthLimitExceeded_Throw(bool bufferReques
138139
var body = MakeStream(bufferRequest, "foo=1&baz=1234567890123");
139140

140141
var exception = await Assert.ThrowsAsync<InvalidDataException>(
141-
() => new FormReader(body) { ValueLengthLimit = 10 }.ReadFormAsync());
142+
() => ReadFormAsync(new FormReader(body) { ValueLengthLimit = 10 }));
142143
Assert.Equal("Form key or value length limit 10 exceeded.", exception.Message);
143144
}
144145

146+
[Theory]
147+
[InlineData(true)]
148+
[InlineData(false)]
149+
public async Task ReadNextPair_ReadsAllPairs(bool bufferRequest)
150+
{
151+
var body = MakeStream(bufferRequest, "foo=&baz=2");
152+
153+
var reader = new FormReader(body);
154+
155+
var pair = (KeyValuePair<string, string>)await ReadPair(reader);
156+
157+
Assert.Equal("foo", pair.Key);
158+
Assert.Equal("", pair.Value);
159+
160+
pair = (KeyValuePair<string, string>)await ReadPair(reader);
161+
162+
Assert.Equal("baz", pair.Key);
163+
Assert.Equal("2", pair.Value);
164+
165+
Assert.Null(await ReadPair(reader));
166+
}
167+
168+
[Theory]
169+
[InlineData(true)]
170+
[InlineData(false)]
171+
public async Task ReadNextPair_ReturnsNullOnEmptyStream(bool bufferRequest)
172+
{
173+
var body = MakeStream(bufferRequest, "");
174+
175+
var reader = new FormReader(body);
176+
177+
Assert.Null(await ReadPair(reader));
178+
}
179+
180+
protected virtual Task<Dictionary<string, StringValues>> ReadFormAsync(FormReader reader)
181+
{
182+
return Task.FromResult(reader.ReadForm());
183+
}
184+
185+
protected virtual Task<KeyValuePair<string, string>?> ReadPair(FormReader reader)
186+
{
187+
return Task.FromResult(reader.ReadNextPair());
188+
}
189+
145190
private static Stream MakeStream(bool bufferRequest, string text)
146191
{
147192
var formContent = Encoding.UTF8.GetBytes(text);

test/Microsoft.AspNetCore.WebUtilities.Tests/NonSeekableReadStream.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@ public override void Write(byte[] buffer, int offset, int count)
6161

6262
public override int Read(byte[] buffer, int offset, int count)
6363
{
64+
count = Math.Max(count, 1);
6465
return _inner.Read(buffer, offset, count);
6566
}
6667

6768
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
6869
{
70+
count = Math.Max(count, 1);
6971
return _inner.ReadAsync(buffer, offset, count, cancellationToken);
7072
}
7173
}

0 commit comments

Comments
 (0)