Skip to content

Commit 246eef0

Browse files
committed
Add direct Kestrel implementations for plaintext & json
1 parent 9b5402f commit 246eef0

File tree

8 files changed

+314
-0
lines changed

8 files changed

+314
-0
lines changed

src/BenchmarksApps.sln

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpSys", "BenchmarksApps\T
7272
EndProject
7373
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kestrel", "BenchmarksApps\TLS\Kestrel\Kestrel.csproj", "{291DCDF7-4B7C-D687-A62B-9DF7DF50F2F2}"
7474
EndProject
75+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kestrel", "BenchmarksApps\TechEmpower\Kestrel\Kestrel.csproj", "{41B067BC-22C8-FD0E-0D3C-1956F446171E}"
76+
EndProject
7577
Global
7678
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7779
Debug_Database|Any CPU = Debug_Database|Any CPU
@@ -280,6 +282,14 @@ Global
280282
{291DCDF7-4B7C-D687-A62B-9DF7DF50F2F2}.Release_Database|Any CPU.Build.0 = Release_Database|Any CPU
281283
{291DCDF7-4B7C-D687-A62B-9DF7DF50F2F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
282284
{291DCDF7-4B7C-D687-A62B-9DF7DF50F2F2}.Release|Any CPU.Build.0 = Release|Any CPU
285+
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Debug_Database|Any CPU.ActiveCfg = Debug_Database|Any CPU
286+
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Debug_Database|Any CPU.Build.0 = Debug_Database|Any CPU
287+
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
288+
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Debug|Any CPU.Build.0 = Debug|Any CPU
289+
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Release_Database|Any CPU.ActiveCfg = Release_Database|Any CPU
290+
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Release_Database|Any CPU.Build.0 = Release_Database|Any CPU
291+
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Release|Any CPU.ActiveCfg = Release|Any CPU
292+
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Release|Any CPU.Build.0 = Release|Any CPU
283293
EndGlobalSection
284294
GlobalSection(SolutionProperties) = preSolution
285295
HideSolutionNode = FALSE
@@ -297,6 +307,7 @@ Global
297307
{D6616E03-A2DA-4929-AD28-595ECC4C004D} = {B6DB234C-8F80-4160-B95D-D70AFC444A3D}
298308
{455942DF-6C8E-4054-AF1D-41A10BE1466F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
299309
{291DCDF7-4B7C-D687-A62B-9DF7DF50F2F2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
310+
{41B067BC-22C8-FD0E-0D3C-1956F446171E} = {B6DB234C-8F80-4160-B95D-D70AFC444A3D}
300311
EndGlobalSection
301312
GlobalSection(ExtensibilityGlobals) = postSolution
302313
SolutionGuid = {117072DC-DE12-4F74-90CA-692FA2BE8DCB}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using System.Buffers;
2+
using System.Text.Json;
3+
using Microsoft.AspNetCore.Hosting.Server;
4+
using Microsoft.AspNetCore.Http.Features;
5+
using Microsoft.Extensions.ObjectPool;
6+
7+
public class BenchmarkApp : IHttpApplication<IFeatureCollection>
8+
{
9+
public IFeatureCollection CreateContext(IFeatureCollection features) => features;
10+
11+
public Task ProcessRequestAsync(IFeatureCollection features)
12+
{
13+
var req = features.GetRequestFeature();
14+
var res = features.GetResponseFeature();
15+
16+
if (req.Method != "GET")
17+
{
18+
res.StatusCode = 405;
19+
var body = features.GetResponseBodyFeature();
20+
return body.StartAsync().ContinueWith(t => body.CompleteAsync());
21+
}
22+
23+
return req.Path switch
24+
{
25+
"/plaintext" => Plaintext(req, res, features),
26+
"/json" => Json(req, res, features),
27+
"/" => Index(req, res, features),
28+
_ => NotFound(req, res, features),
29+
};
30+
}
31+
32+
private static async Task NotFound(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features)
33+
{
34+
res.StatusCode = 404;
35+
res.Headers.ContentType = "text/plain";
36+
res.Headers.ContentLength = HelloWorldPayload.Length;
37+
38+
var body = features.GetResponseBodyFeature();
39+
40+
await body.StartAsync();
41+
body.Writer.Write(HelloWorldPayload);
42+
await body.CompleteAsync();
43+
}
44+
45+
public void DisposeContext(IFeatureCollection features, Exception? exception) { }
46+
47+
private static ReadOnlySpan<byte> IndexPayload => "Running directly on Kestrel! Navigate to /plaintext and /json to see other endpoints."u8;
48+
49+
private static async Task Index(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features)
50+
{
51+
res.StatusCode = 200;
52+
res.Headers.ContentType = "text/plain";
53+
res.Headers.ContentLength = IndexPayload.Length;
54+
55+
var body = features.GetResponseBodyFeature();
56+
57+
await body.StartAsync();
58+
body.Writer.Write(IndexPayload);
59+
await body.Writer.FlushAsync();
60+
await body.CompleteAsync();
61+
}
62+
63+
private static ReadOnlySpan<byte> HelloWorldPayload => "Hello, World!"u8;
64+
65+
private static async Task Plaintext(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features)
66+
{
67+
res.StatusCode = 200;
68+
res.Headers.ContentType = "text/plain";
69+
res.Headers.ContentLength = HelloWorldPayload.Length;
70+
71+
var body = features.GetResponseBodyFeature();
72+
73+
await body.StartAsync();
74+
body.Writer.Write(HelloWorldPayload);
75+
await body.Writer.FlushAsync();
76+
await body.CompleteAsync();
77+
}
78+
79+
private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web);
80+
private static readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider();
81+
private static readonly ObjectPool<ArrayBufferWriter<byte>> _bufferWriterPool = _objectPoolProvider.Create<ArrayBufferWriter<byte>>();
82+
private static readonly ObjectPool<Utf8JsonWriter> _jsonWriterPool = _objectPoolProvider.Create(new Utf8JsonWriterPooledObjectPolicy());
83+
84+
private static async Task Json(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features)
85+
{
86+
res.StatusCode = 200;
87+
res.Headers.ContentType = "application/json";
88+
89+
//Span<byte> buffer = stackalloc byte[256];
90+
var bufferWriter = _bufferWriterPool.Get();
91+
var jsonWriter = _jsonWriterPool.Get();
92+
93+
bufferWriter.ResetWrittenCount();
94+
jsonWriter.Reset(bufferWriter);
95+
96+
JsonSerializer.Serialize(jsonWriter, new { message = "Hello, World!" }, _jsonSerializerOptions);
97+
98+
res.Headers.ContentLength = bufferWriter.WrittenCount;
99+
100+
var body = features.GetResponseBodyFeature();
101+
102+
await body.StartAsync();
103+
bufferWriter.WrittenSpan.CopyTo(body.Writer.GetSpan(bufferWriter.WrittenCount));
104+
body.Writer.Advance(bufferWriter.WrittenCount);
105+
await body.Writer.FlushAsync();
106+
await body.CompleteAsync();
107+
108+
_jsonWriterPool.Return(jsonWriter);
109+
_bufferWriterPool.Return(bufferWriter);
110+
}
111+
112+
private class Utf8JsonWriterPooledObjectPolicy : IPooledObjectPolicy<Utf8JsonWriter>
113+
{
114+
private static readonly ArrayBufferWriter<byte> _dummyBufferWriter = new(256);
115+
116+
public Utf8JsonWriter Create() => new(_dummyBufferWriter, new() { Indented = false, SkipValidation = true });
117+
118+
public bool Return(Utf8JsonWriter obj)
119+
{
120+
//obj.Reset();
121+
return true;
122+
}
123+
}
124+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.Diagnostics;
2+
using System.Runtime.InteropServices;
3+
4+
public class ConsoleLifetime : IDisposable
5+
{
6+
private readonly TaskCompletionSource _tcs = new();
7+
private PosixSignalRegistration? _sigIntRegistration;
8+
private PosixSignalRegistration? _sigQuitRegistration;
9+
private PosixSignalRegistration? _sigTermRegistration;
10+
11+
public ConsoleLifetime()
12+
{
13+
if (!OperatingSystem.IsWasi())
14+
{
15+
Action<PosixSignalContext> handler = HandlePosixSignal;
16+
_sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, handler);
17+
_sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, handler);
18+
_sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, handler);
19+
20+
Console.WriteLine("Application started. Press Ctrl+C to shut down.");
21+
}
22+
}
23+
24+
public Task LifetimeTask => _tcs.Task;
25+
26+
public void Dispose()
27+
{
28+
UnregisterShutdownHandlers();
29+
}
30+
31+
private void HandlePosixSignal(PosixSignalContext context)
32+
{
33+
Debug.Assert(context.Signal == PosixSignal.SIGINT || context.Signal == PosixSignal.SIGQUIT || context.Signal == PosixSignal.SIGTERM);
34+
35+
context.Cancel = true;
36+
37+
_tcs.TrySetResult();
38+
}
39+
40+
private void UnregisterShutdownHandlers()
41+
{
42+
_sigIntRegistration?.Dispose();
43+
_sigQuitRegistration?.Dispose();
44+
_sigTermRegistration?.Dispose();
45+
}
46+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Microsoft.AspNetCore.Http.Features;
2+
3+
public static class FeatureCollectionExtensions
4+
{
5+
public static IHttpRequestFeature GetRequestFeature(this IFeatureCollection features)
6+
{
7+
return features.GetRequiredFeature<IHttpRequestFeature>();
8+
}
9+
10+
public static IHttpResponseFeature GetResponseFeature(this IFeatureCollection features)
11+
{
12+
return features.GetRequiredFeature<IHttpResponseFeature>();
13+
}
14+
15+
public static IHttpResponseBodyFeature GetResponseBodyFeature(this IFeatureCollection features)
16+
{
17+
return features.GetRequiredFeature<IHttpResponseBodyFeature>();
18+
}
19+
20+
public static TFeature GetRequiredFeature<TFeature>(this IFeatureCollection features)
21+
{
22+
return features.Get<TFeature>() ?? throw new InvalidOperationException($"Feature of type {typeof(TFeature).Name} not found");
23+
}
24+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
</Project>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Runtime.InteropServices;
2+
using Microsoft.AspNetCore.Hosting.Server.Features;
3+
using Microsoft.AspNetCore.Server.Kestrel.Core;
4+
using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
5+
using Microsoft.Extensions.Logging.Abstractions;
6+
using Microsoft.Extensions.Options;
7+
8+
var loggerFactory = new NullLoggerFactory();
9+
var socketOptions = new SocketTransportOptions()
10+
{
11+
WaitForDataBeforeAllocatingBuffer = false,
12+
UnsafePreferInlineScheduling = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
13+
&& Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS") == "1"
14+
};
15+
if (int.TryParse(Environment.GetEnvironmentVariable("threadCount"), out var value))
16+
{
17+
socketOptions.IOQueueCount = value;
18+
}
19+
using var server = new KestrelServer(
20+
Options.Create(new KestrelServerOptions()),
21+
new SocketTransportFactory(Options.Create(socketOptions), loggerFactory),
22+
loggerFactory
23+
);
24+
25+
await server.StartAsync(new BenchmarkApp(), CancellationToken.None);
26+
27+
var addresses = server.Features.GetRequiredFeature<IServerAddressesFeature>().Addresses;
28+
foreach (var address in addresses)
29+
{
30+
Console.WriteLine($"Now listening on: {address}");
31+
}
32+
33+
using var lifetime = new ConsoleLifetime();
34+
await lifetime.LifetimeTask;
35+
36+
Console.Write("Server shutting down...");
37+
await server.StopAsync(CancellationToken.None);
38+
Console.Write(" done.");
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"http": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": false,
8+
"environmentVariables": {
9+
"ASPNETCORE_ENVIRONMENT": "Development"
10+
}
11+
}
12+
}
13+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
imports:
2+
- https://raw.githubusercontent.com/dotnet/crank/main/src/Microsoft.Crank.Jobs.Wrk/wrk.yml
3+
- https://github.com/aspnet/Benchmarks/blob/main/scenarios/aspnet.profiles.standard.yml?raw=true
4+
5+
variables:
6+
serverPort: 5000
7+
8+
jobs:
9+
kestrel:
10+
source:
11+
repository: https://github.com/aspnet/benchmarks.git
12+
branchOrCommit: main
13+
project: src/BenchmarksApps/TechEmpower/Kestrel/Kestrel.csproj
14+
readyStateText: Application started.
15+
arguments: "--urls {{serverScheme}}://{{serverAddress}}:{{serverPort}}"
16+
variables:
17+
serverScheme: http
18+
environmentVariables:
19+
20+
scenarios:
21+
plaintext:
22+
application:
23+
job: kestrel
24+
load:
25+
job: wrk
26+
variables:
27+
presetHeaders: plaintext
28+
path: /plaintext
29+
pipeline: 16
30+
31+
json:
32+
application:
33+
job: kestrel
34+
load:
35+
job: wrk
36+
variables:
37+
presetHeaders: json
38+
path: /json
39+
40+
profiles:
41+
# this profile uses the local folder as the source
42+
# instead of the public repository
43+
source:
44+
agents:
45+
main:
46+
source:
47+
localFolder: .
48+
respository: ''
49+
project: Kestrel.csproj

0 commit comments

Comments
 (0)