Skip to content
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
12 changes: 6 additions & 6 deletions frameworks/CSharp/wiredio/benchmark_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"plaintext_url": "/plaintext",
"json_url": "/json",
"port": 8080,
"approach": "Stripped",
"classification": "Micro",
"approach": "Realistic",
"classification": "Platform",
"database": "None",
"framework": "Unhinged",
"language": "C#",
Expand All @@ -34,15 +34,15 @@
"webserver": "Unhinged",
"os": "Linux",
"database_os": "Linux",
"display_name": "Unhinged",
"display_name": "unhinged [52] [aot] [epoll]",
"notes": "epoll"
},
"mcr-p": {
"plaintext_url": "/plaintext",
"json_url": "/json",
"port": 8080,
"approach": "Stripped",
"classification": "Micro",
"approach": "Realistic",
"classification": "Platform",
"database": "None",
"framework": "Unhinged",
"language": "C#",
Expand All @@ -51,7 +51,7 @@
"webserver": "Unhinged",
"os": "Linux",
"database_os": "Linux",
"display_name": "Unhinged [p]",
"display_name": "unhinged [56] [aot] [epoll]",
"notes": "epoll"
}
}
Expand Down
4 changes: 2 additions & 2 deletions frameworks/CSharp/wiredio/src/Platform/Platform.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Expand All @@ -20,6 +20,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Unhinged" Version="9.0.7" />
<PackageReference Include="Unhinged" Version="10.0.0" />
</ItemGroup>
</Project>
75 changes: 26 additions & 49 deletions frameworks/CSharp/wiredio/src/Platform/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,6 @@

#pragma warning disable CA2014

/* (MDA2AV)Dev notes:
*
* Wired.IO Platform benchmark using [Unhinged - https://github.com/MDA2AV/Unhinged] epoll engine.
*
* This test was created purely for benchmark/comparison between .NET solutions.
* It should not be considered EVER as a go-to framework to build any kind of webserver!
* For such purpose please use the main Wired.IO framework [Wired.IO - https://github.com/MDA2AV/Wired.IO].
*
* This benchmarks follows the JsonSerialization and PlainText rules imposed by the TechEmpower team.
*
* The Http parsing by the Unhinged engine is still naive(work in progress), yet it's development will not have any impact
* on these benchmarks results as the extra request parsing overhead is much smaller than the read/send syscalls'.
*/

namespace Platform;

[SkipLocalsInit]
Expand All @@ -32,55 +18,34 @@ public static void Main(string[] args)
var builder = UnhingedEngine
.CreateBuilder()
.SetPort(8080)


// Number of working threads
// Reasoning behind Environment.ProcessorCount / 2
// It's the number of real cpu cores not cpu threads
// This can improve the cache hits on L1/L2 since only one thread
// is running per cpu core.
.SetNWorkersSolver(() => Environment.ProcessorCount - 2)

// Accept up to 16384 connections
.SetNWorkersSolver(() => 52)
.SetBacklog(16384)

// Max 512 epoll events per wake (quite overkill)
.SetMaxEventsPerWake(512)

// Max 1024 connection per thread
.SetMaxNumberConnectionsPerWorker(1024)

// 32KB in and 16KB out slabs to handle 16 pipeline depth
.SetSlabSizes(32 * 1024, 16 * 1024)
.InjectRequestHandler(RequestHandler);

var engine = builder.Build();
engine.Run();
}

private const string Json = "/json";
private const string PlainText = "/plaintext";

private static ValueTask RequestHandler(Connection connection)
{
// FNV-1a Hashed routes to avoid string allocations
if(connection.H1HeaderData.Route == Json) // /json
CommitJsonResponse(connection);

else if (connection.H1HeaderData.Route == PlainText) // /plaintext
CommitPlainTextResponse(connection);

var route = connection.BinaryH1HeaderData.Route.AsSpan();
if (route[1] == (byte)'j') CommitJsonResponse(connection);
else CommitPlainTextResponse(connection);
return ValueTask.CompletedTask;
}

[ThreadStatic] private static Utf8JsonWriter? t_utf8JsonWriter;
private static readonly JsonContext SerializerContext = JsonContext.Default;
private static void CommitJsonResponse(Connection connection)
private static unsafe void CommitJsonResponse(Connection connection)
{
var tail = connection.WriteBuffer.Tail;
connection.WriteBuffer.WriteUnmanaged("HTTP/1.1 200 OK\r\n"u8 +
"Server: W\r\n"u8 +
"Content-Type: application/json; charset=UTF-8\r\n"u8 +
"Content-Length: 27\r\n"u8);
"Content-Length: \r\n"u8 +
"Server: U\r\n"u8 +
"Content-Type: application/json\r\n"u8);
connection.WriteBuffer.WriteUnmanaged(DateHelper.HeaderBytes);

t_utf8JsonWriter ??= new Utf8JsonWriter(connection.WriteBuffer, new JsonWriterOptions { SkipValidation = true });
Expand All @@ -90,15 +55,27 @@ private static void CommitJsonResponse(Connection connection)
var message = new JsonMessage { Message = "Hello, World!" };
// Serializing it every request
JsonSerializer.Serialize(t_utf8JsonWriter, message, SerializerContext.JsonMessage);

var contentLength = (int)t_utf8JsonWriter.BytesCommitted;

byte* dst = connection.WriteBuffer.Ptr + tail + 33;
int tens = contentLength / 10;
int ones = contentLength - tens * 10;

dst[0] = (byte)('0' + tens);
dst[1] = (byte)('0' + ones);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a truly bizarre way to calculate the content-length of a response, and I don't think it really matches the spirit of what we're trying to get at with the "Realistic" classification. A truly "Realistic" approach to all tests other than plaintext would be not writing the headers at all and leveraging the framework to do that work.

It looks like you're writing 2 spaces in the content-length header, writing the body, then overwriting those two spaces with the "tens" and "ones" places that you get from the bytes committed. It's clever, but I am left asking the question of why this is necessary and whether the author intends end-users (e.g., developers) to perform this exercise on a realistic project.

I'm not going to accept this PR as-is. For one, this does not generalize to payloads over 100 bytes. That said, I went in search of better examples of "Realistic" implementations and found essentially everyone computing and specifying their own content-length (which, again, I think is an absurd expectation for end-users but whatever), so I'll say this: if you rearrange this to serialize the json message before constructing the headers, and use the length of that serialization in the header, then send the entire response, then I will consider it "Realistic" alongside its peers who do the same. Looking at spring as a simple example of what I mean.

Personally, I think it should be more like Gemini which simply calls the framework's json method and allows that to compute and set content length as well as other headers, but I'm biased.

Copy link
Contributor Author

@MDA2AV MDA2AV Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to indeed agree with you that it is somewhat of a fishy implementation.. but because it only supports content length up to 99. That can be fixed easily by simply leaving more spaces in the Content-Length header, or to be more fair, leave 10 zeroes like Content-Length: 0000000000\r\n, left trailing zeroes are (surprisingly) valid.

Again, I appreciate you guys actually reviewing the entries and I am not here trying to jeopardize anything, in fact I believe that every framework should work as you mentioned - no hardcoded or manually writing headers and would 100% love if that is the only acceptable format, however since it isn't I am forced to do this to be able to achieve the same or higher throughput as other frameworks that also do it.

if you rearrange this to serialize the json message before constructing the headers, and use the length of that serialization in the header, then send the entire response, then I will consider it "Realistic" alongside its peers who do the same.

So, this is a little bit unfair and let me explain why, I built both the epoll and io_uring engines from scratch to leverage low level access and manipulation on the write buffers, otherwise it would not be possible to do this as we need the exact pointer location on the write buffer even when flushing multiple responses at once in pipelined case.

Now, I also have to agree that for a user to have to do write such code it makes no sense so here is my proposal - I'll add a generalized helper to the framework that hides this mechanism from the user so that any length json is supported and user does not have to care about low level details, also this is marked as a platform framework so "(not actually a framework at all)", this is an engine other frameworks use and this entry serves as a benchmark to compare the engine performance without framework overhead.

Again, thanks for the time and effort on this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that, but if you want to get it merged in as-is you'll have to mark it "Stripped". It will still be shown in the results, but hidden by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will adjust this to let the framework calculate the content-length as other frameworks

}

private static void CommitPlainTextResponse(Connection connection)
private static ReadOnlySpan<byte> s_plainTextBody => "Hello, World!"u8;

private static unsafe void CommitPlainTextResponse(Connection connection)
{
connection.WriteBuffer.WriteUnmanaged("HTTP/1.1 200 OK\r\n"u8 +
"Server: W\r\n"u8 +
"Content-Type: text/plain\r\n"u8 +
"Content-Length: 13\r\n"u8);
"Content-Length: 13\r\n"u8 +
"Server: U\r\n"u8 +
"Content-Type: text/plain\r\n"u8);
connection.WriteBuffer.WriteUnmanaged(DateHelper.HeaderBytes);
connection.WriteBuffer.WriteUnmanaged("Hello, World!"u8);
connection.WriteBuffer.WriteUnmanaged(s_plainTextBody);
}
}
4 changes: 2 additions & 2 deletions frameworks/CSharp/wiredio/src/PlatformP/Platform.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Expand All @@ -20,6 +20,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Unhinged" Version="9.0.7" />
<PackageReference Include="Unhinged" Version="10.0.0" />
</ItemGroup>
</Project>
69 changes: 26 additions & 43 deletions frameworks/CSharp/wiredio/src/PlatformP/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,6 @@

#pragma warning disable CA2014

/* (MDA2AV)Dev notes:
*
* Wired.IO Platform benchmark using [Unhinged - https://github.com/MDA2AV/Unhinged] epoll engine.
*
* This test was created purely for benchmark/comparison between .NET solutions.
* It should not be considered EVER as a go-to framework to build any kind of webserver!
* For such purpose please use the main Wired.IO framework [Wired.IO - https://github.com/MDA2AV/Wired.IO].
*
* This benchmarks follows the JsonSerialization and PlainText rules imposed by the TechEmpower team.
*
* The Http parsing by the Unhinged engine is still naive(work in progress), yet it's development will not have any impact
* on these benchmarks results as the extra request parsing overhead is much smaller than the read/send syscalls'.
*/

namespace Platform;

[SkipLocalsInit]
Expand All @@ -32,49 +18,34 @@ public static void Main(string[] args)
var builder = UnhingedEngine
.CreateBuilder()
.SetPort(8080)

.SetNWorkersSolver(() => Environment.ProcessorCount )

// Accept up to 16384 connections
.SetNWorkersSolver(() => Environment.ProcessorCount)
.SetBacklog(16384)

// Max 512 epoll events per wake (quite overkill)
.SetMaxEventsPerWake(512)

// Max 1024 connection per thread
.SetMaxNumberConnectionsPerWorker(1024)

// 32KB in and 16KB out slabs to handle 16 pipeline depth
.SetSlabSizes(32 * 1024, 16 * 1024)
.InjectRequestHandler(RequestHandler);

var engine = builder.Build();
engine.Run();
}

private const string Json = "/json";
private const string PlainText = "/plaintext";

private static ValueTask RequestHandler(Connection connection)
{
// FNV-1a Hashed routes to avoid string allocations
if(connection.H1HeaderData.Route == Json) // /json
CommitJsonResponse(connection);

else if (connection.H1HeaderData.Route == PlainText) // /plaintext
CommitPlainTextResponse(connection);

var route = connection.BinaryH1HeaderData.Route.AsSpan();
if (route[1] == (byte)'j') CommitJsonResponse(connection);
else CommitPlainTextResponse(connection);
return ValueTask.CompletedTask;
}

[ThreadStatic] private static Utf8JsonWriter? t_utf8JsonWriter;
private static readonly JsonContext SerializerContext = JsonContext.Default;
private static void CommitJsonResponse(Connection connection)
private static unsafe void CommitJsonResponse(Connection connection)
{
var tail = connection.WriteBuffer.Tail;
connection.WriteBuffer.WriteUnmanaged("HTTP/1.1 200 OK\r\n"u8 +
"Server: W\r\n"u8 +
"Content-Type: application/json; charset=UTF-8\r\n"u8 +
"Content-Length: 27\r\n"u8);
"Content-Length: \r\n"u8 +
"Server: U\r\n"u8 +
"Content-Type: application/json; charset=UTF-8\r\n"u8);
connection.WriteBuffer.WriteUnmanaged(DateHelper.HeaderBytes);

t_utf8JsonWriter ??= new Utf8JsonWriter(connection.WriteBuffer, new JsonWriterOptions { SkipValidation = true });
Expand All @@ -84,15 +55,27 @@ private static void CommitJsonResponse(Connection connection)
var message = new JsonMessage { Message = "Hello, World!" };
// Serializing it every request
JsonSerializer.Serialize(t_utf8JsonWriter, message, SerializerContext.JsonMessage);

var contentLength = (int)t_utf8JsonWriter.BytesCommitted;

byte* dst = connection.WriteBuffer.Ptr + tail + 33;
int tens = contentLength / 10;
int ones = contentLength - tens * 10;

dst[0] = (byte)('0' + tens);
dst[1] = (byte)('0' + ones);

}

private static void CommitPlainTextResponse(Connection connection)
private static ReadOnlySpan<byte> s_plainTextBody => "Hello, World!"u8;

private static unsafe void CommitPlainTextResponse(Connection connection)
{
connection.WriteBuffer.WriteUnmanaged("HTTP/1.1 200 OK\r\n"u8 +
"Server: W\r\n"u8 +
"Content-Type: text/plain\r\n"u8 +
"Content-Length: 13\r\n"u8);
"Content-Length: 13\r\n"u8 +
"Server: U\r\n"u8 +
"Content-Type: text/plain\r\n"u8);
connection.WriteBuffer.WriteUnmanaged(DateHelper.HeaderBytes);
connection.WriteBuffer.WriteUnmanaged("Hello, World!"u8);
connection.WriteBuffer.WriteUnmanaged(s_plainTextBody);
}
}
4 changes: 2 additions & 2 deletions frameworks/CSharp/wiredio/wiredio-mcr-p.dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
RUN apk add --no-cache clang build-base zlib-dev linux-headers
WORKDIR /src
COPY src/PlatformP/ ./PlatformP/
Expand All @@ -13,7 +13,7 @@ RUN dotnet publish -c Release \
-o /app/out

# Runtime (musl)
FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-alpine
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine
ENV URLS=http://+:8080
WORKDIR /app
COPY --from=build /app/out ./
Expand Down
4 changes: 2 additions & 2 deletions frameworks/CSharp/wiredio/wiredio-mcr.dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
RUN apk add --no-cache clang build-base zlib-dev linux-headers
WORKDIR /src
COPY src/Platform/ ./Platform/
Expand All @@ -13,7 +13,7 @@ RUN dotnet publish -c Release \
-o /app/out

# Runtime (musl)
FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-alpine
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine
ENV URLS=http://+:8080
WORKDIR /app
COPY --from=build /app/out ./
Expand Down
42 changes: 42 additions & 0 deletions frameworks/CSharp/zerg/benchmark_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"framework": "zerg",
"maintainers": ["MDA2AV"],
"tests": [
{
"default": {
"plaintext_url": "/plaintext",
"json_url": "/json",
"port": 8080,
"approach": "Realistic",
"classification": "Platform",
"database": "None",
"framework": "zerg",
"language": "C#",
"orm": "None",
"platform": ".NET",
"webserver": "zerg",
"os": "Linux",
"database_os": "Linux",
"display_name": "zerg",
"notes": ""
},
"aot": {
"plaintext_url": "/plaintext",
"json_url": "/json",
"port": 8080,
"approach": "Realistic",
"classification": "Platform",
"database": "None",
"framework": "zerg",
"language": "C#",
"orm": "None",
"platform": ".NET",
"webserver": "zerg",
"os": "Linux",
"database_os": "Linux",
"display_name": "zerg",
"notes": ""
}
}
]
}
Loading
Loading