Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plaintext & json implementations that use KestrelServer directly #2043

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/BenchmarksApps.sln
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpSys", "BenchmarksApps\T
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kestrel", "BenchmarksApps\TLS\Kestrel\Kestrel.csproj", "{291DCDF7-4B7C-D687-A62B-9DF7DF50F2F2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kestrel", "BenchmarksApps\TechEmpower\Kestrel\Kestrel.csproj", "{41B067BC-22C8-FD0E-0D3C-1956F446171E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug_Database|Any CPU = Debug_Database|Any CPU
Expand Down Expand Up @@ -280,6 +282,14 @@ Global
{291DCDF7-4B7C-D687-A62B-9DF7DF50F2F2}.Release_Database|Any CPU.Build.0 = Release_Database|Any CPU
{291DCDF7-4B7C-D687-A62B-9DF7DF50F2F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{291DCDF7-4B7C-D687-A62B-9DF7DF50F2F2}.Release|Any CPU.Build.0 = Release|Any CPU
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Debug_Database|Any CPU.ActiveCfg = Debug_Database|Any CPU
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Debug_Database|Any CPU.Build.0 = Debug_Database|Any CPU
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Release_Database|Any CPU.ActiveCfg = Release_Database|Any CPU
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Release_Database|Any CPU.Build.0 = Release_Database|Any CPU
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41B067BC-22C8-FD0E-0D3C-1956F446171E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -297,6 +307,7 @@ Global
{D6616E03-A2DA-4929-AD28-595ECC4C004D} = {B6DB234C-8F80-4160-B95D-D70AFC444A3D}
{455942DF-6C8E-4054-AF1D-41A10BE1466F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{291DCDF7-4B7C-D687-A62B-9DF7DF50F2F2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{41B067BC-22C8-FD0E-0D3C-1956F446171E} = {B6DB234C-8F80-4160-B95D-D70AFC444A3D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {117072DC-DE12-4F74-90CA-692FA2BE8DCB}
Expand Down
198 changes: 198 additions & 0 deletions src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.Features;

namespace Kestrel;

public sealed partial class BenchmarkApp : IHttpApplication<IFeatureCollection>
{
private const string TextPlainContentType = "text/plain";
private const string JsonContentTypeWithCharset = "application/json; charset=utf-8";

public IFeatureCollection CreateContext(IFeatureCollection features) => features;

public Task ProcessRequestAsync(IFeatureCollection features)
{
var req = features.GetRequestFeature();
var res = features.GetResponseFeature();

//if (req.Method != "GET")
//{
// res.StatusCode = StatusCodes.Status405MethodNotAllowed;
// return Task.CompletedTask;
//}

var pathSpan = req.Path.AsSpan();
if (Paths.IsPath(pathSpan, Paths.Plaintext))
{
return Plaintext(res, features);
}
else if (Paths.IsPath(pathSpan, Paths.Json))
{
return Json(res, features);
}
else if (Paths.IsPath(pathSpan, Paths.JsonString))
{
return JsonString(res, features);
}
else if (Paths.IsPath(pathSpan, Paths.JsonUtf8Bytes))
{
return JsonUtf8Bytes(res, features);
}
else if (Paths.IsPath(pathSpan, Paths.JsonChunked))
{
return JsonChunked(res, features);
}
else if (pathSpan.IsEmpty || Paths.IsPath(pathSpan, Paths.Index))
{
return Index(res, features);
}

return NotFound(res, features);
}

private static Task NotFound(IHttpResponseFeature res, IFeatureCollection features)
{
res.StatusCode = StatusCodes.Status404NotFound;
return Task.CompletedTask;
}

public void DisposeContext(IFeatureCollection features, Exception? exception) { }

private static ReadOnlySpan<byte> IndexPayload => "Running directly on Kestrel! Navigate to /plaintext and /json to see other endpoints."u8;

private static async Task Index(IHttpResponseFeature res, IFeatureCollection features)
{
res.StatusCode = StatusCodes.Status200OK;
res.Headers.ContentType = TextPlainContentType;
res.Headers.ContentLength = IndexPayload.Length;

var body = features.GetResponseBodyFeature();

await body.StartAsync();
body.Writer.Write(IndexPayload);
await body.Writer.FlushAsync();
}

private static ReadOnlySpan<byte> HelloWorldPayload => "Hello, World!"u8;

private static Task Plaintext(IHttpResponseFeature res, IFeatureCollection features)
{
res.StatusCode = StatusCodes.Status200OK;
res.Headers.ContentType = TextPlainContentType;
res.Headers.ContentLength = HelloWorldPayload.Length;

var body = features.GetResponseBodyFeature();

body.Writer.Write(HelloWorldPayload);

return Task.CompletedTask;
}

private static Task JsonChunked(IHttpResponseFeature res, IFeatureCollection features)
{
res.StatusCode = StatusCodes.Status200OK;
res.Headers.ContentType = JsonContentTypeWithCharset;

var body = features.GetResponseBodyFeature();
return JsonSerializer.SerializeAsync(body.Writer, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage);
}

private static Task JsonString(IHttpResponseFeature res, IFeatureCollection features)
{
res.StatusCode = StatusCodes.Status200OK;
res.Headers.ContentType = JsonContentTypeWithCharset;

var message = JsonSerializer.Serialize(new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage);
Span<byte> buffer = stackalloc byte[64];
var length = Encoding.UTF8.GetBytes(message, buffer);
res.Headers.ContentLength = length;

var body = features.GetResponseBodyFeature();

body.Writer.Write(buffer[..length]);

return Task.CompletedTask;
}

private static Task JsonUtf8Bytes(IHttpResponseFeature res, IFeatureCollection features)
{
res.StatusCode = StatusCodes.Status200OK;
res.Headers.ContentType = JsonContentTypeWithCharset;

var messageBytes = JsonSerializer.SerializeToUtf8Bytes(new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage);
res.Headers.ContentLength = messageBytes.Length;

var body = features.GetResponseBodyFeature();

body.Writer.Write(messageBytes);

return Task.CompletedTask;
}

private static Task Json(IHttpResponseFeature res, IFeatureCollection features)
{
res.StatusCode = StatusCodes.Status200OK;
res.Headers.ContentType = JsonContentTypeWithCharset;

var messageSpan = JsonSerializeToUtf8Span(new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage);
res.Headers.ContentLength = messageSpan.Length;

var body = features.GetResponseBodyFeature();

body.Writer.Write(messageSpan);

return Task.CompletedTask;
}

[ThreadStatic]
private static ArrayBufferWriter<byte>? _bufferWriter;
[ThreadStatic]
private static Utf8JsonWriter? _jsonWriter;

private static ReadOnlySpan<byte> JsonSerializeToUtf8Span<T>(T value, JsonTypeInfo<T> jsonTypeInfo)
{
var bufferWriter = _bufferWriter ??= new(64);
var jsonWriter = _jsonWriter ??= new(_bufferWriter, new() { Indented = false, SkipValidation = true });

bufferWriter.ResetWrittenCount();
jsonWriter.Reset(bufferWriter);

JsonSerializer.Serialize(jsonWriter, value, jsonTypeInfo);

return bufferWriter.WrittenSpan;
}

private struct JsonMessage
{
public required string message { get; set; }
}

private static readonly JsonContext SerializerContext = JsonContext.Default;

// BUG: Can't use GenerationMode = JsonSourceGenerationMode.Serialization here due to https://github.com/dotnet/runtime/issues/111477
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Default)]
[JsonSerializable(typeof(JsonMessage))]
private partial class JsonContext : JsonSerializerContext
{

}

private static class Paths
{
public static ReadOnlySpan<char> Plaintext => "/plaintext";
public static ReadOnlySpan<char> Json => "/json";
public static ReadOnlySpan<char> JsonString => "/json-string";
public static ReadOnlySpan<char> JsonChunked => "/json-chunked";
public static ReadOnlySpan<char> JsonUtf8Bytes => "/json-utf8bytes";
public static ReadOnlySpan<char> Index => "/";

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPath(ReadOnlySpan<char> path, ReadOnlySpan<char> targetPath) => path.SequenceEqual(targetPath);
}
}
48 changes: 48 additions & 0 deletions src/BenchmarksApps/TechEmpower/Kestrel/ConsoleLifetime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Kestrel;

public class ConsoleLifetime : IDisposable
{
private readonly TaskCompletionSource _tcs = new();
private PosixSignalRegistration? _sigIntRegistration;
private PosixSignalRegistration? _sigQuitRegistration;
private PosixSignalRegistration? _sigTermRegistration;

public ConsoleLifetime()
{
if (!OperatingSystem.IsWasi())
{
Action<PosixSignalContext> handler = HandlePosixSignal;
_sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, handler);
_sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, handler);
_sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, handler);

Console.WriteLine("Application started. Press Ctrl+C to shut down.");
}
}

public Task LifetimeTask => _tcs.Task;

public void Dispose()
{
UnregisterShutdownHandlers();
}

private void HandlePosixSignal(PosixSignalContext context)
{
Debug.Assert(context.Signal == PosixSignal.SIGINT || context.Signal == PosixSignal.SIGQUIT || context.Signal == PosixSignal.SIGTERM);

context.Cancel = true;

_tcs.TrySetResult();
}

private void UnregisterShutdownHandlers()
{
_sigIntRegistration?.Dispose();
_sigQuitRegistration?.Dispose();
_sigTermRegistration?.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Microsoft.AspNetCore.Http.Features;

public static class FeatureCollectionExtensions
{
public static IHttpRequestFeature GetRequestFeature(this IFeatureCollection features)
{
return features.GetRequiredFeature<IHttpRequestFeature>();
}

public static IHttpResponseFeature GetResponseFeature(this IFeatureCollection features)
{
return features.GetRequiredFeature<IHttpResponseFeature>();
}

public static IHttpResponseBodyFeature GetResponseBodyFeature(this IFeatureCollection features)
{
return features.GetRequiredFeature<IHttpResponseBodyFeature>();
}
}
9 changes: 9 additions & 0 deletions src/BenchmarksApps/TechEmpower/Kestrel/Kestrel.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

</Project>
58 changes: 58 additions & 0 deletions src/BenchmarksApps/TechEmpower/Kestrel/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Runtime.InteropServices;
using Kestrel;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

var loggerFactory = new NullLoggerFactory();
var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables("ASPNETCORE_")
.AddCommandLine(args)
.Build();

var socketOptions = new SocketTransportOptions()
{
WaitForDataBeforeAllocatingBuffer = false,
UnsafePreferInlineScheduling = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
&& Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS") == "1"
};
if (int.TryParse(configuration["threadCount"], out var value))
{
socketOptions.IOQueueCount = value;
}
var kestrelOptions = new KestrelServerOptions();
kestrelOptions.Limits.MinRequestBodyDataRate = null;
kestrelOptions.Limits.MinResponseDataRate = null;

using var server = new KestrelServer(
Options.Create(kestrelOptions),
new SocketTransportFactory(Options.Create(socketOptions), loggerFactory),
loggerFactory
);

var addresses = server.Features.GetRequiredFeature<IServerAddressesFeature>().Addresses;
var urls = configuration["urls"];
if (!string.IsNullOrEmpty(urls))
{
foreach (var url in urls.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
addresses.Add(url);
}
}

await server.StartAsync(new BenchmarkApp(), CancellationToken.None);

foreach (var address in addresses)
{
Console.WriteLine($"Now listening on: {address}");
}

using var lifetime = new ConsoleLifetime();
await lifetime.LifetimeTask;

Console.Write("Server shutting down...");
await server.StopAsync(CancellationToken.None);
Console.Write(" done.");
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5123",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Loading
Loading