Skip to content

Commit 2639212

Browse files
committed
Fix some bugs.
Beginning of benchmark. Move HTTP tests to their own namespace, matching folder structure.
1 parent 8a3a687 commit 2639212

29 files changed

+741
-105
lines changed

NetworkToolkit.Benchmarks/Assembly.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[module: System.Runtime.CompilerServices.SkipLocalsInit]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net5.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\NetworkToolkit\NetworkToolkit.csproj" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<Compile Include="..\NetworkToolKit\ArrayBuffer.cs" />
20+
</ItemGroup>
21+
22+
</Project>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using BenchmarkDotNet.Configs;
2+
using BenchmarkDotNet.Diagnosers;
3+
using BenchmarkDotNet.Jobs;
4+
5+
namespace NetworkToolkit.Benchmarks
6+
{
7+
public class NetworkToolkitConfig : ManualConfig
8+
{
9+
public NetworkToolkitConfig()
10+
{
11+
AddDiagnoser(MemoryDiagnoser.Default);
12+
AddJob(Job.Default
13+
.WithGcServer(true)
14+
.WithEnvironmentVariable("DOTNET_SYSTEM_THREADING_POOLASYNCVALUETASKS", "1"));
15+
}
16+
}
17+
}

NetworkToolkit.Benchmarks/Program.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using BenchmarkDotNet.Running;
2+
using System;
3+
4+
namespace NetworkToolkit.Benchmarks
5+
{
6+
class Program
7+
{
8+
static void Main(string[] args)
9+
{
10+
BenchmarkRunner.Run(typeof(Program).Assembly);
11+
}
12+
}
13+
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
using BenchmarkDotNet.Attributes;
2+
using NetworkToolkit.Connections;
3+
using NetworkToolkit.Http.Primitives;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Net.Http;
7+
using System.Text;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
namespace NetworkToolkit.Benchmarks
12+
{
13+
[Config(typeof(NetworkToolkitConfig))]
14+
public class SerializedGet
15+
{
16+
private readonly ConnectionFactory _connectionFactory = new MemoryConnectionFactory();
17+
18+
private ConnectionListener? _connectionListener;
19+
private SimpleHttp1Server? _server;
20+
21+
private HttpMessageInvoker? _httpClient;
22+
private Uri? _uri;
23+
24+
private HttpConnection? _primitiveConnection;
25+
private byte[]? _encodedScheme, _encodedAuthority, _encodedPathAndQuery;
26+
27+
[ParamsSource(nameof(ValuesForRequestHeaders))]
28+
public RequestHeaderCollection? RequestHeaders { get; set; }
29+
30+
public static IEnumerable<RequestHeaderCollection> ValuesForRequestHeaders => new[]
31+
{
32+
new RequestHeaderCollection("Minimal").Build(),
33+
new RequestHeaderCollection("Normal")
34+
{
35+
StaticHeaders = new List<(string, string)>
36+
{
37+
("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"),
38+
("accept-encoding", "gzip, deflate, br"),
39+
("accept-language", "en-US,en;q=0.9"),
40+
("sec-fetch-dest", "document"),
41+
("sec-fetch-mode", "navigate"),
42+
("sec-fetch-site", "none"),
43+
("sec-fetch-user", "?1"),
44+
("upgrade-insecure-requests", "1"),
45+
("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 Edg/86.0.622.69")
46+
},
47+
DynamicHeaders = new List<(string, string)>
48+
{
49+
("cookie", "cookie: aaaa=000000000000000000000000000000000000; bbb=111111111111111111111111111; ccccc=22222222222222222222222222; dddddd=333333333333333333333333333333333333333333333333333333333333333333333; eeee=444444444444444444444444444444444444444444444444444444444444444444444444444")
50+
}
51+
}.Build()
52+
};
53+
54+
[ParamsSource(nameof(ValuesForResponseBytes))]
55+
public EncodedMessage? ResponseBytes { get; set; }
56+
57+
public static IEnumerable<EncodedMessage> ValuesForResponseBytes => new[]
58+
{
59+
new EncodedMessage("Minimal", "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"),
60+
new EncodedMessage("StackOverflow",
61+
"HTTP/1.1 200 OK\r\n" +
62+
"Content-Length: 0\r\n" +
63+
"Accept-Ranges: bytes\r\n" +
64+
"Cache-Control: private\r\n" +
65+
"Content-Security-Policy: upgrade-insecure-requests; frame-ancestors 'self' https://stackexchange.com\r\n" +
66+
"Content-Type: text/html; charset=utf-8\r\n" +
67+
"Date: Mon, 16 Nov 2020 23:35:36 GMT\r\n" +
68+
"Feature-Policy: microphone 'none'; speaker 'none'\r\n" +
69+
"Server: Microsoft-IIS/10.0\r\n" +
70+
"Strict-Transport-Security: max-age=15552000\r\n" +
71+
"Vary: Accept-Encoding,Fastly-SSL\r\n" +
72+
"Via: 1.1 varnish\r\n" +
73+
"x-account-id: 12345\r\n" +
74+
"x-aspnet-duration-ms: 44\r\n" +
75+
"x-cache: MISS\r\n" +
76+
"x-cache-hits: 0\r\n" +
77+
"x-dns-prefetch-control: off\r\n" +
78+
"x-flags: QA\r\n" +
79+
"x-frame-options: SAMEORIGIN\r\n" +
80+
"x-http-count: 2\r\n" +
81+
"x-http-duration-ms: 8\r\n" +
82+
"x-is-crawler: 0\r\n" +
83+
"x-page-view: 1\r\n" +
84+
"x-providence-cookie: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\r\n" +
85+
"x-redis-count: 22\r\n" +
86+
"x-redis-duration-ms: 2\r\n" +
87+
"x-request-guid: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\r\n" +
88+
"x-route-name: Home/Index\r\n" +
89+
"x-served-by: cache-sea4460-SEA\r\n" +
90+
"x-sql-count: 12\r\n" +
91+
"x-sql-duration-ms: 12\r\n" +
92+
"x-timer: S1605569737.604081,VS0,VE106\r\n" +
93+
"\r\n")
94+
};
95+
96+
[GlobalSetup]
97+
public void GlobalSetup()
98+
{
99+
_connectionListener = _connectionFactory.ListenAsync().AsTask().Result;
100+
_server = new SimpleHttp1Server(_connectionListener, trigger: Encoding.ASCII.GetBytes("\r\n\r\n"), response: ResponseBytes!.Response);
101+
_primitiveConnection = new Http1Connection(_connectionFactory.ConnectAsync(_connectionListener.EndPoint!).AsTask().Result);
102+
_httpClient = new HttpMessageInvoker(new SocketsHttpHandler
103+
{
104+
AllowAutoRedirect = false,
105+
AutomaticDecompression = System.Net.DecompressionMethods.None,
106+
ConnectCallback = async (ct, token) => (await _connectionFactory.ConnectAsync(_connectionListener.EndPoint!).ConfigureAwait(false)).Stream,
107+
UseProxy = false
108+
});
109+
110+
_uri = new Uri("http://localhost/");
111+
_encodedScheme = Encoding.ASCII.GetBytes("http");
112+
_encodedAuthority = Encoding.ASCII.GetBytes(_uri.IdnHost);
113+
_encodedPathAndQuery = Encoding.ASCII.GetBytes("/");
114+
}
115+
116+
[GlobalCleanup]
117+
public void GlobalCleanup()
118+
{
119+
_httpClient!.Dispose();
120+
_primitiveConnection!.DisposeAsync().AsTask().GetAwaiter().GetResult();
121+
_server!.DisposeAsync().AsTask().GetAwaiter().GetResult();
122+
_connectionListener!.DisposeAsync().AsTask().GetAwaiter().GetResult();
123+
}
124+
125+
[Benchmark]
126+
public Task Primitive() =>
127+
Primitive(RequestHeaders!.EncodedAllHeaders, preparedHeaders: null);
128+
129+
[Benchmark]
130+
public Task PrimitivePrepared() =>
131+
Primitive(RequestHeaders!.EncodedDynamicHeaders, RequestHeaders!.PreparedHeaders);
132+
133+
private async Task Primitive(List<(byte[], byte[])>? encodedHeaders, PreparedHeaderSet? preparedHeaders)
134+
{
135+
ValueHttpRequest request = (await _primitiveConnection!.CreateNewRequestAsync(HttpPrimitiveVersion.Version11, HttpVersionPolicy.RequestVersionExact).ConfigureAwait(false)).Value;
136+
await using (request.ConfigureAwait(false))
137+
{
138+
request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false);
139+
request.WriteRequest(HttpRequest.GetMethod, _encodedScheme!, _encodedAuthority!, _encodedPathAndQuery!);
140+
141+
if (encodedHeaders != null)
142+
{
143+
foreach ((byte[] name, byte[] value) in encodedHeaders)
144+
{
145+
request.WriteHeader(name, value);
146+
}
147+
}
148+
149+
if (preparedHeaders != null)
150+
{
151+
request.WriteHeader(preparedHeaders);
152+
}
153+
154+
await request.CompleteRequestAsync().ConfigureAwait(false);
155+
156+
while (await request.ReadAsync().ConfigureAwait(false) != HttpReadType.EndOfStream)
157+
{
158+
// skip everything.
159+
}
160+
}
161+
}
162+
163+
[Benchmark(Baseline = true)]
164+
public async Task SocketsHandler()
165+
{
166+
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, _uri!);
167+
168+
if (RequestHeaders!.StaticHeaders is List<(string, string)> staticHeaders)
169+
{
170+
foreach ((string name, string value) in staticHeaders)
171+
{
172+
requestMessage.Headers.TryAddWithoutValidation(name, value);
173+
}
174+
}
175+
176+
if (RequestHeaders!.DynamicHeaders is List<(string, string)> dynamicHeaders)
177+
{
178+
foreach ((string name, string value) in dynamicHeaders)
179+
{
180+
requestMessage.Headers.TryAddWithoutValidation(name, value);
181+
}
182+
}
183+
184+
using var responseMessage = await _httpClient!.SendAsync(requestMessage, CancellationToken.None).ConfigureAwait(false);
185+
}
186+
187+
public sealed class RequestHeaderCollection
188+
{
189+
public string Name { get; }
190+
public List<(string,string)>? StaticHeaders { get; init; }
191+
public List<(string,string)>? DynamicHeaders { get; init; }
192+
193+
public PreparedHeaderSet? PreparedHeaders { get; private set; }
194+
public List<(byte[], byte[])>? EncodedDynamicHeaders { get; private set; }
195+
public List<(byte[], byte[])>? EncodedAllHeaders { get; private set; }
196+
197+
public RequestHeaderCollection(string name)
198+
{
199+
Name = name;
200+
}
201+
202+
public override string ToString() => Name;
203+
204+
public RequestHeaderCollection Build()
205+
{
206+
if (StaticHeaders is List<(string, string)> staticHeaders)
207+
{
208+
EncodedAllHeaders ??= new List<(byte[], byte[])>();
209+
210+
var builder = new PreparedHeaderSetBuilder();
211+
foreach ((string name, string value) in staticHeaders)
212+
{
213+
EncodedAllHeaders.Add((Encoding.ASCII.GetBytes(name), Encoding.ASCII.GetBytes(value)));
214+
builder.AddHeader(name, value);
215+
}
216+
217+
PreparedHeaders = builder.Build();
218+
}
219+
220+
if (DynamicHeaders is List<(string, string)> dynamicHeaders)
221+
{
222+
EncodedAllHeaders ??= new List<(byte[], byte[])>();
223+
EncodedDynamicHeaders = new List<(byte[], byte[])>();
224+
225+
foreach ((string name, string value) in dynamicHeaders)
226+
{
227+
EncodedAllHeaders.Add((Encoding.ASCII.GetBytes(name), Encoding.ASCII.GetBytes(value)));
228+
EncodedDynamicHeaders.Add((Encoding.ASCII.GetBytes(name), Encoding.ASCII.GetBytes(value)));
229+
}
230+
}
231+
232+
return this;
233+
}
234+
}
235+
236+
public sealed record EncodedMessage(string Name, byte[] Response)
237+
{
238+
public EncodedMessage(string name, string response)
239+
: this(name, Encoding.ASCII.GetBytes(response))
240+
{
241+
}
242+
243+
public override string ToString() => Name;
244+
}
245+
}
246+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using NetworkToolkit.Connections;
2+
using System;
3+
using System.IO;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace NetworkToolkit.Benchmarks
8+
{
9+
internal sealed class SimpleHttp1Server : IAsyncDisposable
10+
{
11+
private readonly CancellationTokenSource _cts = new();
12+
private readonly ConnectionListener _listener;
13+
private readonly byte[] _trigger;
14+
private readonly byte[] _response;
15+
16+
public SimpleHttp1Server(ConnectionListener listener, byte[] trigger, byte[] response)
17+
{
18+
_listener = listener;
19+
_trigger = trigger;
20+
_response = response;
21+
_ = ListenAsync();
22+
}
23+
24+
public async ValueTask DisposeAsync()
25+
{
26+
_cts.Cancel();
27+
await _listener.DisposeAsync().ConfigureAwait(false);
28+
}
29+
30+
private async Task ListenAsync()
31+
{
32+
Connection? con;
33+
while ((con = await _listener.AcceptConnectionAsync(cancellationToken: _cts.Token)) != null)
34+
{
35+
_ = RunConnectionAsync(con);
36+
}
37+
}
38+
39+
private async Task RunConnectionAsync(Connection connection)
40+
{
41+
try
42+
{
43+
await using (connection.ConfigureAwait(false))
44+
using (var readBuffer = new ArrayBuffer(4096, usePool: true))
45+
{
46+
Stream stream = connection.Stream;
47+
48+
while (true)
49+
{
50+
int triggerIdx;
51+
while ((triggerIdx = readBuffer.ActiveSpan.IndexOf(_trigger)) == -1)
52+
{
53+
readBuffer.EnsureAvailableSpace(1);
54+
55+
int readLen = await stream.ReadAsync(readBuffer.AvailableMemory).ConfigureAwait(false);
56+
if (readLen == 0) return;
57+
58+
readBuffer.Commit(readLen);
59+
}
60+
61+
readBuffer.Discard(triggerIdx + _trigger.Length);
62+
63+
await stream.WriteAsync(_response).ConfigureAwait(false);
64+
await stream.FlushAsync().ConfigureAwait(false);
65+
}
66+
}
67+
}
68+
catch (Exception ex)
69+
{
70+
Console.WriteLine(ex);
71+
}
72+
}
73+
}
74+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net5.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\NetworkToolkit\NetworkToolkit.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<Compile Include="..\NetworkToolKit\ArrayBuffer.cs" />
16+
<Compile Include="..\NetworkToolKit.Benchmarks\SimpleHttp1Server.cs" />
17+
</ItemGroup>
18+
19+
</Project>

0 commit comments

Comments
 (0)