From 246eef06723c54f7d1db957111592078c3f54675 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 13 Jan 2025 13:42:16 -0800 Subject: [PATCH 01/18] Add direct Kestrel implementations for plaintext & json --- src/BenchmarksApps.sln | 11 ++ .../TechEmpower/Kestrel/BenchmarkApp.cs | 124 ++++++++++++++++++ .../TechEmpower/Kestrel/ConsoleLifetime.cs | 46 +++++++ .../Kestrel/FeatureCollectionExtensions.cs | 24 ++++ .../TechEmpower/Kestrel/Kestrel.csproj | 9 ++ .../TechEmpower/Kestrel/Program.cs | 38 ++++++ .../Kestrel/Properties/launchSettings.json | 13 ++ .../Kestrel/kestrel.benchmarks.yml | 49 +++++++ 8 files changed, 314 insertions(+) create mode 100644 src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs create mode 100644 src/BenchmarksApps/TechEmpower/Kestrel/ConsoleLifetime.cs create mode 100644 src/BenchmarksApps/TechEmpower/Kestrel/FeatureCollectionExtensions.cs create mode 100644 src/BenchmarksApps/TechEmpower/Kestrel/Kestrel.csproj create mode 100644 src/BenchmarksApps/TechEmpower/Kestrel/Program.cs create mode 100644 src/BenchmarksApps/TechEmpower/Kestrel/Properties/launchSettings.json create mode 100644 src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml diff --git a/src/BenchmarksApps.sln b/src/BenchmarksApps.sln index c10baaabc..6a3d6f12a 100644 --- a/src/BenchmarksApps.sln +++ b/src/BenchmarksApps.sln @@ -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 @@ -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 @@ -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} diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs new file mode 100644 index 000000000..0a0655255 --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -0,0 +1,124 @@ +using System.Buffers; +using System.Text.Json; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.ObjectPool; + +public class BenchmarkApp : IHttpApplication +{ + 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 = 405; + var body = features.GetResponseBodyFeature(); + return body.StartAsync().ContinueWith(t => body.CompleteAsync()); + } + + return req.Path switch + { + "/plaintext" => Plaintext(req, res, features), + "/json" => Json(req, res, features), + "/" => Index(req, res, features), + _ => NotFound(req, res, features), + }; + } + + private static async Task NotFound(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features) + { + res.StatusCode = 404; + res.Headers.ContentType = "text/plain"; + res.Headers.ContentLength = HelloWorldPayload.Length; + + var body = features.GetResponseBodyFeature(); + + await body.StartAsync(); + body.Writer.Write(HelloWorldPayload); + await body.CompleteAsync(); + } + + public void DisposeContext(IFeatureCollection features, Exception? exception) { } + + private static ReadOnlySpan IndexPayload => "Running directly on Kestrel! Navigate to /plaintext and /json to see other endpoints."u8; + + private static async Task Index(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features) + { + res.StatusCode = 200; + res.Headers.ContentType = "text/plain"; + res.Headers.ContentLength = IndexPayload.Length; + + var body = features.GetResponseBodyFeature(); + + await body.StartAsync(); + body.Writer.Write(IndexPayload); + await body.Writer.FlushAsync(); + await body.CompleteAsync(); + } + + private static ReadOnlySpan HelloWorldPayload => "Hello, World!"u8; + + private static async Task Plaintext(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features) + { + res.StatusCode = 200; + res.Headers.ContentType = "text/plain"; + res.Headers.ContentLength = HelloWorldPayload.Length; + + var body = features.GetResponseBodyFeature(); + + await body.StartAsync(); + body.Writer.Write(HelloWorldPayload); + await body.Writer.FlushAsync(); + await body.CompleteAsync(); + } + + private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); + private static readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider(); + private static readonly ObjectPool> _bufferWriterPool = _objectPoolProvider.Create>(); + private static readonly ObjectPool _jsonWriterPool = _objectPoolProvider.Create(new Utf8JsonWriterPooledObjectPolicy()); + + private static async Task Json(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features) + { + res.StatusCode = 200; + res.Headers.ContentType = "application/json"; + + //Span buffer = stackalloc byte[256]; + var bufferWriter = _bufferWriterPool.Get(); + var jsonWriter = _jsonWriterPool.Get(); + + bufferWriter.ResetWrittenCount(); + jsonWriter.Reset(bufferWriter); + + JsonSerializer.Serialize(jsonWriter, new { message = "Hello, World!" }, _jsonSerializerOptions); + + res.Headers.ContentLength = bufferWriter.WrittenCount; + + var body = features.GetResponseBodyFeature(); + + await body.StartAsync(); + bufferWriter.WrittenSpan.CopyTo(body.Writer.GetSpan(bufferWriter.WrittenCount)); + body.Writer.Advance(bufferWriter.WrittenCount); + await body.Writer.FlushAsync(); + await body.CompleteAsync(); + + _jsonWriterPool.Return(jsonWriter); + _bufferWriterPool.Return(bufferWriter); + } + + private class Utf8JsonWriterPooledObjectPolicy : IPooledObjectPolicy + { + private static readonly ArrayBufferWriter _dummyBufferWriter = new(256); + + public Utf8JsonWriter Create() => new(_dummyBufferWriter, new() { Indented = false, SkipValidation = true }); + + public bool Return(Utf8JsonWriter obj) + { + //obj.Reset(); + return true; + } + } +} diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/ConsoleLifetime.cs b/src/BenchmarksApps/TechEmpower/Kestrel/ConsoleLifetime.cs new file mode 100644 index 000000000..adc811ac4 --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/Kestrel/ConsoleLifetime.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +public class ConsoleLifetime : IDisposable +{ + private readonly TaskCompletionSource _tcs = new(); + private PosixSignalRegistration? _sigIntRegistration; + private PosixSignalRegistration? _sigQuitRegistration; + private PosixSignalRegistration? _sigTermRegistration; + + public ConsoleLifetime() + { + if (!OperatingSystem.IsWasi()) + { + Action 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(); + } +} diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/FeatureCollectionExtensions.cs b/src/BenchmarksApps/TechEmpower/Kestrel/FeatureCollectionExtensions.cs new file mode 100644 index 000000000..f70c35240 --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/Kestrel/FeatureCollectionExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http.Features; + +public static class FeatureCollectionExtensions +{ + public static IHttpRequestFeature GetRequestFeature(this IFeatureCollection features) + { + return features.GetRequiredFeature(); + } + + public static IHttpResponseFeature GetResponseFeature(this IFeatureCollection features) + { + return features.GetRequiredFeature(); + } + + public static IHttpResponseBodyFeature GetResponseBodyFeature(this IFeatureCollection features) + { + return features.GetRequiredFeature(); + } + + public static TFeature GetRequiredFeature(this IFeatureCollection features) + { + return features.Get() ?? throw new InvalidOperationException($"Feature of type {typeof(TFeature).Name} not found"); + } +} diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/Kestrel.csproj b/src/BenchmarksApps/TechEmpower/Kestrel/Kestrel.csproj new file mode 100644 index 000000000..6568b3dcf --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/Kestrel/Kestrel.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs b/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs new file mode 100644 index 000000000..a766efa9b --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs @@ -0,0 +1,38 @@ +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Hosting.Server.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 socketOptions = new SocketTransportOptions() +{ + WaitForDataBeforeAllocatingBuffer = false, + UnsafePreferInlineScheduling = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS") == "1" +}; +if (int.TryParse(Environment.GetEnvironmentVariable("threadCount"), out var value)) +{ + socketOptions.IOQueueCount = value; +} +using var server = new KestrelServer( + Options.Create(new KestrelServerOptions()), + new SocketTransportFactory(Options.Create(socketOptions), loggerFactory), + loggerFactory + ); + +await server.StartAsync(new BenchmarkApp(), CancellationToken.None); + +var addresses = server.Features.GetRequiredFeature().Addresses; +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."); diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/Properties/launchSettings.json b/src/BenchmarksApps/TechEmpower/Kestrel/Properties/launchSettings.json new file mode 100644 index 000000000..c219b49c0 --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/Kestrel/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml b/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml new file mode 100644 index 000000000..a537b814d --- /dev/null +++ b/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml @@ -0,0 +1,49 @@ +imports: + - https://raw.githubusercontent.com/dotnet/crank/main/src/Microsoft.Crank.Jobs.Wrk/wrk.yml + - https://github.com/aspnet/Benchmarks/blob/main/scenarios/aspnet.profiles.standard.yml?raw=true + +variables: + serverPort: 5000 + +jobs: + kestrel: + source: + repository: https://github.com/aspnet/benchmarks.git + branchOrCommit: main + project: src/BenchmarksApps/TechEmpower/Kestrel/Kestrel.csproj + readyStateText: Application started. + arguments: "--urls {{serverScheme}}://{{serverAddress}}:{{serverPort}}" + variables: + serverScheme: http + environmentVariables: + +scenarios: + plaintext: + application: + job: kestrel + load: + job: wrk + variables: + presetHeaders: plaintext + path: /plaintext + pipeline: 16 + + json: + application: + job: kestrel + load: + job: wrk + variables: + presetHeaders: json + path: /json + +profiles: + # this profile uses the local folder as the source + # instead of the public repository + source: + agents: + main: + source: + localFolder: . + respository: '' + project: Kestrel.csproj From b8ecf767381ae822fe50b5f3f6c915b0c35dfbd6 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 13 Jan 2025 15:00:08 -0800 Subject: [PATCH 02/18] Tweaks --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 0a0655255..c87a2fe8c 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -33,12 +33,10 @@ private static async Task NotFound(IHttpRequestFeature req, IHttpResponseFeature { res.StatusCode = 404; res.Headers.ContentType = "text/plain"; - res.Headers.ContentLength = HelloWorldPayload.Length; var body = features.GetResponseBodyFeature(); await body.StartAsync(); - body.Writer.Write(HelloWorldPayload); await body.CompleteAsync(); } @@ -56,8 +54,6 @@ private static async Task Index(IHttpRequestFeature req, IHttpResponseFeature re await body.StartAsync(); body.Writer.Write(IndexPayload); - await body.Writer.FlushAsync(); - await body.CompleteAsync(); } private static ReadOnlySpan HelloWorldPayload => "Hello, World!"u8; @@ -69,11 +65,8 @@ private static async Task Plaintext(IHttpRequestFeature req, IHttpResponseFeatur res.Headers.ContentLength = HelloWorldPayload.Length; var body = features.GetResponseBodyFeature(); - await body.StartAsync(); body.Writer.Write(HelloWorldPayload); - await body.Writer.FlushAsync(); - await body.CompleteAsync(); } private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); @@ -84,31 +77,32 @@ private static async Task Plaintext(IHttpRequestFeature req, IHttpResponseFeatur private static async Task Json(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = 200; - res.Headers.ContentType = "application/json"; + res.Headers.ContentType = "application/json; charset=utf-8"; - //Span buffer = stackalloc byte[256]; var bufferWriter = _bufferWriterPool.Get(); var jsonWriter = _jsonWriterPool.Get(); bufferWriter.ResetWrittenCount(); jsonWriter.Reset(bufferWriter); - JsonSerializer.Serialize(jsonWriter, new { message = "Hello, World!" }, _jsonSerializerOptions); + JsonSerializer.Serialize(jsonWriter, new JsonMessage { message = "Hello, World!" }, _jsonSerializerOptions); res.Headers.ContentLength = bufferWriter.WrittenCount; var body = features.GetResponseBodyFeature(); await body.StartAsync(); - bufferWriter.WrittenSpan.CopyTo(body.Writer.GetSpan(bufferWriter.WrittenCount)); - body.Writer.Advance(bufferWriter.WrittenCount); - await body.Writer.FlushAsync(); - await body.CompleteAsync(); + body.Writer.Write(bufferWriter.WrittenSpan); _jsonWriterPool.Return(jsonWriter); _bufferWriterPool.Return(bufferWriter); } + private struct JsonMessage + { + public required string message { get; set; } + } + private class Utf8JsonWriterPooledObjectPolicy : IPooledObjectPolicy { private static readonly ArrayBufferWriter _dummyBufferWriter = new(256); From 32797b41e6936044553a342dfbbb95948ebde20f Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 13 Jan 2025 15:42:13 -0800 Subject: [PATCH 03/18] Call FlushAsync explicitly --- src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index c87a2fe8c..8e6e6693e 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -54,6 +54,7 @@ private static async Task Index(IHttpRequestFeature req, IHttpResponseFeature re await body.StartAsync(); body.Writer.Write(IndexPayload); + await body.Writer.FlushAsync(); } private static ReadOnlySpan HelloWorldPayload => "Hello, World!"u8; @@ -67,6 +68,7 @@ private static async Task Plaintext(IHttpRequestFeature req, IHttpResponseFeatur var body = features.GetResponseBodyFeature(); await body.StartAsync(); body.Writer.Write(HelloWorldPayload); + await body.Writer.FlushAsync(); } private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); @@ -93,6 +95,7 @@ private static async Task Json(IHttpRequestFeature req, IHttpResponseFeature res await body.StartAsync(); body.Writer.Write(bufferWriter.WrittenSpan); + await body.Writer.FlushAsync(); _jsonWriterPool.Return(jsonWriter); _bufferWriterPool.Return(bufferWriter); @@ -109,10 +112,6 @@ private class Utf8JsonWriterPooledObjectPolicy : IPooledObjectPolicy new(_dummyBufferWriter, new() { Indented = false, SkipValidation = true }); - public bool Return(Utf8JsonWriter obj) - { - //obj.Reset(); - return true; - } + public bool Return(Utf8JsonWriter obj) => true; } } From f374f4ca6d0a2c55e7751cc28f6e62ea3dc54a9a Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 13 Jan 2025 16:40:45 -0800 Subject: [PATCH 04/18] Update kestrel.benchmarks.yml --- src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml b/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml index a537b814d..ab0c5aee9 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml +++ b/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml @@ -15,7 +15,6 @@ jobs: arguments: "--urls {{serverScheme}}://{{serverAddress}}:{{serverPort}}" variables: serverScheme: http - environmentVariables: scenarios: plaintext: From 66029f3d2d113e5949f5edeaab9ffbf25472fc17 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 13 Jan 2025 17:12:46 -0800 Subject: [PATCH 05/18] Support passed URLs --- .../TechEmpower/Kestrel/Program.cs | 18 ++++++++++++++++-- .../Kestrel/Properties/launchSettings.json | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs b/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs index a766efa9b..191b93b1b 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs @@ -6,13 +6,18 @@ 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(Environment.GetEnvironmentVariable("threadCount"), out var value)) +if (int.TryParse(configuration["threadCount"], out var value)) { socketOptions.IOQueueCount = value; } @@ -22,9 +27,18 @@ loggerFactory ); +var addresses = server.Features.GetRequiredFeature().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); -var addresses = server.Features.GetRequiredFeature().Addresses; foreach (var address in addresses) { Console.WriteLine($"Now listening on: {address}"); diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/Properties/launchSettings.json b/src/BenchmarksApps/TechEmpower/Kestrel/Properties/launchSettings.json index c219b49c0..b7581646a 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/Properties/launchSettings.json +++ b/src/BenchmarksApps/TechEmpower/Kestrel/Properties/launchSettings.json @@ -5,6 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, + "applicationUrl": "http://localhost:5123", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } From 60c7ac1db41891470386dae9cd5bb81000225e26 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 13 Jan 2025 17:51:23 -0800 Subject: [PATCH 06/18] More tweaks & add JSON auto-chunked --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 48 +++++++++++-------- .../Kestrel/kestrel.benchmarks.yml | 9 ++++ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 8e6e6693e..5c19bb514 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -15,38 +15,32 @@ public Task ProcessRequestAsync(IFeatureCollection features) if (req.Method != "GET") { - res.StatusCode = 405; - var body = features.GetResponseBodyFeature(); - return body.StartAsync().ContinueWith(t => body.CompleteAsync()); + res.StatusCode = StatusCodes.Status405MethodNotAllowed; } return req.Path switch { - "/plaintext" => Plaintext(req, res, features), - "/json" => Json(req, res, features), - "/" => Index(req, res, features), - _ => NotFound(req, res, features), + "/plaintext" => Plaintext(res, features), + "/json" => Json(res, features), + "/json-chunked" => JsonChunked(res, features), + "/" => Index(res, features), + _ => NotFound(res, features), }; } - private static async Task NotFound(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features) + private static Task NotFound(IHttpResponseFeature res, IFeatureCollection features) { - res.StatusCode = 404; - res.Headers.ContentType = "text/plain"; - - var body = features.GetResponseBodyFeature(); - - await body.StartAsync(); - await body.CompleteAsync(); + res.StatusCode = StatusCodes.Status404NotFound; + return Task.CompletedTask; } public void DisposeContext(IFeatureCollection features, Exception? exception) { } private static ReadOnlySpan IndexPayload => "Running directly on Kestrel! Navigate to /plaintext and /json to see other endpoints."u8; - private static async Task Index(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features) + private static async Task Index(IHttpResponseFeature res, IFeatureCollection features) { - res.StatusCode = 200; + res.StatusCode = StatusCodes.Status200OK; res.Headers.ContentType = "text/plain"; res.Headers.ContentLength = IndexPayload.Length; @@ -59,9 +53,9 @@ private static async Task Index(IHttpRequestFeature req, IHttpResponseFeature re private static ReadOnlySpan HelloWorldPayload => "Hello, World!"u8; - private static async Task Plaintext(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features) + private static async Task Plaintext(IHttpResponseFeature res, IFeatureCollection features) { - res.StatusCode = 200; + res.StatusCode = StatusCodes.Status200OK; res.Headers.ContentType = "text/plain"; res.Headers.ContentLength = HelloWorldPayload.Length; @@ -72,13 +66,25 @@ private static async Task Plaintext(IHttpRequestFeature req, IHttpResponseFeatur } private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); + + private static async Task JsonChunked(IHttpResponseFeature res, IFeatureCollection features) + { + res.StatusCode = StatusCodes.Status200OK; + res.Headers.ContentType = "application/json; charset=utf-8"; + + var body = features.GetResponseBodyFeature(); + await body.StartAsync(); + await JsonSerializer.SerializeAsync(body.Writer, new JsonMessage { message = "Hello, World!" }, _jsonSerializerOptions); + await body.Writer.FlushAsync(); + } + private static readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider(); private static readonly ObjectPool> _bufferWriterPool = _objectPoolProvider.Create>(); private static readonly ObjectPool _jsonWriterPool = _objectPoolProvider.Create(new Utf8JsonWriterPooledObjectPolicy()); - private static async Task Json(IHttpRequestFeature req, IHttpResponseFeature res, IFeatureCollection features) + private static async Task Json(IHttpResponseFeature res, IFeatureCollection features) { - res.StatusCode = 200; + res.StatusCode = StatusCodes.Status200OK; res.Headers.ContentType = "application/json; charset=utf-8"; var bufferWriter = _bufferWriterPool.Get(); diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml b/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml index ab0c5aee9..a87a72bba 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml +++ b/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml @@ -36,6 +36,15 @@ scenarios: presetHeaders: json path: /json + json-chunked: + application: + job: kestrel + load: + job: wrk + variables: + presetHeaders: json + path: /json-chunked + profiles: # this profile uses the local folder as the source # instead of the public repository From 44f03035ca5c07d1057dc4a93e6484227c80bf26 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 13 Jan 2025 18:03:56 -0800 Subject: [PATCH 07/18] More JSON variants --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 37 +++++++++++++++++++ .../Kestrel/kestrel.benchmarks.yml | 18 +++++++++ 2 files changed, 55 insertions(+) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 5c19bb514..0f9e3b5dc 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; @@ -22,6 +23,8 @@ public Task ProcessRequestAsync(IFeatureCollection features) { "/plaintext" => Plaintext(res, features), "/json" => Json(res, features), + "/json-string" => JsonString(res, features), + "/json-utf8bytes" => JsonUtf8Bytes(res, features), "/json-chunked" => JsonChunked(res, features), "/" => Index(res, features), _ => NotFound(res, features), @@ -78,6 +81,40 @@ private static async Task JsonChunked(IHttpResponseFeature res, IFeatureCollecti await body.Writer.FlushAsync(); } + private static async Task JsonString(IHttpResponseFeature res, IFeatureCollection features) + { + res.StatusCode = StatusCodes.Status200OK; + res.Headers.ContentType = "application/json; charset=utf-8"; + + var message = JsonSerializer.Serialize(new JsonMessage { message = "Hello, World!" }, _jsonSerializerOptions); + res.Headers.ContentLength = Encoding.UTF8.GetByteCount(message); + + var body = features.GetResponseBodyFeature(); + await body.StartAsync(); + + Span buffer = stackalloc byte[256]; + var length = Encoding.UTF8.GetBytes(message, buffer); + body.Writer.Write(buffer[..length]); + + await body.Writer.FlushAsync(); + } + + private static async Task JsonUtf8Bytes(IHttpResponseFeature res, IFeatureCollection features) + { + res.StatusCode = StatusCodes.Status200OK; + res.Headers.ContentType = "application/json; charset=utf-8"; + + var messageBytes = JsonSerializer.SerializeToUtf8Bytes(new JsonMessage { message = "Hello, World!" }, _jsonSerializerOptions); + res.Headers.ContentLength = messageBytes.Length; + + var body = features.GetResponseBodyFeature(); + await body.StartAsync(); + + body.Writer.Write(messageBytes); + + await body.Writer.FlushAsync(); + } + private static readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider(); private static readonly ObjectPool> _bufferWriterPool = _objectPoolProvider.Create>(); private static readonly ObjectPool _jsonWriterPool = _objectPoolProvider.Create(new Utf8JsonWriterPooledObjectPolicy()); diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml b/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml index a87a72bba..cad79e21d 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml +++ b/src/BenchmarksApps/TechEmpower/Kestrel/kestrel.benchmarks.yml @@ -36,6 +36,24 @@ scenarios: presetHeaders: json path: /json + json-string: + application: + job: kestrel + load: + job: wrk + variables: + presetHeaders: json + path: /json-string + + json-utf8bytes: + application: + job: kestrel + load: + job: wrk + variables: + presetHeaders: json + path: /json-utf8bytes + json-chunked: application: job: kestrel From 2d5bdda5701698160b89847cfa5fe5af88422d87 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 15 Jan 2025 11:48:19 -0800 Subject: [PATCH 08/18] Use JSON source generator --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 32 +++++++++++++------ .../TechEmpower/Kestrel/ConsoleLifetime.cs | 2 ++ .../Kestrel/FeatureCollectionExtensions.cs | 7 +--- .../TechEmpower/Kestrel/Program.cs | 2 ++ 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 0f9e3b5dc..39b36993f 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -1,11 +1,15 @@ using System.Buffers; +using System.Formats.Asn1; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.ObjectPool; -public class BenchmarkApp : IHttpApplication +namespace Kestrel; + +public sealed partial class BenchmarkApp : IHttpApplication { public IFeatureCollection CreateContext(IFeatureCollection features) => features; @@ -68,8 +72,6 @@ private static async Task Plaintext(IHttpResponseFeature res, IFeatureCollection await body.Writer.FlushAsync(); } - private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web); - private static async Task JsonChunked(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; @@ -77,7 +79,7 @@ private static async Task JsonChunked(IHttpResponseFeature res, IFeatureCollecti var body = features.GetResponseBodyFeature(); await body.StartAsync(); - await JsonSerializer.SerializeAsync(body.Writer, new JsonMessage { message = "Hello, World!" }, _jsonSerializerOptions); + await JsonSerializer.SerializeAsync(body.Writer, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); await body.Writer.FlushAsync(); } @@ -86,7 +88,7 @@ private static async Task JsonString(IHttpResponseFeature res, IFeatureCollectio res.StatusCode = StatusCodes.Status200OK; res.Headers.ContentType = "application/json; charset=utf-8"; - var message = JsonSerializer.Serialize(new JsonMessage { message = "Hello, World!" }, _jsonSerializerOptions); + var message = JsonSerializer.Serialize(new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); res.Headers.ContentLength = Encoding.UTF8.GetByteCount(message); var body = features.GetResponseBodyFeature(); @@ -104,7 +106,7 @@ private static async Task JsonUtf8Bytes(IHttpResponseFeature res, IFeatureCollec res.StatusCode = StatusCodes.Status200OK; res.Headers.ContentType = "application/json; charset=utf-8"; - var messageBytes = JsonSerializer.SerializeToUtf8Bytes(new JsonMessage { message = "Hello, World!" }, _jsonSerializerOptions); + var messageBytes = JsonSerializer.SerializeToUtf8Bytes(new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); res.Headers.ContentLength = messageBytes.Length; var body = features.GetResponseBodyFeature(); @@ -130,7 +132,10 @@ private static async Task Json(IHttpResponseFeature res, IFeatureCollection feat bufferWriter.ResetWrittenCount(); jsonWriter.Reset(bufferWriter); - JsonSerializer.Serialize(jsonWriter, new JsonMessage { message = "Hello, World!" }, _jsonSerializerOptions); + JsonSerializer.Serialize(jsonWriter, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); + + _jsonWriterPool.Return(jsonWriter); + _bufferWriterPool.Return(bufferWriter); res.Headers.ContentLength = bufferWriter.WrittenCount; @@ -139,9 +144,6 @@ private static async Task Json(IHttpResponseFeature res, IFeatureCollection feat await body.StartAsync(); body.Writer.Write(bufferWriter.WrittenSpan); await body.Writer.FlushAsync(); - - _jsonWriterPool.Return(jsonWriter); - _bufferWriterPool.Return(bufferWriter); } private struct JsonMessage @@ -157,4 +159,14 @@ private class Utf8JsonWriterPooledObjectPolicy : IPooledObjectPolicy true; } + + private static readonly JsonContext SerializerContext = JsonContext.Default; + + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(JsonMessage))] + private partial class JsonContext : JsonSerializerContext + { + + } + } diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/ConsoleLifetime.cs b/src/BenchmarksApps/TechEmpower/Kestrel/ConsoleLifetime.cs index adc811ac4..97303a6e6 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/ConsoleLifetime.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/ConsoleLifetime.cs @@ -1,6 +1,8 @@ using System.Diagnostics; using System.Runtime.InteropServices; +namespace Kestrel; + public class ConsoleLifetime : IDisposable { private readonly TaskCompletionSource _tcs = new(); diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/FeatureCollectionExtensions.cs b/src/BenchmarksApps/TechEmpower/Kestrel/FeatureCollectionExtensions.cs index f70c35240..f04af5a7f 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/FeatureCollectionExtensions.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/FeatureCollectionExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http.Features; +namespace Microsoft.AspNetCore.Http.Features; public static class FeatureCollectionExtensions { @@ -16,9 +16,4 @@ public static IHttpResponseBodyFeature GetResponseBodyFeature(this IFeatureColle { return features.GetRequiredFeature(); } - - public static TFeature GetRequiredFeature(this IFeatureCollection features) - { - return features.Get() ?? throw new InvalidOperationException($"Feature of type {typeof(TFeature).Name} not found"); - } } diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs b/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs index 191b93b1b..8f98c9080 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs @@ -1,5 +1,7 @@ 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; From 26f3b685f785312dd8e7fc4a3d63999d3ccab1ed Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 15 Jan 2025 15:22:23 -0800 Subject: [PATCH 09/18] Use JsonSourceGenerationMode.Default --- src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs | 5 ++--- .../TechEmpower/Kestrel/Properties/launchSettings.json | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 39b36993f..7a6ef696a 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -1,5 +1,4 @@ using System.Buffers; -using System.Formats.Asn1; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -162,11 +161,11 @@ private class Utf8JsonWriterPooledObjectPolicy : IPooledObjectPolicy Date: Wed, 15 Jan 2025 17:10:50 -0800 Subject: [PATCH 10/18] Don't pool writers --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 7a6ef696a..10a61f00e 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -125,16 +125,16 @@ private static async Task Json(IHttpResponseFeature res, IFeatureCollection feat res.StatusCode = StatusCodes.Status200OK; res.Headers.ContentType = "application/json; charset=utf-8"; - var bufferWriter = _bufferWriterPool.Get(); - var jsonWriter = _jsonWriterPool.Get(); + //var bufferWriter = _bufferWriterPool.Get(); + //var jsonWriter = _jsonWriterPool.Get(); - bufferWriter.ResetWrittenCount(); - jsonWriter.Reset(bufferWriter); + var bufferWriter = new ArrayBufferWriter(64); + await using var jsonWriter = new Utf8JsonWriter(bufferWriter, new() { Indented = false, SkipValidation = true }); - JsonSerializer.Serialize(jsonWriter, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); + //bufferWriter.ResetWrittenCount(); + //jsonWriter.Reset(bufferWriter); - _jsonWriterPool.Return(jsonWriter); - _bufferWriterPool.Return(bufferWriter); + JsonSerializer.Serialize(jsonWriter, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); res.Headers.ContentLength = bufferWriter.WrittenCount; @@ -143,6 +143,9 @@ private static async Task Json(IHttpResponseFeature res, IFeatureCollection feat await body.StartAsync(); body.Writer.Write(bufferWriter.WrittenSpan); await body.Writer.FlushAsync(); + + //_jsonWriterPool.Return(jsonWriter); + //_bufferWriterPool.Return(bufferWriter); } private struct JsonMessage From 9e8a6b053155702b7fd88a294b2f313abff9ec69 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 15 Jan 2025 17:24:00 -0800 Subject: [PATCH 11/18] Thread static writers --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 10a61f00e..fbf3b68eb 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -1,4 +1,6 @@ using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Formats.Asn1; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -128,26 +130,46 @@ private static async Task Json(IHttpResponseFeature res, IFeatureCollection feat //var bufferWriter = _bufferWriterPool.Get(); //var jsonWriter = _jsonWriterPool.Get(); - var bufferWriter = new ArrayBufferWriter(64); - await using var jsonWriter = new Utf8JsonWriter(bufferWriter, new() { Indented = false, SkipValidation = true }); + //var bufferWriter = new ArrayBufferWriter(64); + //await using var jsonWriter = new Utf8JsonWriter(bufferWriter, new() { Indented = false, SkipValidation = true }); //bufferWriter.ResetWrittenCount(); //jsonWriter.Reset(bufferWriter); - JsonSerializer.Serialize(jsonWriter, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); + //JsonSerializer.Serialize(jsonWriter, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); - res.Headers.ContentLength = bufferWriter.WrittenCount; + var messageSpan = WriteMessage(new JsonMessage { message = "Hello, World!" }); + res.Headers.ContentLength = messageSpan.Length; var body = features.GetResponseBodyFeature(); + body.Writer.Write(messageSpan); + await body.StartAsync(); - body.Writer.Write(bufferWriter.WrittenSpan); await body.Writer.FlushAsync(); //_jsonWriterPool.Return(jsonWriter); //_bufferWriterPool.Return(bufferWriter); } + [ThreadStatic] + private static ArrayBufferWriter? _bufferWriter; + [ThreadStatic] + private static Utf8JsonWriter? _jsonWriter; + + private static ReadOnlySpan WriteMessage(JsonMessage message) + { + var bufferWriter = _bufferWriter ??= new(64); + var jsonWriter = _jsonWriter ??= new(_bufferWriter, new() { Indented = false, SkipValidation = true }); + + bufferWriter.ResetWrittenCount(); + jsonWriter.Reset(bufferWriter); + + JsonSerializer.Serialize(jsonWriter, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); + + return bufferWriter.WrittenSpan; + } + private struct JsonMessage { public required string message { get; set; } From 7bead583d67fce4ba6238a9e4cc6af8522562722 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 15 Jan 2025 17:31:22 -0800 Subject: [PATCH 12/18] Remove writer pooling --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 37 ++----------------- 1 file changed, 4 insertions(+), 33 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index fbf3b68eb..6d836f0a4 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -1,12 +1,10 @@ using System.Buffers; -using System.Diagnostics.CodeAnalysis; -using System.Formats.Asn1; 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; -using Microsoft.Extensions.ObjectPool; namespace Kestrel; @@ -118,27 +116,12 @@ private static async Task JsonUtf8Bytes(IHttpResponseFeature res, IFeatureCollec await body.Writer.FlushAsync(); } - private static readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider(); - private static readonly ObjectPool> _bufferWriterPool = _objectPoolProvider.Create>(); - private static readonly ObjectPool _jsonWriterPool = _objectPoolProvider.Create(new Utf8JsonWriterPooledObjectPolicy()); - private static async Task Json(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; res.Headers.ContentType = "application/json; charset=utf-8"; - //var bufferWriter = _bufferWriterPool.Get(); - //var jsonWriter = _jsonWriterPool.Get(); - - //var bufferWriter = new ArrayBufferWriter(64); - //await using var jsonWriter = new Utf8JsonWriter(bufferWriter, new() { Indented = false, SkipValidation = true }); - - //bufferWriter.ResetWrittenCount(); - //jsonWriter.Reset(bufferWriter); - - //JsonSerializer.Serialize(jsonWriter, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); - - var messageSpan = WriteMessage(new JsonMessage { message = "Hello, World!" }); + var messageSpan = JsonSerializeToUtf8Span(new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); res.Headers.ContentLength = messageSpan.Length; var body = features.GetResponseBodyFeature(); @@ -147,9 +130,6 @@ private static async Task Json(IHttpResponseFeature res, IFeatureCollection feat await body.StartAsync(); await body.Writer.FlushAsync(); - - //_jsonWriterPool.Return(jsonWriter); - //_bufferWriterPool.Return(bufferWriter); } [ThreadStatic] @@ -157,7 +137,7 @@ private static async Task Json(IHttpResponseFeature res, IFeatureCollection feat [ThreadStatic] private static Utf8JsonWriter? _jsonWriter; - private static ReadOnlySpan WriteMessage(JsonMessage message) + private static ReadOnlySpan JsonSerializeToUtf8Span(T value, JsonTypeInfo jsonTypeInfo) { var bufferWriter = _bufferWriter ??= new(64); var jsonWriter = _jsonWriter ??= new(_bufferWriter, new() { Indented = false, SkipValidation = true }); @@ -165,7 +145,7 @@ private static ReadOnlySpan WriteMessage(JsonMessage message) bufferWriter.ResetWrittenCount(); jsonWriter.Reset(bufferWriter); - JsonSerializer.Serialize(jsonWriter, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); + JsonSerializer.Serialize(jsonWriter, value, jsonTypeInfo); return bufferWriter.WrittenSpan; } @@ -175,15 +155,6 @@ private struct JsonMessage public required string message { get; set; } } - private class Utf8JsonWriterPooledObjectPolicy : IPooledObjectPolicy - { - private static readonly ArrayBufferWriter _dummyBufferWriter = new(256); - - public Utf8JsonWriter Create() => new(_dummyBufferWriter, new() { Indented = false, SkipValidation = true }); - - public bool Return(Utf8JsonWriter obj) => true; - } - private static readonly JsonContext SerializerContext = JsonContext.Default; // BUG: Can't use GenerationMode = JsonSourceGenerationMode.Serialization here due to https://github.com/dotnet/runtime/issues/111477 From 3f7dec50fae480b96283678c71ea826a15439c34 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 15 Jan 2025 17:48:42 -0800 Subject: [PATCH 13/18] Use constants for content types --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 6d836f0a4..a3bd2b888 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -10,6 +10,9 @@ namespace Kestrel; public sealed partial class BenchmarkApp : IHttpApplication { + 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) @@ -47,7 +50,7 @@ public void DisposeContext(IFeatureCollection features, Exception? exception) { private static async Task Index(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; - res.Headers.ContentType = "text/plain"; + res.Headers.ContentType = TextPlainContentType; res.Headers.ContentLength = IndexPayload.Length; var body = features.GetResponseBodyFeature(); @@ -62,7 +65,7 @@ private static async Task Index(IHttpResponseFeature res, IFeatureCollection fea private static async Task Plaintext(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; - res.Headers.ContentType = "text/plain"; + res.Headers.ContentType = TextPlainContentType; res.Headers.ContentLength = HelloWorldPayload.Length; var body = features.GetResponseBodyFeature(); @@ -74,7 +77,7 @@ private static async Task Plaintext(IHttpResponseFeature res, IFeatureCollection private static async Task JsonChunked(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; - res.Headers.ContentType = "application/json; charset=utf-8"; + res.Headers.ContentType = JsonContentTypeWithCharset; var body = features.GetResponseBodyFeature(); await body.StartAsync(); @@ -85,7 +88,7 @@ private static async Task JsonChunked(IHttpResponseFeature res, IFeatureCollecti private static async Task JsonString(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; - res.Headers.ContentType = "application/json; charset=utf-8"; + res.Headers.ContentType = JsonContentTypeWithCharset; var message = JsonSerializer.Serialize(new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); res.Headers.ContentLength = Encoding.UTF8.GetByteCount(message); @@ -103,7 +106,7 @@ private static async Task JsonString(IHttpResponseFeature res, IFeatureCollectio private static async Task JsonUtf8Bytes(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; - res.Headers.ContentType = "application/json; charset=utf-8"; + res.Headers.ContentType = JsonContentTypeWithCharset; var messageBytes = JsonSerializer.SerializeToUtf8Bytes(new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); res.Headers.ContentLength = messageBytes.Length; @@ -119,7 +122,7 @@ private static async Task JsonUtf8Bytes(IHttpResponseFeature res, IFeatureCollec private static async Task Json(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; - res.Headers.ContentType = "application/json; charset=utf-8"; + res.Headers.ContentType = JsonContentTypeWithCharset; var messageSpan = JsonSerializeToUtf8Span(new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); res.Headers.ContentLength = messageSpan.Length; From f6fc284b6142c583aee107709423437e0f055132 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 15 Jan 2025 18:05:09 -0800 Subject: [PATCH 14/18] Update BenchmarkApp.cs --- src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index a3bd2b888..61aaa6569 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -23,6 +23,7 @@ public Task ProcessRequestAsync(IFeatureCollection features) if (req.Method != "GET") { res.StatusCode = StatusCodes.Status405MethodNotAllowed; + return Task.CompletedTask; } return req.Path switch From 5741ac722ed934d1dbc23e7c3a05a7701dbafb78 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 17 Jan 2025 11:44:59 -0800 Subject: [PATCH 15/18] Use span comparison for request dispatching --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 62 +++++++++++++++---- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 61aaa6569..96275381c 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -20,22 +20,50 @@ public Task ProcessRequestAsync(IFeatureCollection features) var req = features.GetRequestFeature(); var res = features.GetResponseFeature(); - if (req.Method != "GET") + //if (req.Method != "GET") + //{ + // res.StatusCode = StatusCodes.Status405MethodNotAllowed; + // return Task.CompletedTask; + //} + + var pathSpan = req.Path.AsSpan(); + if (Paths.IsPath(pathSpan, Paths.Plaintext)) { - res.StatusCode = StatusCodes.Status405MethodNotAllowed; - return Task.CompletedTask; + return Plaintext(res, features); } - - return req.Path switch + else if (Paths.IsPath(pathSpan, Paths.Json)) + { + return Json(res, features); + } + else if (pathSpan.StartsWith("json-string", StringComparison.OrdinalIgnoreCase)) + { + return JsonString(res, features); + } + else if (pathSpan.StartsWith("json-utf8bytes", StringComparison.OrdinalIgnoreCase)) + { + return JsonUtf8Bytes(res, features); + } + else if (pathSpan.StartsWith("json-chunked", StringComparison.OrdinalIgnoreCase)) { - "/plaintext" => Plaintext(res, features), - "/json" => Json(res, features), - "/json-string" => JsonString(res, features), - "/json-utf8bytes" => JsonUtf8Bytes(res, features), - "/json-chunked" => JsonChunked(res, features), - "/" => Index(res, features), - _ => NotFound(res, features), - }; + return JsonChunked(res, features); + } + else if (pathSpan.IsEmpty || pathSpan.Equals("/", StringComparison.OrdinalIgnoreCase)) + { + return Index(res, features); + } + + return NotFound(res, features); + + //return req.Path switch + //{ + // "/plaintext" => Plaintext(res, features), + // "/json" => Json(res, features), + // "/json-string" => JsonString(res, features), + // "/json-utf8bytes" => JsonUtf8Bytes(res, features), + // "/json-chunked" => JsonChunked(res, features), + // "/" => Index(res, features), + // _ => NotFound(res, features), + //}; } private static Task NotFound(IHttpResponseFeature res, IFeatureCollection features) @@ -168,4 +196,12 @@ private partial class JsonContext : JsonSerializerContext { } + + private static class Paths + { + public static ReadOnlySpan Plaintext => "/plaintext"; + public static ReadOnlySpan Json => "/json"; + + public static bool IsPath(ReadOnlySpan path, ReadOnlySpan targetPath) => path.SequenceEqual(targetPath); + } } From 82b7d3ebaef886209757603e3a58740471d4a746 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 17 Jan 2025 13:21:15 -0800 Subject: [PATCH 16/18] Tweaks --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 29 ++++++++----------- .../TechEmpower/Kestrel/Program.cs | 6 +++- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 96275381c..3cf07a47c 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -35,35 +36,24 @@ public Task ProcessRequestAsync(IFeatureCollection features) { return Json(res, features); } - else if (pathSpan.StartsWith("json-string", StringComparison.OrdinalIgnoreCase)) + else if (Paths.IsPath(pathSpan, Paths.JsonString)) { return JsonString(res, features); } - else if (pathSpan.StartsWith("json-utf8bytes", StringComparison.OrdinalIgnoreCase)) + else if (Paths.IsPath(pathSpan, Paths.JsonUtf8Bytes)) { return JsonUtf8Bytes(res, features); } - else if (pathSpan.StartsWith("json-chunked", StringComparison.OrdinalIgnoreCase)) + else if (Paths.IsPath(pathSpan, Paths.JsonChunked)) { return JsonChunked(res, features); } - else if (pathSpan.IsEmpty || pathSpan.Equals("/", StringComparison.OrdinalIgnoreCase)) + else if (pathSpan.IsEmpty || Paths.IsPath(pathSpan, Paths.Index)) { return Index(res, features); } return NotFound(res, features); - - //return req.Path switch - //{ - // "/plaintext" => Plaintext(res, features), - // "/json" => Json(res, features), - // "/json-string" => JsonString(res, features), - // "/json-utf8bytes" => JsonUtf8Bytes(res, features), - // "/json-chunked" => JsonChunked(res, features), - // "/" => Index(res, features), - // _ => NotFound(res, features), - //}; } private static Task NotFound(IHttpResponseFeature res, IFeatureCollection features) @@ -160,8 +150,8 @@ private static async Task Json(IHttpResponseFeature res, IFeatureCollection feat body.Writer.Write(messageSpan); - await body.StartAsync(); - await body.Writer.FlushAsync(); + //await body.StartAsync(); + //await body.Writer.FlushAsync(); } [ThreadStatic] @@ -201,7 +191,12 @@ private static class Paths { public static ReadOnlySpan Plaintext => "/plaintext"; public static ReadOnlySpan Json => "/json"; + public static ReadOnlySpan JsonString => "/json-string"; + public static ReadOnlySpan JsonChunked => "/json-chunked"; + public static ReadOnlySpan JsonUtf8Bytes => "/json-utf8bytes"; + public static ReadOnlySpan Index => "/"; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsPath(ReadOnlySpan path, ReadOnlySpan targetPath) => path.SequenceEqual(targetPath); } } diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs b/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs index 8f98c9080..e439455d2 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/Program.cs @@ -23,8 +23,12 @@ { socketOptions.IOQueueCount = value; } +var kestrelOptions = new KestrelServerOptions(); +kestrelOptions.Limits.MinRequestBodyDataRate = null; +kestrelOptions.Limits.MinResponseDataRate = null; + using var server = new KestrelServer( - Options.Create(new KestrelServerOptions()), + Options.Create(kestrelOptions), new SocketTransportFactory(Options.Create(socketOptions), loggerFactory), loggerFactory ); From 32a14606127fa84fcf446f3506b06646ae3a9f77 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 17 Jan 2025 13:22:37 -0800 Subject: [PATCH 17/18] Update BenchmarkApp.cs --- src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 3cf07a47c..0b5836649 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -138,7 +138,7 @@ private static async Task JsonUtf8Bytes(IHttpResponseFeature res, IFeatureCollec await body.Writer.FlushAsync(); } - private static async Task Json(IHttpResponseFeature res, IFeatureCollection features) + private static Task Json(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; res.Headers.ContentType = JsonContentTypeWithCharset; @@ -152,6 +152,7 @@ private static async Task Json(IHttpResponseFeature res, IFeatureCollection feat //await body.StartAsync(); //await body.Writer.FlushAsync(); + return Task.CompletedTask; } [ThreadStatic] From 50f48a4eefa844937b3ab59f739ea3d775eb1ed9 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 17 Jan 2025 13:27:18 -0800 Subject: [PATCH 18/18] Update BenchmarkApp.cs --- .../TechEmpower/Kestrel/BenchmarkApp.cs | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs index 0b5836649..960ceda95 100644 --- a/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs +++ b/src/BenchmarksApps/TechEmpower/Kestrel/BenchmarkApp.cs @@ -81,48 +81,46 @@ private static async Task Index(IHttpResponseFeature res, IFeatureCollection fea private static ReadOnlySpan HelloWorldPayload => "Hello, World!"u8; - private static async Task Plaintext(IHttpResponseFeature res, IFeatureCollection features) + 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(); - await body.StartAsync(); + body.Writer.Write(HelloWorldPayload); - await body.Writer.FlushAsync(); + + return Task.CompletedTask; } - private static async Task JsonChunked(IHttpResponseFeature res, IFeatureCollection features) + private static Task JsonChunked(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; res.Headers.ContentType = JsonContentTypeWithCharset; var body = features.GetResponseBodyFeature(); - await body.StartAsync(); - await JsonSerializer.SerializeAsync(body.Writer, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); - await body.Writer.FlushAsync(); + return JsonSerializer.SerializeAsync(body.Writer, new JsonMessage { message = "Hello, World!" }, SerializerContext.JsonMessage); } - private static async Task JsonString(IHttpResponseFeature res, IFeatureCollection features) + 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); - res.Headers.ContentLength = Encoding.UTF8.GetByteCount(message); + Span buffer = stackalloc byte[64]; + var length = Encoding.UTF8.GetBytes(message, buffer); + res.Headers.ContentLength = length; var body = features.GetResponseBodyFeature(); - await body.StartAsync(); - Span buffer = stackalloc byte[256]; - var length = Encoding.UTF8.GetBytes(message, buffer); body.Writer.Write(buffer[..length]); - await body.Writer.FlushAsync(); + return Task.CompletedTask; } - private static async Task JsonUtf8Bytes(IHttpResponseFeature res, IFeatureCollection features) + private static Task JsonUtf8Bytes(IHttpResponseFeature res, IFeatureCollection features) { res.StatusCode = StatusCodes.Status200OK; res.Headers.ContentType = JsonContentTypeWithCharset; @@ -131,11 +129,10 @@ private static async Task JsonUtf8Bytes(IHttpResponseFeature res, IFeatureCollec res.Headers.ContentLength = messageBytes.Length; var body = features.GetResponseBodyFeature(); - await body.StartAsync(); body.Writer.Write(messageBytes); - await body.Writer.FlushAsync(); + return Task.CompletedTask; } private static Task Json(IHttpResponseFeature res, IFeatureCollection features) @@ -150,8 +147,6 @@ private static Task Json(IHttpResponseFeature res, IFeatureCollection features) body.Writer.Write(messageSpan); - //await body.StartAsync(); - //await body.Writer.FlushAsync(); return Task.CompletedTask; }