Skip to content

Commit 794f99e

Browse files
author
ladeak
committed
Http3FramingStreamWriter perf improvements (10%-500%)
1 parent 129d4b9 commit 794f99e

File tree

3 files changed

+160
-77
lines changed

3 files changed

+160
-77
lines changed

src/CHttpServer/CHttpServer/Http3/Http3FramingStreamWriter.cs

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,22 @@ private readonly struct Segment()
1313

1414
public byte[] Reference { get; init; } = [];
1515
public Memory<byte> Used { get; init; } = Memory<byte>.Empty;
16+
17+
public bool IsEmpty => Used.IsEmpty;
18+
19+
public bool IsAllocated => Reference.Length > 0;
1620
}
1721

1822
private readonly byte[] _buffer = new byte[9];
1923
private readonly Lock _lockObject = new();
20-
private readonly List<Segment> _segments = new List<Segment>(128) { new Segment() };
24+
private readonly List<Segment> _segments = new List<Segment>(128);
2125
private readonly ArrayPool<byte> _memoryPool = memoryPool ?? ArrayPool<byte>.Shared;
2226
private Stream _responseStream = responseStream;
2327
private readonly byte _frameType = frameType;
2428
private CancellationTokenSource? _cts;
2529
private bool _isCompleted = false;
2630
private long _unflushedBytes;
31+
private Segment _currentSegment = Segment.Empty;
2732
private TaskCompletionSource _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
2833
private Func<CancellationToken, Task>? _onResponseStartingCallback = onResponseStartingCallback;
2934

@@ -60,12 +65,11 @@ public void Reset(Stream responseStream, Func<CancellationToken, Task>? onRespon
6065
public override void Advance(int bytes)
6166
{
6267
ThrowIfCompleted();
63-
var current = _segments[^1];
68+
var current = _currentSegment;
6469
var usedLength = current.Used.Length;
6570
var available = current.Reference.Length - usedLength;
6671
ArgumentOutOfRangeException.ThrowIfLessThan(available, bytes);
67-
current = current with { Used = current.Reference.AsMemory(0, usedLength + bytes) };
68-
_segments[^1] = current;
72+
_currentSegment = current with { Used = current.Reference.AsMemory(0, usedLength + bytes) };
6973
_unflushedBytes += bytes;
7074
}
7175

@@ -111,23 +115,19 @@ public override Memory<byte> GetMemory(int sizeHint = 0)
111115
{
112116
ThrowIfCompleted();
113117
ArgumentOutOfRangeException.ThrowIfLessThan(sizeHint, 0);
114-
var current = _segments[^1];
118+
var current = _currentSegment;
115119
var available = current.Reference.Length - current.Used.Length;
116120
if (sizeHint <= available)
117121
return current.Reference.AsMemory(current.Used.Length);
118122

119-
var segment = new Segment() { Reference = _memoryPool.Rent(sizeHint) }; // Minimum size left for ArrayPool.
123+
// Not enough memory left in the current segment.
124+
if (!current.IsEmpty)
125+
_segments.Add(current);
126+
else if (current.IsAllocated)
127+
_memoryPool.Return(current.Reference, true);
120128

121-
// If it is unused segment, but too small, return the currently rented array and replace the segment.
122-
if (current.Used.Length == 0)
123-
{
124-
if (current.Reference.Length != 0)
125-
_memoryPool.Return(current.Reference, true);
126-
_segments[^1] = segment;
127-
}
128-
else
129-
_segments.Add(segment);
130-
return segment.Reference.AsMemory();
129+
_currentSegment = new Segment() { Reference = _memoryPool.Rent(sizeHint) }; // Minimum size left for ArrayPool.
130+
return _currentSegment.Reference.AsMemory();
131131
}
132132

133133
public override Span<byte> GetSpan(int sizeHint = 0) => GetMemory(sizeHint).Span;
@@ -189,21 +189,20 @@ public override async ValueTask<FlushResult> FlushAsync(CancellationToken cancel
189189
var dataFrameHeaderLength = PrepareFrameHeader(_unflushedBytes);
190190
await _responseStream.WriteAsync(_buffer.AsMemory(0, dataFrameHeaderLength), localToken);
191191

192-
int i = 0;
193-
var emptySegment = Segment.Empty;
194-
for (; i < _segments.Count; i++)
192+
// Shortcut: if flushed after every write, no need to address segments
193+
if (_segments.Count == 0)
195194
{
196-
var memory = _segments[i];
197-
if (memory.Reference.Length == 0)
198-
break;
199-
if (memory.Used.Length > 0)
200-
await _responseStream.WriteAsync(memory.Used, localToken);
201-
_memoryPool.Return(memory.Reference, true);
202-
_segments[i] = emptySegment;
195+
if (!_currentSegment.IsEmpty)
196+
await _responseStream.WriteAsync(_currentSegment.Used, localToken);
197+
if (_currentSegment.IsAllocated)
198+
_memoryPool.Return(_currentSegment.Reference, true);
199+
_currentSegment = Segment.Empty;
200+
_unflushedBytes = 0;
201+
await _responseStream.FlushAsync();
202+
return new FlushResult(false, false);
203203
}
204-
_unflushedBytes = 0;
205-
await _responseStream.FlushAsync(localToken);
206-
return new FlushResult(isCanceled: false, isCompleted: false);
204+
205+
return await FlushAllSegmentsAsync(localToken);
207206
}
208207
catch (OperationCanceledException)
209208
{
@@ -233,6 +232,27 @@ public override async ValueTask<FlushResult> FlushAsync(CancellationToken cancel
233232
}
234233
}
235234

235+
private async Task<FlushResult> FlushAllSegmentsAsync(CancellationToken localToken)
236+
{
237+
if (!_currentSegment.IsEmpty)
238+
_segments.Add(_currentSegment);
239+
else if (_currentSegment.IsAllocated)
240+
_memoryPool.Return(_currentSegment.Reference, true);
241+
_currentSegment = Segment.Empty;
242+
243+
for (int i = 0; i < _segments.Count; i++)
244+
{
245+
var memory = _segments[i];
246+
if (!memory.IsEmpty)
247+
await _responseStream.WriteAsync(memory.Used, localToken);
248+
_memoryPool.Return(memory.Reference, true);
249+
}
250+
_segments.Clear();
251+
_unflushedBytes = 0;
252+
await _responseStream.FlushAsync(localToken);
253+
return new FlushResult(isCanceled: false, isCompleted: false);
254+
}
255+
236256
private void Flush()
237257
{
238258
if (_unflushedBytes == 0)
@@ -245,19 +265,40 @@ private void Flush()
245265
}
246266
var dataFrameHeaderLength = PrepareFrameHeader(_unflushedBytes);
247267
_responseStream.Write(_buffer.AsSpan(0, dataFrameHeaderLength));
268+
269+
// Shortcut: if flushed after every write, no need to address segments
270+
if (_segments.Count == 0)
271+
{
272+
if (!_currentSegment.IsEmpty)
273+
_responseStream.Write(_currentSegment.Used.Span);
274+
if (_currentSegment.IsAllocated)
275+
_memoryPool.Return(_currentSegment.Reference, true);
276+
_currentSegment = Segment.Empty;
277+
_unflushedBytes = 0;
278+
_responseStream.Flush();
279+
return;
280+
}
281+
282+
FlushAllSegments();
283+
}
284+
285+
private void FlushAllSegments()
286+
{
287+
if (!_currentSegment.IsEmpty)
288+
_segments.Add(_currentSegment);
289+
else if (_currentSegment.IsAllocated)
290+
_memoryPool.Return(_currentSegment.Reference, true);
291+
_currentSegment = Segment.Empty;
292+
248293
var source = CollectionsMarshal.AsSpan(_segments);
249-
int i = 0;
250-
var emptySegment = Segment.Empty;
251-
for (; i < _segments.Count; i++)
294+
for (int i = 0; i < _segments.Count; i++)
252295
{
253296
ref var memory = ref source[i];
254-
if (memory.Reference.Length == 0)
255-
break;
256297
if (memory.Used.Length > 0)
257298
_responseStream.Write(memory.Used.Span);
258299
_memoryPool.Return(memory.Reference, true);
259-
source[i] = emptySegment;
260300
}
301+
_segments.Clear();
261302
_unflushedBytes = 0;
262303
_responseStream.Flush();
263304
}
@@ -290,7 +331,9 @@ private void ClearSegments(Span<Segment> source)
290331
}
291332
_unflushedBytes = 0;
292333
_segments.Clear();
293-
_segments.Add(Segment.Empty);
334+
if (_currentSegment.IsAllocated)
335+
_memoryPool.Return(_currentSegment.Reference);
336+
_currentSegment = Segment.Empty;
294337
}
295338

296339
private void ThrowIfCompleted()

tests/CHttp.Benchmarks/Program.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,24 @@ public bool EncodeLong()
206206
return QPackIntegerEncoder.TryEncode(buffer, Value, 7, out _);
207207
}
208208
}
209+
210+
[SimpleJob, DisassemblyDiagnoser]
211+
public class Http3FramingStreamWriterBenchmarks
212+
{
213+
private Http3FramingStreamWriter _writer = new Http3FramingStreamWriter(Stream.Null, 0);
214+
215+
[Benchmark]
216+
public async Task FlushAsync()
217+
{
218+
Span<byte> data = [0, 1, 2, 3, 4];
219+
var memory0 = _writer.GetMemory(data.Length);
220+
data.CopyTo(memory0.Span);
221+
_writer.Advance(data.Length);
222+
223+
var memory1 = _writer.GetMemory(data.Length);
224+
data.CopyTo(memory1.Span);
225+
_writer.Advance(data.Length);
226+
227+
await _writer.FlushAsync();
228+
}
229+
}

tests/CHttpServer.Tests/Http3/Http3FramingStreamWriterTests.cs

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Buffers;
2+
using System.Formats.Asn1;
23
using System.Net.Quic;
34
using System.Runtime.InteropServices;
45
using CHttpServer.Http3;
@@ -362,6 +363,24 @@ public async Task WriteAsync_Into_ClosedStream()
362363
Assert.Equal(0, arrayPool.OutstandingBytes);
363364
}
364365

366+
[Fact]
367+
public async Task GetMemory_WhenNoDataAvailable_InSegment()
368+
{
369+
var ms = new MemoryStream();
370+
var sut = new Http3FramingStreamWriter(ms, 0);
371+
Span<byte> data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
372+
var memory0 = sut.GetMemory(data.Length);
373+
data.CopyTo(memory0.Span);
374+
sut.Advance(data.Length);
375+
376+
var memory1 = sut.GetMemory(data.Length);
377+
data.CopyTo(memory1.Span);
378+
sut.Advance(data.Length);
379+
380+
await sut.FlushAsync(TestContext.Current.CancellationToken);
381+
Assert.Equal(24, ms.Length);
382+
}
383+
365384
[Fact]
366385
public async Task GetMemory_WhenDataAvailable_InSegment()
367386
{
@@ -581,58 +600,58 @@ public async Task WriteAsync_Writes_FrameType()
581600
}
582601

583602
private class WaitBeforeWriteStream(TaskCompletionSource tcs) : MemoryStream
584-
{
585-
public int WrittenBytes { get; private set; }
603+
{
604+
public int WrittenBytes { get; private set; }
586605

587-
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
588-
{
589-
await tcs.Task;
590-
await base.WriteAsync(buffer, cancellationToken);
591-
WrittenBytes += buffer.Length;
592-
}
606+
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
607+
{
608+
await tcs.Task;
609+
await base.WriteAsync(buffer, cancellationToken);
610+
WrittenBytes += buffer.Length;
593611
}
612+
}
594613

595-
private class WaitAfterWriteStream(TaskCompletionSource tcs) : MemoryStream
596-
{
597-
public int WrittenBytes { get; private set; }
614+
private class WaitAfterWriteStream(TaskCompletionSource tcs) : MemoryStream
615+
{
616+
public int WrittenBytes { get; private set; }
598617

599-
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
600-
{
601-
await base.WriteAsync(buffer, cancellationToken);
602-
WrittenBytes += buffer.Length;
603-
await tcs.Task;
604-
}
618+
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
619+
{
620+
await base.WriteAsync(buffer, cancellationToken);
621+
WrittenBytes += buffer.Length;
622+
await tcs.Task;
605623
}
624+
}
606625

607-
private class TestArrayPool : ArrayPool<byte>
608-
{
609-
private readonly ArrayPool<byte> _internalPool;
610-
private readonly List<byte[]> _rentedArrays = new();
626+
private class TestArrayPool : ArrayPool<byte>
627+
{
628+
private readonly ArrayPool<byte> _internalPool;
629+
private readonly List<byte[]> _rentedArrays = new();
611630

612-
public TestArrayPool()
613-
{
614-
_internalPool = Shared;
615-
}
631+
public TestArrayPool()
632+
{
633+
_internalPool = Shared;
634+
}
616635

617-
public TestArrayPool(int maxArrayLength, int maxArrayPerBucket)
618-
{
619-
_internalPool = Create(maxArrayLength, maxArrayPerBucket);
620-
}
636+
public TestArrayPool(int maxArrayLength, int maxArrayPerBucket)
637+
{
638+
_internalPool = Create(maxArrayLength, maxArrayPerBucket);
639+
}
621640

622-
public int OutstandingBytes => _rentedArrays.Sum(x => x.Length);
641+
public int OutstandingBytes => _rentedArrays.Sum(x => x.Length);
623642

624-
public override byte[] Rent(int minimumLength)
625-
{
626-
var buffer = _internalPool.Rent(minimumLength);
627-
_rentedArrays.Add(buffer);
628-
return buffer;
629-
}
643+
public override byte[] Rent(int minimumLength)
644+
{
645+
var buffer = _internalPool.Rent(minimumLength);
646+
_rentedArrays.Add(buffer);
647+
return buffer;
648+
}
630649

631-
public override void Return(byte[] array, bool clearArray = false)
632-
{
633-
_internalPool.Return(array, clearArray);
634-
_rentedArrays.Remove(array);
635-
}
650+
public override void Return(byte[] array, bool clearArray = false)
651+
{
652+
_internalPool.Return(array, clearArray);
653+
_rentedArrays.Remove(array);
636654
}
637655
}
656+
}
638657

0 commit comments

Comments
 (0)