Skip to content

Commit e3b3db3

Browse files
committed
ReadLine reimplementation
1 parent cd7538b commit e3b3db3

File tree

4 files changed

+240
-26
lines changed

4 files changed

+240
-26
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Diagnostics;
2+
using System.IO;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using BenchmarkDotNet.Attributes;
6+
using BenchmarkDotNet.Attributes.Jobs;
7+
8+
namespace Microsoft.AspNetCore.WebUtilities
9+
{
10+
[SimpleJob]
11+
public class HttpRequestStreamReaderReadLineBenchmark
12+
{
13+
private MemoryStream _stream;
14+
15+
[Params(200, 1000, 1025, 1600)] // Default buffer length is 1024
16+
public int Length { get; set; }
17+
18+
[GlobalSetup]
19+
public void GlobalSetup()
20+
{
21+
var data = new char[Length];
22+
23+
data[Length - 2] = '\r';
24+
data[Length - 1] = '\n';
25+
26+
_stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
27+
}
28+
29+
[Benchmark]
30+
public async Task<string> ReadLineAsync()
31+
{
32+
var reader = CreateReader();
33+
var result = await reader.ReadLineAsync();
34+
Debug.Assert(result.Length == Length - 2);
35+
return result;
36+
}
37+
38+
[Benchmark]
39+
public string ReadLine()
40+
{
41+
var reader = CreateReader();
42+
var result = reader.ReadLine();
43+
Debug.Assert(result.Length == Length - 2);
44+
return result;
45+
}
46+
47+
[Benchmark]
48+
public HttpRequestStreamReader CreateReader()
49+
{
50+
_stream.Seek(0, SeekOrigin.Begin);
51+
return new HttpRequestStreamReader(_stream, Encoding.UTF8);
52+
}
53+
}
54+
}

src/Http/WebUtilities/perf/Microsoft.AspNetCore.WebUtilities.Performance/Microsoft.AspNetCore.WebUtilities.Performance.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>

src/Http/WebUtilities/src/HttpRequestStreamReader.cs

Lines changed: 126 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -340,50 +340,138 @@ public override async Task<string> ReadLineAsync()
340340
if (_disposed)
341341
{
342342
throw new ObjectDisposedException(nameof(HttpRequestStreamReader));
343-
}
343+
}
344+
345+
StringBuilder sb = null;
346+
var consumeLineFeed = false;
344347

345-
StringBuilder sb = new StringBuilder();
346348
while (true)
347349
{
348350
if (_charBufferIndex == _charsRead)
349351
{
350352
if (await ReadIntoBufferAsync() == 0)
351353
{
352-
break; // reached EOF, we need to return null if we were at EOF from the beginning
354+
// reached EOF, we need to return null if we were at EOF from the beginning
355+
return sb?.ToString();
353356
}
354357
}
355358

356-
var ch = _charBuffer[_charBufferIndex++];
357-
358-
if (ch == '\r' || ch == '\n')
359+
var stepResult = ReadLineStep(ref sb, ref consumeLineFeed);
360+
361+
if (stepResult.Completed)
359362
{
360-
if (ch == '\r')
363+
return stepResult.Result ?? sb?.ToString();
364+
}
365+
366+
continue;
367+
}
368+
}
369+
370+
// Reads a line. A line is defined as a sequence of characters followed by
371+
// a carriage return ('\r'), a line feed ('\n'), or a carriage return
372+
// immediately followed by a line feed. The resulting string does not
373+
// contain the terminating carriage return and/or line feed. The returned
374+
// value is null if the end of the input stream has been reached.
375+
public override string ReadLine()
376+
{
377+
if (_disposed)
378+
{
379+
throw new ObjectDisposedException(nameof(HttpRequestStreamReader));
380+
}
381+
382+
StringBuilder sb = null;
383+
var consumeLineFeed = false;
384+
385+
while (true)
386+
{
387+
if (_charBufferIndex == _charsRead)
388+
{
389+
if (ReadIntoBuffer() == 0)
361390
{
362-
if (_charBufferIndex == _charsRead)
391+
// reached EOF, we need to return null if we were at EOF from the beginning
392+
return sb?.ToString();
393+
}
394+
}
395+
396+
var stepResult = ReadLineStep(ref sb, ref consumeLineFeed);
397+
398+
if (stepResult.Completed)
399+
{
400+
return stepResult.Result ?? sb?.ToString();
401+
}
402+
403+
continue;
404+
}
405+
}
406+
407+
private ReadLineStepResult ReadLineStep(ref StringBuilder sb, ref bool consumeLineFeed)
408+
{
409+
if (consumeLineFeed)
410+
{
411+
if (_charBuffer[_charBufferIndex] == '\n')
412+
{
413+
_charBufferIndex++;
414+
}
415+
return ReadLineStepResult.Done;
416+
}
417+
418+
var span = new Span<char>(_charBuffer, _charBufferIndex, _charsRead - _charBufferIndex);
419+
420+
var index = span.IndexOfAny('\r', '\n');
421+
422+
if (index != -1)
423+
{
424+
if (span[index] == '\r')
425+
{
426+
span = span.Slice(0, index);
427+
_charBufferIndex += index + 1;
428+
429+
if (_charBufferIndex < _charsRead)
430+
{
431+
// consume following \n
432+
if (_charBuffer[_charBufferIndex] == '\n')
363433
{
364-
if (await ReadIntoBufferAsync() == 0)
365-
{
366-
return sb.ToString(); // reached EOF
367-
}
434+
_charBufferIndex++;
368435
}
369436

370-
if (_charBuffer[_charBufferIndex] == '\n')
437+
if (sb != null)
371438
{
372-
_charBufferIndex++; // consume the \n character
439+
sb.Append(span);
440+
return ReadLineStepResult.Done;
373441
}
442+
443+
// perf: if the new line is found in first pass, we skip the StringBuilder
444+
return ReadLineStepResult.FromResult(span.ToString());
374445
}
375446

376-
return sb.ToString();
447+
// we where at the end of buffer, we need to read more to check for a line feed to consume
448+
sb ??= new StringBuilder();
449+
sb.Append(span);
450+
consumeLineFeed = true;
451+
return ReadLineStepResult.Continue;
377452
}
378-
sb.Append(ch);
379-
}
380453

381-
if (sb.Length > 0)
382-
{
383-
return sb.ToString();
454+
if (span[index] == '\n')
455+
{
456+
span = span.Slice(0, index);
457+
_charBufferIndex += index + 1;
458+
459+
if (sb != null)
460+
{
461+
sb.Append(span);
462+
return ReadLineStepResult.Done;
463+
}
464+
465+
// perf: if the new line is found in first pass, we skip the StringBuilder
466+
return ReadLineStepResult.FromResult(span.ToString());
467+
}
384468
}
385469

386-
return null;
470+
sb ??= new StringBuilder();
471+
sb.Append(span);
472+
_charBufferIndex = _charsRead;
473+
474+
return ReadLineStepResult.Continue;
387475
}
388476

389477
private int ReadIntoBuffer()
@@ -445,5 +533,22 @@ private async Task<int> ReadIntoBufferAsync()
445533

446534
return _charsRead;
447535
}
536+
537+
private readonly struct ReadLineStepResult
538+
{
539+
public static readonly ReadLineStepResult Done = new ReadLineStepResult(true, null);
540+
public static readonly ReadLineStepResult Continue = new ReadLineStepResult(false, null);
541+
542+
public static ReadLineStepResult FromResult(string value) => new ReadLineStepResult(true, value);
543+
544+
private ReadLineStepResult(bool completed, string result)
545+
{
546+
Completed = completed;
547+
Result = result;
548+
}
549+
550+
public bool Completed { get; }
551+
public string Result { get; }
552+
}
448553
}
449554
}

src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,63 @@ public static async Task ReadLine_MultipleContinuousLines(Func<HttpRequestStream
194194
Assert.Equal(string.Empty, data);
195195
}
196196

197-
var eol = await action(reader);
198-
Assert.Null(eol);
197+
var eof = await action(reader);
198+
Assert.Null(eof);
199+
}
200+
201+
[Theory]
202+
[MemberData(nameof(ReadLineData))]
203+
public static async Task ReadLine_CarriageReturnAndLineFeedAcrossBufferBundaries(Func<HttpRequestStreamReader, Task<string>> action)
204+
{
205+
// Arrange
206+
var stream = new MemoryStream();
207+
var writer = new StreamWriter(stream);
208+
writer.Write("123456789\r\nfoo");
209+
writer.Flush();
210+
stream.Position = 0;
211+
212+
var reader = new HttpRequestStreamReader(stream, Encoding.UTF8, 10);
213+
214+
// Act & Assert
215+
var data = await action(reader);
216+
Assert.Equal("123456789", data);
217+
218+
data = await action(reader);
219+
Assert.Equal("foo", data);
220+
221+
var eof = await action(reader);
222+
Assert.Null(eof);
223+
}
224+
225+
[Theory]
226+
[MemberData(nameof(ReadLineData))]
227+
public static async Task ReadLine_EOF(Func<HttpRequestStreamReader, Task<string>> action)
228+
{
229+
// Arrange
230+
var stream = new MemoryStream();
231+
var reader = new HttpRequestStreamReader(stream, Encoding.UTF8);
232+
233+
// Act & Assert
234+
var eof = await action(reader);
235+
Assert.Null(eof);
236+
}
237+
238+
[Theory]
239+
[MemberData(nameof(ReadLineData))]
240+
public static async Task ReadLine_NewLineOnly(Func<HttpRequestStreamReader, Task<string>> action)
241+
{
242+
// Arrange
243+
var stream = new MemoryStream();
244+
var writer = new StreamWriter(stream);
245+
writer.Write("\r\n");
246+
writer.Flush();
247+
stream.Position = 0;
248+
249+
var reader = new HttpRequestStreamReader(stream, Encoding.UTF8);
250+
251+
// Act & Assert
252+
var empty = await action(reader);
253+
Assert.Equal(string.Empty, empty);
199254
}
200255

201256
[Fact]
@@ -416,10 +471,10 @@ public static IEnumerable<object[]> HttpRequestDisposeDataAsync()
416471
public static IEnumerable<object[]> ReadLineData()
417472
{
418473
yield return new object[] { new Func<HttpRequestStreamReader, Task<string>>((httpRequestStreamReader) =>
419-
httpRequestStreamReader.ReadLineAsync()
474+
Task.FromResult(httpRequestStreamReader.ReadLine())
420475
)};
421476
yield return new object[] { new Func<HttpRequestStreamReader, Task<string>>((httpRequestStreamReader) =>
422-
Task.FromResult(httpRequestStreamReader.ReadLine())
477+
httpRequestStreamReader.ReadLineAsync()
423478
)};
424479
}
425480
}

0 commit comments

Comments
 (0)