Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit da8c62d

Browse files
committed
Pool SocketAsyncEventArgs used to establish connections
SocketAsyncEventArgs is a heavy object to use once and throw away. This change lets us pool and reuse them.
1 parent fdfc497 commit da8c62d

File tree

3 files changed

+83
-59
lines changed

3 files changed

+83
-59
lines changed

src/Common/src/System/Collections/Concurrent/ConcurrentQueue_Segment.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System.Collections.Generic;
65
using System.Diagnostics;
76
using System.Runtime.InteropServices;
87
using System.Threading;
98

109
namespace System.Collections.Concurrent
1110
{
12-
public partial class ConcurrentQueue<T>
11+
partial class ConcurrentQueue<T>
1312
{
1413
/// <summary>
1514
/// Provides a multi-producer, multi-consumer thread-safe bounded segment. When the queue is full,
@@ -34,8 +33,10 @@ internal sealed class Segment
3433
internal bool _preservedForObservation;
3534
/// <summary>Indicates whether the segment has been marked such that no additional items may be enqueued.</summary>
3635
internal bool _frozenForEnqueues;
36+
#pragma warning disable 0649 // some builds don't assign to this field
3737
/// <summary>The segment following this one in the queue, or null if this segment is the last in the queue.</summary>
3838
internal Segment _nextSegment;
39+
#pragma warning restore 0649
3940

4041
/// <summary>Creates the segment.</summary>
4142
/// <param name="boundedLength">

src/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@
152152
<Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpHandler.cs" />
153153
<Compile Include="System\Net\Http\SocketsHttpHandler\RawConnectionStream.cs" />
154154
<Compile Include="System\Net\Http\SocketsHttpHandler\RedirectHandler.cs" />
155+
<Compile Include="$(CommonPath)\System\Collections\Concurrent\ConcurrentQueue_Segment.cs">
156+
<Link>Common\System\Collections\Concurrent\ConcurrentQueue_Segment.cs</Link>
157+
</Compile>
155158
<Compile Include="$(CommonPath)\System\Net\NTAuthentication.Common.cs">
156159
<Link>Common\System\Net\NTAuthentication.Common.cs</Link>
157160
</Compile>

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs

Lines changed: 77 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections.Concurrent;
56
using System.Diagnostics;
67
using System.IO;
78
using System.Net.Security;
@@ -15,6 +16,11 @@ namespace System.Net.Http
1516
{
1617
internal static class ConnectHelper
1718
{
19+
/// <summary>Pool of event args to use to establish connections.</summary>
20+
private static readonly ConcurrentQueue<ConnectEventArgs>.Segment s_connectEventArgs =
21+
new ConcurrentQueue<ConnectEventArgs>.Segment(
22+
ConcurrentQueue<ConnectEventArgs>.Segment.RoundUpToPowerOf2(Environment.ProcessorCount));
23+
1824
/// <summary>
1925
/// Helper type used by HttpClientHandler when wrapping SocketsHttpHandler to map its
2026
/// certificate validation callback to the one used by SslStream.
@@ -34,86 +40,100 @@ public CertificateCallbackMapper(Func<HttpRequestMessage, X509Certificate2, X509
3440

3541
public static async ValueTask<(Socket, Stream)> ConnectAsync(string host, int port, CancellationToken cancellationToken)
3642
{
37-
try
43+
// Rather than creating a new Socket and calling ConnectAsync on it, we use the static
44+
// Socket.ConnectAsync with a SocketAsyncEventArgs, as we can then use Socket.CancelConnectAsync
45+
// to cancel it if needed. Rent or allocate one.
46+
ConnectEventArgs saea;
47+
if (!s_connectEventArgs.TryDequeue(out saea))
3848
{
39-
// Rather than creating a new Socket and calling ConnectAsync on it, we use the static
40-
// Socket.ConnectAsync with a SocketAsyncEventArgs, as we can then use Socket.CancelConnectAsync
41-
// to cancel it if needed.
42-
using (var saea = new BuilderAndCancellationTokenSocketAsyncEventArgs(cancellationToken))
43-
{
44-
// Configure which server to which to connect.
45-
saea.RemoteEndPoint = IPAddress.TryParse(host, out IPAddress address) ?
46-
(EndPoint)new IPEndPoint(address, port) :
47-
new DnsEndPoint(host, port);
49+
saea = new ConnectEventArgs();
50+
}
51+
saea.Initialize(cancellationToken);
4852

49-
// Hook up a callback that'll complete the Task when the operation completes.
50-
saea.Completed += (s, e) =>
51-
{
52-
var csaea = (BuilderAndCancellationTokenSocketAsyncEventArgs)e;
53-
switch (e.SocketError)
54-
{
55-
case SocketError.Success:
56-
csaea.Builder.SetResult();
57-
break;
58-
case SocketError.OperationAborted:
59-
case SocketError.ConnectionAborted:
60-
if (csaea.CancellationToken.IsCancellationRequested)
61-
{
62-
csaea.Builder.SetException(CancellationHelper.CreateOperationCanceledException(null, csaea.CancellationToken));
63-
break;
64-
}
65-
goto default;
66-
default:
67-
csaea.Builder.SetException(new SocketException((int)e.SocketError));
68-
break;
69-
}
70-
};
53+
try
54+
{
55+
// Configure which server to which to connect.
56+
saea.RemoteEndPoint = IPAddress.TryParse(host, out IPAddress address) ?
57+
(EndPoint)new IPEndPoint(address, port) :
58+
new DnsEndPoint(host, port);
7159

72-
// Initiate the connection.
73-
if (Socket.ConnectAsync(SocketType.Stream, ProtocolType.Tcp, saea))
74-
{
75-
// Connect completing asynchronously. Enable it to be canceled and wait for it.
76-
using (cancellationToken.Register(s => Socket.CancelConnectAsync((SocketAsyncEventArgs)s), saea))
77-
{
78-
await saea.Builder.Task.ConfigureAwait(false);
79-
}
80-
}
81-
else if (saea.SocketError != SocketError.Success)
60+
// Initiate the connection.
61+
if (Socket.ConnectAsync(SocketType.Stream, ProtocolType.Tcp, saea))
62+
{
63+
// Connect completing asynchronously. Enable it to be canceled and wait for it.
64+
using (cancellationToken.Register(s => Socket.CancelConnectAsync((SocketAsyncEventArgs)s), saea))
8265
{
83-
// Connect completed synchronously but unsuccessfully.
84-
throw new SocketException((int)saea.SocketError);
66+
await saea.Builder.Task.ConfigureAwait(false);
8567
}
86-
87-
Debug.Assert(saea.SocketError == SocketError.Success, $"Expected Success, got {saea.SocketError}.");
88-
Debug.Assert(saea.ConnectSocket != null, "Expected non-null socket");
89-
90-
// Configure the socket and return a stream for it.
91-
Socket socket = saea.ConnectSocket;
92-
socket.NoDelay = true;
93-
return (socket, new NetworkStream(socket, ownsSocket: true));
9468
}
69+
else if (saea.SocketError != SocketError.Success)
70+
{
71+
// Connect completed synchronously but unsuccessfully.
72+
throw new SocketException((int)saea.SocketError);
73+
}
74+
75+
Debug.Assert(saea.SocketError == SocketError.Success, $"Expected Success, got {saea.SocketError}.");
76+
Debug.Assert(saea.ConnectSocket != null, "Expected non-null socket");
77+
78+
// Configure the socket and return a stream for it.
79+
Socket socket = saea.ConnectSocket;
80+
socket.NoDelay = true;
81+
return (socket, new NetworkStream(socket, ownsSocket: true));
9582
}
9683
catch (Exception error)
9784
{
9885
throw CancellationHelper.ShouldWrapInOperationCanceledException(error, cancellationToken) ?
9986
CancellationHelper.CreateOperationCanceledException(error, cancellationToken) :
10087
new HttpRequestException(error.Message, error);
10188
}
89+
finally
90+
{
91+
// Pool the event args, or if the pool is full, dispose of it.
92+
saea.Clear();
93+
if (!s_connectEventArgs.TryEnqueue(saea))
94+
{
95+
saea.Dispose();
96+
}
97+
}
10298
}
10399

104100
/// <summary>SocketAsyncEventArgs that carries with it additional state for a Task builder and a CancellationToken.</summary>
105-
private sealed class BuilderAndCancellationTokenSocketAsyncEventArgs : SocketAsyncEventArgs
101+
private sealed class ConnectEventArgs : SocketAsyncEventArgs
106102
{
107-
public AsyncTaskMethodBuilder Builder { get; }
108-
public CancellationToken CancellationToken { get; }
103+
public AsyncTaskMethodBuilder Builder { get; private set; }
104+
public CancellationToken CancellationToken { get; private set; }
109105

110-
public BuilderAndCancellationTokenSocketAsyncEventArgs(CancellationToken cancellationToken)
106+
public void Initialize(CancellationToken cancellationToken)
111107
{
108+
CancellationToken = cancellationToken;
112109
var b = new AsyncTaskMethodBuilder();
113110
var ignored = b.Task; // force initialization
114111
Builder = b;
112+
}
115113

116-
CancellationToken = cancellationToken;
114+
public void Clear() => CancellationToken = default;
115+
116+
protected override void OnCompleted(SocketAsyncEventArgs _)
117+
{
118+
switch (SocketError)
119+
{
120+
case SocketError.Success:
121+
Builder.SetResult();
122+
break;
123+
124+
case SocketError.OperationAborted:
125+
case SocketError.ConnectionAborted:
126+
if (CancellationToken.IsCancellationRequested)
127+
{
128+
Builder.SetException(CancellationHelper.CreateOperationCanceledException(null, CancellationToken));
129+
break;
130+
}
131+
goto default;
132+
133+
default:
134+
Builder.SetException(new SocketException((int)SocketError));
135+
break;
136+
}
117137
}
118138
}
119139

0 commit comments

Comments
 (0)