Skip to content

Commit 0a7a620

Browse files
authored
Pool SocketSenders (#30771)
- SocketAsyncEventArgs have lots of state on them and as a result are quite big (~350) bytes at runtime. We can pool these since sends are usually very fast and we can reduce the per connection overhead as a result. - We also allocate one per IOQueue to reduce contention. - Fixed buffer list management - Disposed pool when the transport is disposed - Added project to slnf so running tests in VS was possible - Clear the buffer and buffer list before returning to the pool - This cleans up dumps as the pooled senders don't see references to buffers while pooled in the queue - Keep track of items in the pool separately from the queue count.
1 parent d2a0cbc commit 0a7a620

9 files changed

+142
-74
lines changed

src/Servers/Kestrel/Kestrel.slnf

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"src\\Servers\\Kestrel\\test\\Sockets.BindTests\\Sockets.BindTests.csproj",
3636
"src\\Servers\\Kestrel\\test\\Sockets.FunctionalTests\\Sockets.FunctionalTests.csproj",
3737
"src\\Servers\\Kestrel\\tools\\CodeGenerator\\CodeGenerator.csproj",
38-
"src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj"
38+
"src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj",
39+
"src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj"
3940
]
4041
}
4142
}

src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ internal class SocketConnectionFactory : IConnectionFactory, IAsyncDisposable
2222
private readonly SocketsTrace _trace;
2323
private readonly PipeOptions _inputOptions;
2424
private readonly PipeOptions _outputOptions;
25+
private readonly SocketSenderPool _socketSenderPool;
2526

2627
public SocketConnectionFactory(IOptions<SocketTransportOptions> options, ILoggerFactory loggerFactory)
2728
{
@@ -46,9 +47,12 @@ public SocketConnectionFactory(IOptions<SocketTransportOptions> options, ILogger
4647
// These are the same, it's either the thread pool or inline
4748
var applicationScheduler = _options.UnsafePreferInlineScheduling ? PipeScheduler.Inline : PipeScheduler.ThreadPool;
4849
var transportScheduler = applicationScheduler;
50+
// https://github.com/aspnet/KestrelHttpServer/issues/2573
51+
var awaiterScheduler = OperatingSystem.IsWindows() ? transportScheduler : PipeScheduler.Inline;
4952

5053
_inputOptions = new PipeOptions(_memoryPool, applicationScheduler, transportScheduler, maxReadBufferSize, maxReadBufferSize / 2, useSynchronizationContext: false);
5154
_outputOptions = new PipeOptions(_memoryPool, transportScheduler, applicationScheduler, maxWriteBufferSize, maxWriteBufferSize / 2, useSynchronizationContext: false);
55+
_socketSenderPool = new SocketSenderPool(awaiterScheduler);
5256
}
5357

5458
public async ValueTask<ConnectionContext> ConnectAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
@@ -72,6 +76,7 @@ public async ValueTask<ConnectionContext> ConnectAsync(EndPoint endpoint, Cancel
7276
_memoryPool,
7377
_inputOptions.ReaderScheduler, // This is either threadpool or inline
7478
_trace,
79+
_socketSenderPool,
7580
_inputOptions,
7681
_outputOptions,
7782
_options.WaitForDataBeforeAllocatingBuffer);

src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketAwaitableEventArgs.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
1313
{
14-
internal sealed class SocketAwaitableEventArgs : SocketAsyncEventArgs, ICriticalNotifyCompletion
14+
internal class SocketAwaitableEventArgs : SocketAsyncEventArgs, ICriticalNotifyCompletion
1515
{
1616
private static readonly Action _callbackCompleted = () => { };
1717

src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs

+14-7
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ internal sealed class SocketConnection : TransportConnection
2020
private readonly Socket _socket;
2121
private readonly ISocketsTrace _trace;
2222
private readonly SocketReceiver _receiver;
23-
private readonly SocketSender _sender;
23+
private SocketSender? _sender;
24+
private readonly SocketSenderPool _socketSenderPool;
2425
private readonly IDuplexPipe _originalTransport;
2526
private readonly CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource();
2627

@@ -36,6 +37,7 @@ internal SocketConnection(Socket socket,
3637
MemoryPool<byte> memoryPool,
3738
PipeScheduler transportScheduler,
3839
ISocketsTrace trace,
40+
SocketSenderPool socketSenderPool,
3941
PipeOptions inputOptions,
4042
PipeOptions outputOptions,
4143
bool waitForData = true)
@@ -48,6 +50,7 @@ internal SocketConnection(Socket socket,
4850
MemoryPool = memoryPool;
4951
_trace = trace;
5052
_waitForData = waitForData;
53+
_socketSenderPool = socketSenderPool;
5154

5255
LocalEndPoint = _socket.LocalEndPoint;
5356
RemoteEndPoint = _socket.RemoteEndPoint;
@@ -59,8 +62,7 @@ internal SocketConnection(Socket socket,
5962
// https://github.com/aspnet/KestrelHttpServer/issues/2573
6063
var awaiterScheduler = OperatingSystem.IsWindows() ? transportScheduler : PipeScheduler.Inline;
6164

62-
_receiver = new SocketReceiver(_socket, awaiterScheduler);
63-
_sender = new SocketSender(_socket, awaiterScheduler);
65+
_receiver = new SocketReceiver(awaiterScheduler);
6466

6567
var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions);
6668

@@ -93,7 +95,7 @@ private async Task StartAsync()
9395
await sendTask;
9496

9597
_receiver.Dispose();
96-
_sender.Dispose();
98+
_sender?.Dispose();
9799
}
98100
catch (Exception ex)
99101
{
@@ -183,13 +185,13 @@ private async Task ProcessReceives()
183185
if (_waitForData)
184186
{
185187
// Wait for data before allocating a buffer.
186-
await _receiver.WaitForDataAsync();
188+
await _receiver.WaitForDataAsync(_socket);
187189
}
188190

189191
// Ensure we have some reasonable amount of buffer space
190192
var buffer = input.GetMemory(MinAllocBufferSize);
191193

192-
var bytesReceived = await _receiver.ReceiveAsync(buffer);
194+
var bytesReceived = await _receiver.ReceiveAsync(_socket, buffer);
193195

194196
if (bytesReceived == 0)
195197
{
@@ -282,7 +284,12 @@ private async Task ProcessSends()
282284
var isCompleted = result.IsCompleted;
283285
if (!buffer.IsEmpty)
284286
{
285-
await _sender.SendAsync(buffer);
287+
_sender = _socketSenderPool.Rent();
288+
await _sender.SendAsync(_socket, buffer);
289+
// We don't return to the pool if there was an exception, and
290+
// we keep the _sender assigned so that we can dispose it in StartAsync.
291+
_socketSenderPool.Return(_sender);
292+
_sender = null;
286293
}
287294

288295
output.AdvanceTo(end);

src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketReceiver.cs

+12-12
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,34 @@
77

88
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
99
{
10-
internal sealed class SocketReceiver : SocketSenderReceiverBase
10+
internal sealed class SocketReceiver : SocketAwaitableEventArgs
1111
{
12-
public SocketReceiver(Socket socket, PipeScheduler scheduler) : base(socket, scheduler)
12+
public SocketReceiver(PipeScheduler ioScheduler) : base(ioScheduler)
1313
{
1414
}
1515

16-
public SocketAwaitableEventArgs WaitForDataAsync()
16+
public SocketAwaitableEventArgs WaitForDataAsync(Socket socket)
1717
{
18-
_awaitableEventArgs.SetBuffer(Memory<byte>.Empty);
18+
SetBuffer(Memory<byte>.Empty);
1919

20-
if (!_socket.ReceiveAsync(_awaitableEventArgs))
20+
if (!socket.ReceiveAsync(this))
2121
{
22-
_awaitableEventArgs.Complete();
22+
Complete();
2323
}
2424

25-
return _awaitableEventArgs;
25+
return this;
2626
}
2727

28-
public SocketAwaitableEventArgs ReceiveAsync(Memory<byte> buffer)
28+
public SocketAwaitableEventArgs ReceiveAsync(Socket socket, Memory<byte> buffer)
2929
{
30-
_awaitableEventArgs.SetBuffer(buffer);
30+
SetBuffer(buffer);
3131

32-
if (!_socket.ReceiveAsync(_awaitableEventArgs))
32+
if (!socket.ReceiveAsync(this))
3333
{
34-
_awaitableEventArgs.Complete();
34+
Complete();
3535
}
3636

37-
return _awaitableEventArgs;
37+
return this;
3838
}
3939
}
4040
}

src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketSender.cs

+30-28
Original file line numberDiff line numberDiff line change
@@ -11,55 +11,61 @@
1111

1212
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
1313
{
14-
internal sealed class SocketSender : SocketSenderReceiverBase
14+
internal sealed class SocketSender : SocketAwaitableEventArgs
1515
{
1616
private List<ArraySegment<byte>>? _bufferList;
1717

18-
public SocketSender(Socket socket, PipeScheduler scheduler) : base(socket, scheduler)
18+
public SocketSender(PipeScheduler scheduler) : base(scheduler)
1919
{
2020
}
2121

22-
public SocketAwaitableEventArgs SendAsync(in ReadOnlySequence<byte> buffers)
22+
public SocketAwaitableEventArgs SendAsync(Socket socket, in ReadOnlySequence<byte> buffers)
2323
{
2424
if (buffers.IsSingleSegment)
2525
{
26-
return SendAsync(buffers.First);
26+
return SendAsync(socket, buffers.First);
2727
}
2828

29-
if (!_awaitableEventArgs.MemoryBuffer.Equals(Memory<byte>.Empty))
30-
{
31-
_awaitableEventArgs.SetBuffer(null, 0, 0);
32-
}
33-
34-
_awaitableEventArgs.BufferList = GetBufferList(buffers);
29+
SetBufferList(buffers);
3530

36-
if (!_socket.SendAsync(_awaitableEventArgs))
31+
if (!socket.SendAsync(this))
3732
{
38-
_awaitableEventArgs.Complete();
33+
Complete();
3934
}
4035

41-
return _awaitableEventArgs;
36+
return this;
4237
}
4338

44-
private SocketAwaitableEventArgs SendAsync(ReadOnlyMemory<byte> memory)
39+
public void Reset()
4540
{
46-
// The BufferList getter is much less expensive then the setter.
47-
if (_awaitableEventArgs.BufferList != null)
41+
// We clear the buffer and buffer list before we put it back into the pool
42+
// it's a small performance hit but it removes the confusion when looking at dumps to see this still
43+
// holds onto the buffer when it's back in the pool
44+
if (BufferList != null)
45+
{
46+
BufferList = null;
47+
48+
_bufferList?.Clear();
49+
}
50+
else
4851
{
49-
_awaitableEventArgs.BufferList = null;
52+
SetBuffer(null, 0, 0);
5053
}
54+
}
5155

52-
_awaitableEventArgs.SetBuffer(MemoryMarshal.AsMemory(memory));
56+
private SocketAwaitableEventArgs SendAsync(Socket socket, ReadOnlyMemory<byte> memory)
57+
{
58+
SetBuffer(MemoryMarshal.AsMemory(memory));
5359

54-
if (!_socket.SendAsync(_awaitableEventArgs))
60+
if (!socket.SendAsync(this))
5561
{
56-
_awaitableEventArgs.Complete();
62+
Complete();
5763
}
5864

59-
return _awaitableEventArgs;
65+
return this;
6066
}
6167

62-
private List<ArraySegment<byte>> GetBufferList(in ReadOnlySequence<byte> buffer)
68+
private void SetBufferList(in ReadOnlySequence<byte> buffer)
6369
{
6470
Debug.Assert(!buffer.IsEmpty);
6571
Debug.Assert(!buffer.IsSingleSegment);
@@ -68,18 +74,14 @@ private List<ArraySegment<byte>> GetBufferList(in ReadOnlySequence<byte> buffer)
6874
{
6975
_bufferList = new List<ArraySegment<byte>>();
7076
}
71-
else
72-
{
73-
// Buffers are pooled, so it's OK to root them until the next multi-buffer write.
74-
_bufferList.Clear();
75-
}
7677

7778
foreach (var b in buffer)
7879
{
7980
_bufferList.Add(b.GetArray());
8081
}
8182

82-
return _bufferList;
83+
// The act of setting this list, sets the buffers in the internal buffer list
84+
BufferList = _bufferList;
8385
}
8486
}
8587
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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;
5+
using System.Collections.Concurrent;
6+
using System.IO.Pipelines;
7+
using System.Threading;
8+
9+
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
10+
{
11+
internal class SocketSenderPool : IDisposable
12+
{
13+
private const int MaxQueueSize = 1024; // REVIEW: Is this good enough?
14+
15+
private readonly ConcurrentQueue<SocketSender> _queue = new();
16+
private int _count;
17+
private readonly PipeScheduler _scheduler;
18+
private bool _disposed;
19+
20+
public SocketSenderPool(PipeScheduler scheduler)
21+
{
22+
_scheduler = scheduler;
23+
}
24+
25+
public SocketSender Rent()
26+
{
27+
if (_queue.TryDequeue(out var sender))
28+
{
29+
Interlocked.Decrement(ref _count);
30+
return sender;
31+
}
32+
return new SocketSender(_scheduler);
33+
}
34+
35+
public void Return(SocketSender sender)
36+
{
37+
// This counting isn't accurate, but it's good enough for what we need to avoid using _queue.Count which could be expensive
38+
if (_disposed || Interlocked.Increment(ref _count) > MaxQueueSize)
39+
{
40+
Interlocked.Decrement(ref _count);
41+
sender.Dispose();
42+
return;
43+
}
44+
45+
sender.Reset();
46+
_queue.Enqueue(sender);
47+
}
48+
49+
public void Dispose()
50+
{
51+
if (!_disposed)
52+
{
53+
_disposed = true;
54+
while (_queue.TryDequeue(out var sender))
55+
{
56+
sender.Dispose();
57+
}
58+
}
59+
}
60+
}
61+
}

src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketSenderReceiverBase.cs

-23
This file was deleted.

0 commit comments

Comments
 (0)