diff --git a/frameworks/CSharp/wiredio/benchmark_config.json b/frameworks/CSharp/wiredio/benchmark_config.json index a2e7a3df977..e349a2263c3 100644 --- a/frameworks/CSharp/wiredio/benchmark_config.json +++ b/frameworks/CSharp/wiredio/benchmark_config.json @@ -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#", @@ -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#", @@ -51,7 +51,7 @@ "webserver": "Unhinged", "os": "Linux", "database_os": "Linux", - "display_name": "Unhinged [p]", + "display_name": "unhinged [56] [aot] [epoll]", "notes": "epoll" } } diff --git a/frameworks/CSharp/wiredio/src/Platform/Platform.csproj b/frameworks/CSharp/wiredio/src/Platform/Platform.csproj index ea26e67a376..70b6f62d4e3 100644 --- a/frameworks/CSharp/wiredio/src/Platform/Platform.csproj +++ b/frameworks/CSharp/wiredio/src/Platform/Platform.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable true @@ -20,6 +20,6 @@ - + diff --git a/frameworks/CSharp/wiredio/src/Platform/Program.cs b/frameworks/CSharp/wiredio/src/Platform/Program.cs index cd8793de431..61de7ff476a 100644 --- a/frameworks/CSharp/wiredio/src/Platform/Program.cs +++ b/frameworks/CSharp/wiredio/src/Platform/Program.cs @@ -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] @@ -32,25 +18,10 @@ 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); @@ -58,29 +29,23 @@ public static void Main(string[] args) 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 }); @@ -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); + } - private static void CommitPlainTextResponse(Connection connection) + private static ReadOnlySpan 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); } } \ No newline at end of file diff --git a/frameworks/CSharp/wiredio/src/PlatformP/Platform.csproj b/frameworks/CSharp/wiredio/src/PlatformP/Platform.csproj index ea26e67a376..70b6f62d4e3 100644 --- a/frameworks/CSharp/wiredio/src/PlatformP/Platform.csproj +++ b/frameworks/CSharp/wiredio/src/PlatformP/Platform.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable true @@ -20,6 +20,6 @@ - + diff --git a/frameworks/CSharp/wiredio/src/PlatformP/Program.cs b/frameworks/CSharp/wiredio/src/PlatformP/Program.cs index 46a7d69219c..1f31655c13e 100644 --- a/frameworks/CSharp/wiredio/src/PlatformP/Program.cs +++ b/frameworks/CSharp/wiredio/src/PlatformP/Program.cs @@ -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] @@ -32,19 +18,10 @@ 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); @@ -52,29 +29,23 @@ public static void Main(string[] args) 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 }); @@ -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 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); } } \ No newline at end of file diff --git a/frameworks/CSharp/wiredio/wiredio-mcr-p.dockerfile b/frameworks/CSharp/wiredio/wiredio-mcr-p.dockerfile index 1ef18f083c2..e7451c84131 100644 --- a/frameworks/CSharp/wiredio/wiredio-mcr-p.dockerfile +++ b/frameworks/CSharp/wiredio/wiredio-mcr-p.dockerfile @@ -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/ @@ -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 ./ diff --git a/frameworks/CSharp/wiredio/wiredio-mcr.dockerfile b/frameworks/CSharp/wiredio/wiredio-mcr.dockerfile index 9bd497eec84..d564c8d5de0 100644 --- a/frameworks/CSharp/wiredio/wiredio-mcr.dockerfile +++ b/frameworks/CSharp/wiredio/wiredio-mcr.dockerfile @@ -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/ @@ -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 ./ diff --git a/frameworks/CSharp/zerg/benchmark_config.json b/frameworks/CSharp/zerg/benchmark_config.json new file mode 100644 index 00000000000..96fa5fd15f9 --- /dev/null +++ b/frameworks/CSharp/zerg/benchmark_config.json @@ -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": "" + } + } + ] +} diff --git a/frameworks/CSharp/zerg/config.toml b/frameworks/CSharp/zerg/config.toml new file mode 100644 index 00000000000..36072ebab42 --- /dev/null +++ b/frameworks/CSharp/zerg/config.toml @@ -0,0 +1,26 @@ +[framework] +name = "zerg" + +[main] +urls.plaintext = "/plaintext" +urls.json = "/json" +approach = "Realistic" +classification = "Platform" +os = "Linux" +database_os = "Linux" +orm = "None" +platform = ".NET" +webserver = "zerg" +versus = "None" + +[aot] +urls.plaintext = "/plaintext" +urls.json = "/json" +approach = "Realistic" +classification = "Platform" +os = "Linux" +database_os = "Linux" +orm = "None" +platform = ".NET" +webserver = "zerg" +versus = "None" diff --git a/frameworks/CSharp/zerg/silverlight.sln b/frameworks/CSharp/zerg/silverlight.sln new file mode 100644 index 00000000000..18cf9f01e49 --- /dev/null +++ b/frameworks/CSharp/zerg/silverlight.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "silverlight", "silverlight\silverlight.csproj", "{A3682EC2-387E-4389-8F76-F4A6C8BAB894}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A3682EC2-387E-4389-8F76-F4A6C8BAB894}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3682EC2-387E-4389-8F76-F4A6C8BAB894}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3682EC2-387E-4389-8F76-F4A6C8BAB894}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3682EC2-387E-4389-8F76-F4A6C8BAB894}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/frameworks/CSharp/zerg/silverlight/ConnectionHandler.cs b/frameworks/CSharp/zerg/silverlight/ConnectionHandler.cs new file mode 100644 index 00000000000..a289f67f483 --- /dev/null +++ b/frameworks/CSharp/zerg/silverlight/ConnectionHandler.cs @@ -0,0 +1,264 @@ +using System.Buffers; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using zerg; +using zerg.Utils; +using zerg.Utils.UnmanagedMemoryManager; + +namespace silverlight; + +internal sealed class ConnectionHandler +{ + private readonly unsafe byte* _inflightData; + private int _inflightTail; + private readonly int _length; + + [ThreadStatic] + private static Utf8JsonWriter? t_writer; + private static readonly JsonContext SerializerContext = JsonContext.Default; + + private const string _jsonBody = "Hello, World!"; + private static ReadOnlySpan s_plainTextBody => "Hello, World!"u8; + + private static ReadOnlySpan s_headersJson => "HTTP/1.1 200 OK\r\nContent-Length: \r\nServer: S\r\nContent-Type: application/json\r\n"u8; + private static ReadOnlySpan s_headersPlainText => "HTTP/1.1 200 OK\r\nContent-Length: 13\r\nServer: S\r\nContent-Type: text/plain\r\n"u8; + + public unsafe ConnectionHandler(int length = 1024 * 16) + { + _length = length; + + // Allocating an unmanaged byte slab to store inflight data + _inflightData = (byte*)NativeMemory.AlignedAlloc((nuint)_length, 64); + + _inflightTail = 0; + } + + // Zero allocation read and write example + // No Peeking + internal async Task HandleConnectionAsync(Connection connection) + { + try + { + while (true) // Outer loop, iterates everytime we read more data from the wire + { + var result = await connection.ReadAsync(); // Read data from the wire + if (result.IsClosed) + break; + + if (HandleResult(connection, ref result)) + { + await connection.FlushAsync(); // Mark data to be ready to be flushed + } + + // Reset connection's ManualResetValueTaskSourceCore + connection.ResetRead(); + } + } + catch (Exception e) + { + Console.WriteLine($"Exception --: {e}"); + } + finally + { + unsafe { NativeMemory.AlignedFree(_inflightData); } + } + } + + private unsafe bool HandleResult(Connection connection, ref ReadResult result) + { + bool flushable; + int advanced; + + UnmanagedMemoryManager[] rings = connection.GetAllSnapshotRingsAsUnmanagedMemory(result); + int ringCount = rings.Length; + + if (ringCount == 0) + return false; + + int oldInflightTail = _inflightTail; + + if (_inflightTail == 0) + { + flushable = ProcessRings(connection, rings, out advanced); + } + else + { + // Cold path + UnmanagedMemoryManager[] mems = new UnmanagedMemoryManager[ringCount + 1]; + + mems[0] = new(_inflightData, _inflightTail); + + for (int i = 1; i < ringCount + 1; i++) + mems[i] = rings[i - 1]; + + flushable = ProcessRings(connection, mems, out advanced); + + if (flushable) // a request was handled so inflight data can be discarded + _inflightTail = 0; + } + + if (!flushable) + { + // No complete request found. Copy ring data to the inflight buffer + // and return rings to the kernel. + for (int i = 0; i < rings.Length; i++) + { + Buffer.MemoryCopy( + rings[i].Ptr, + _inflightData + _inflightTail, + _length - _inflightTail, + rings[i].Length); + _inflightTail += rings[i].Length; + } + + for (int i = 0; i < rings.Length; i++) + connection.ReturnRing(rings[i].BufferId); + + return false; + } + + // When inflight data was prepended, advanced includes those bytes. + // Subtract them so advanced is relative to rings only. + int ringAdvanced = advanced - oldInflightTail; + int ringsTotalLength = CalculateRingsTotalLength(rings); + + if (ringAdvanced < ringsTotalLength) + { + var currentRingIndex = GetCurrentRingIndex(in ringAdvanced, rings, out var currentRingAdvanced); + + // Copy current ring unused data + Buffer.MemoryCopy( + rings[currentRingIndex].Ptr + currentRingAdvanced, // source + _inflightData + _inflightTail, // destination + _length - _inflightTail, // destinationSizeInBytes + rings[currentRingIndex].Length - currentRingAdvanced); // sourceBytesToCopy + + _inflightTail += rings[currentRingIndex].Length - currentRingAdvanced; + + // Copy untouched rings data + for (int i = currentRingIndex + 1; i < rings.Length; i++) + { + Buffer.MemoryCopy( + rings[i].Ptr, // source + _inflightData + _inflightTail, // destination + _length - _inflightTail, // destinationSizeInBytes + rings[i].Length); // sourceBytesToCopy + + _inflightTail += rings[i].Length; + } + } + + // Return the rings to the kernel, at this stage the request was either handled or the rings' data + // has already been copied to the inflight buffer. + for (int i = 0; i < rings.Length; i++) + connection.ReturnRing(rings[i].BufferId); + + return flushable; + } + + [SkipLocalsInit][Pure][MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe bool ProcessRings(Connection connection, UnmanagedMemoryManager[] rings, out int advanced) + { + advanced = 0; + + int idx; + bool flushable = false; + + ReadOnlySpan data = rings.Length == 1 + ? new ReadOnlySpan(rings[0].Ptr, rings[0].Length) + : rings.ToReadOnlySequence().ToArray(); + + while (true) + { + idx = data.IndexOf("\r\n\r\n"u8); + if (idx == -1) return flushable; + + int idx4 = idx + 4; + advanced += idx4; + int space1 = data.IndexOf((byte)' '); + if (space1 == -1) return flushable; + int space2 = data[(space1 + 1)..].IndexOf((byte)' '); + if (space2 <= 0) return flushable; + + ReadOnlySpan route = data[(space1 + 1)..(space1 + 1 + space2)]; + + WriteResponse(connection, route[1] == (byte)'j'); + flushable = true; + if (idx4 >= data.Length) break; + + data = data[idx4..]; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteResponse(Connection connection, bool json) + { + var tail = connection.WriteTail; + int contentLength; + + if (json) + { + connection.Write(s_headersJson); + connection.Write(DateHelper.HeaderBytes); + + var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(connection, new JsonWriterOptions { SkipValidation = true }); + utf8JsonWriter.Reset(connection); + JsonSerializer.Serialize(utf8JsonWriter, new JsonMessage { Message = _jsonBody }, SerializerContext.JsonMessage); + + contentLength = (int)utf8JsonWriter.BytesCommitted; + + unsafe + { + byte* dst = connection.WriteBuffer + tail + 33; + int tens = contentLength / 10; + int ones = contentLength - tens * 10; + + dst[0] = (byte)('0' + tens); + dst[1] = (byte)('0' + ones); + } + } + else + { + connection.Write(s_headersPlainText); + connection.Write(DateHelper.HeaderBytes); + connection.Write(s_plainTextBody); + } + } + + private static int GetCurrentRingIndex(in int totalAdvanced, UnmanagedMemoryManager[] rings, out int currentRingAdvanced) + { + var total = 0; + + for (int i = 0; i < rings.Length; i++) + { + if (rings[i].Length + total >= totalAdvanced) + { + currentRingAdvanced = totalAdvanced - total; + return i; + } + + total += rings[i].Length; + } + + currentRingAdvanced = -1; + return -1; + } + + private static int CalculateRingsTotalLength(UnmanagedMemoryManager[] rings) + { + var total = 0; + for (int i = 0; i < rings.Length; i++) total += rings[i].Length; + return total; + } +} + +public struct JsonMessage { public string Message { get; set; } } + +[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization | JsonSourceGenerationMode.Metadata)] +[JsonSerializable(typeof(JsonMessage))] +public partial class JsonContext : JsonSerializerContext { } \ No newline at end of file diff --git a/frameworks/CSharp/zerg/silverlight/DateHelper.cs b/frameworks/CSharp/zerg/silverlight/DateHelper.cs new file mode 100644 index 00000000000..b2e8a797db6 --- /dev/null +++ b/frameworks/CSharp/zerg/silverlight/DateHelper.cs @@ -0,0 +1,55 @@ +using System.Buffers.Text; + +namespace silverlight; + +/// +/// Manages the generation of the date header value. +/// +public static class DateHelper +{ + private const int PrefixLength = 6; // "Date: ".Length + private const int DateTimeRLength = 29; // Wed, 14 Mar 2018 14:20:00 GMT + private const int SuffixLength = 2; // crlf + private const int SuffixIndex = DateTimeRLength + PrefixLength; + + private static readonly Timer STimer = new((s) => { + SetDateValues(DateTimeOffset.UtcNow); + }, null, 1000, 1000); + + private static byte[] _sHeaderBytesMaster = new byte[PrefixLength + DateTimeRLength + 2 * SuffixLength]; + private static byte[] _sHeaderBytesScratch = new byte[PrefixLength + DateTimeRLength + 2 * SuffixLength]; + + static DateHelper() + { + var utf8 = "Date: "u8; + + utf8.CopyTo(_sHeaderBytesMaster); + utf8.CopyTo(_sHeaderBytesScratch); + _sHeaderBytesMaster[SuffixIndex] = (byte)'\r'; + _sHeaderBytesMaster[SuffixIndex + 1] = (byte)'\n'; + _sHeaderBytesMaster[SuffixIndex + 2] = (byte)'\r'; + _sHeaderBytesMaster[SuffixIndex + 3] = (byte)'\n'; + _sHeaderBytesScratch[SuffixIndex] = (byte)'\r'; + _sHeaderBytesScratch[SuffixIndex + 1] = (byte)'\n'; + _sHeaderBytesScratch[SuffixIndex + 2] = (byte)'\r'; + _sHeaderBytesScratch[SuffixIndex + 3] = (byte)'\n'; + + SetDateValues(DateTimeOffset.UtcNow); + SyncDateTimer(); + } + + private static void SyncDateTimer() => STimer.Change(1000, 1000); + public static ReadOnlySpan HeaderBytes => _sHeaderBytesMaster; + + private static void SetDateValues(DateTimeOffset value) + { + lock (_sHeaderBytesScratch) + { + if (!Utf8Formatter.TryFormat(value, _sHeaderBytesScratch.AsSpan(PrefixLength), out var written, 'R')) + throw new Exception("date time format failed"); + + //Debug.Assert(written == dateTimeRLength); + (_sHeaderBytesScratch, _sHeaderBytesMaster) = (_sHeaderBytesMaster, _sHeaderBytesScratch); + } + } +} \ No newline at end of file diff --git a/frameworks/CSharp/zerg/silverlight/Program.cs b/frameworks/CSharp/zerg/silverlight/Program.cs new file mode 100644 index 00000000000..070ca46fd36 --- /dev/null +++ b/frameworks/CSharp/zerg/silverlight/Program.cs @@ -0,0 +1,41 @@ +using silverlight; +using zerg.Engine; +using zerg.Engine.Configs; + +// dotnet publish -f net10.0 -c Release /p:PublishAot=true /p:OptimizationPreference=Speed + +var reactorConfigs = new ReactorConfig[Environment.ProcessorCount]; +var reactorConfig = new ReactorConfig +{ + RingFlags = 0x3000, // IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN + RingEntries = 1024, + RecvBufferSize = 1024 * 4, + BufferRingEntries = 1024 * 16, + BatchCqes = 1024 * 4, + MaxConnectionsPerReactor = 1024 +}; +Array.Fill(reactorConfigs, reactorConfig); + +var acceptorConfig = new AcceptorConfig +{ + RingFlags = 0, + RingEntries = 256, + BatchSqes = 1024 * 4, +}; + +var engine = new Engine(new EngineOptions +{ + Ip = "0.0.0.0", + Port = 8080, + ReactorCount = Environment.ProcessorCount, + AcceptorConfig = acceptorConfig, + ReactorConfigs = reactorConfigs, +}); +engine.Listen(); + +while (engine.ServerRunning) +{ + var connection = await engine.AcceptAsync(); + if (connection is null) continue; + _ = new ConnectionHandler().HandleConnectionAsync(connection); +} \ No newline at end of file diff --git a/frameworks/CSharp/zerg/silverlight/silverlight.csproj b/frameworks/CSharp/zerg/silverlight/silverlight.csproj new file mode 100644 index 00000000000..d633c967ccc --- /dev/null +++ b/frameworks/CSharp/zerg/silverlight/silverlight.csproj @@ -0,0 +1,23 @@ + + + + Exe + net10.0 + enable + enable + true + + true + true + true + + + + + + + + + + + diff --git a/frameworks/CSharp/zerg/silverlight/uringshim.c b/frameworks/CSharp/zerg/silverlight/uringshim.c new file mode 100644 index 00000000000..45f76484795 --- /dev/null +++ b/frameworks/CSharp/zerg/silverlight/uringshim.c @@ -0,0 +1,481 @@ +// uringshim.c +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include // syscall() +#include // __NR_io_uring_enter +#include + +// Build: +// gcc -O2 -fPIC -shared -o liburingshim.so uringshim.c -luring +// +// Purpose +// ------- +// This is a thin C shim around liburing that exposes a stable C ABI for P/Invoke. +// Keep it boring: return negative errno-style codes, avoid callbacks, avoid C++. +// +// Conventions +// ----------- +// - Functions returning int: 0 / positive on success, or -errno on failure. +// - Pointers returned: NULL on failure, with *err_out / *ret_out filled if provided. +// - Managed side should treat all negative values as errors and map them to exceptions. +// +// Kernel/liburing assumptions +// --------------------------- +// This assumes a recent kernel + liburing for: +// - io_uring_prep_multishot_accept() +// - io_uring_prep_recv_multishot() + IOSQE_BUFFER_SELECT +// - io_uring_setup_buf_ring() / io_uring_free_buf_ring() +// +// Notes on io_uring_enter timeout ABI +// ---------------------------------- +// There are two relevant ABIs: +// 1) "Simple" ABI: arg = (sigset_t*) or NULL, argsz = sigset_t size or 0. +// 2) "Extended" ABI (IORING_ENTER_EXT_ARG): arg points to io_uring_getevents_arg, +// which can include a pointer to __kernel_timespec. +// +// liburing wraps these via io_uring_enter() / io_uring_enter2(). +// We provide shim_enter() that directly uses the syscall with EXT_ARG when ts != NULL, +// so managed code can do "one enter per loop" without liburing helpers. + +// ----------------------------------------------------------------------------- +// Ring lifecycle +// ----------------------------------------------------------------------------- + +/** + * Returns the ring's setup flags (IORING_SETUP_* bits) as stored in struct io_uring. + * Useful to confirm that SQPOLL / SQ_AFF actually got enabled by the kernel. + */ +unsigned shim_get_ring_flags(struct io_uring* ring) +{ + if (!ring) return 0; + return ring->flags; +} + +/** + * Create ring with io_uring_queue_init_params. + * + * entries : ring size + * flags : IORING_SETUP_* flags (e.g. IORING_SETUP_SQPOLL | IORING_SETUP_SQ_AFF) + * sq_thread_cpu : cpu to pin SQPOLL thread to (only used if IORING_SETUP_SQ_AFF) + * pass -1 to let kernel choose. + * sq_thread_idle_ms : SQPOLL idle in milliseconds (only used if IORING_SETUP_SQPOLL) + * + * Returns: heap-allocated struct io_uring* or NULL on error. + * err_out: set to 0 on success or -errno / -ENOMEM on failure. + */ +struct io_uring* shim_create_ring_ex(unsigned entries, + unsigned flags, + int sq_thread_cpu, + unsigned sq_thread_idle_ms, + int* err_out) +{ + struct io_uring* ring = (struct io_uring*)malloc(sizeof(struct io_uring)); + if (!ring) + { + if (err_out) *err_out = -ENOMEM; + return NULL; + } + + memset(ring, 0, sizeof(*ring)); + + struct io_uring_params p; + memset(&p, 0, sizeof(p)); + + p.flags = flags; + + // SQPOLL tuning only matters if SQPOLL is requested. + if (flags & IORING_SETUP_SQPOLL) + { + p.sq_thread_idle = sq_thread_idle_ms; + + // Only set sq_thread_cpu if SQ_AFF is requested and caller passed a valid cpu. + if ((flags & IORING_SETUP_SQ_AFF) && sq_thread_cpu >= 0) + { + p.sq_thread_cpu = (unsigned)sq_thread_cpu; + } + } + + fprintf(stderr, "shim_create_ring_ex: flags=0x%x sq_cpu=%d idle=%u\n", + flags, sq_thread_cpu, sq_thread_idle_ms); + fprintf(stderr, "params: p.flags=0x%x p.wq_fd=%u\n", p.flags, p.wq_fd); + + int rc = io_uring_queue_init_params(entries, ring, &p); + if (rc < 0) + { + free(ring); + if (err_out) *err_out = rc; + return NULL; + } + + if (err_out) *err_out = 0; + return ring; +} + +/** + * Create ring with default parameters (single issuer; no SQPOLL). + */ +struct io_uring* shim_create_ring(unsigned entries, int* err_out) +{ + struct io_uring* ring = (struct io_uring*)malloc(sizeof(struct io_uring)); + if (!ring) + { + if (err_out) *err_out = -ENOMEM; + return NULL; + } + + memset(ring, 0, sizeof(*ring)); + + int rc = io_uring_queue_init(entries, ring, 0); + if (rc < 0) + { + free(ring); + if (err_out) *err_out = rc; + return NULL; + } + + if (err_out) *err_out = 0; + return ring; +} + +/** + * Tear down ring and free heap memory (safe with NULL). + */ +void shim_destroy_ring(struct io_uring* ring) +{ + if (!ring) return; + io_uring_queue_exit(ring); + free(ring); +} + +// ----------------------------------------------------------------------------- +// SQ / CQ core operations +// ----------------------------------------------------------------------------- + +/** Get a free SQE or NULL if SQ is full. */ +struct io_uring_sqe* shim_get_sqe(struct io_uring* ring) +{ + return io_uring_get_sqe(ring); +} + +/** Number of SQEs currently queued (not yet submitted). */ +unsigned shim_sq_ready(struct io_uring* ring) +{ + return io_uring_sq_ready(ring); +} + +/** Submit pending SQEs. Returns number submitted or -errno. */ +int shim_submit(struct io_uring* ring) +{ + return io_uring_submit(ring); +} + +/** Number of CQEs available to consume right now (userspace-only check). */ +unsigned shim_cq_ready(struct io_uring* ring) +{ + return io_uring_cq_ready(ring); +} + +/** Block until at least 1 CQE is available. */ +int shim_wait_cqe(struct io_uring* ring, struct io_uring_cqe** cqe) +{ + return io_uring_wait_cqe(ring, cqe); +} + +/** + * Wait for at least wait_nr CQEs or timeout (or earlier if kernel decides). + * Returns 0 on success, -errno on error/timeout. + * + * NOTE: ts is a relative timeout here, kernel-style. + */ +int shim_wait_cqes(struct io_uring *ring, + struct io_uring_cqe **cqe_ptr, + unsigned wait_nr, + struct __kernel_timespec *ts) +{ + // sigmask=NULL => no signal mask changes + return io_uring_wait_cqes(ring, cqe_ptr, wait_nr, ts, NULL); +} + +/** Wait for 1 CQE or timeout. Returns 0 or -errno (commonly -ETIME). */ +int shim_wait_cqe_timeout(struct io_uring *ring, + struct io_uring_cqe **cqe_ptr, + struct __kernel_timespec *ts) +{ + return io_uring_wait_cqe_timeout(ring, cqe_ptr, ts); +} + +/** + * Convenience: timeout in milliseconds. + * Returns 0 on success, -ETIME on timeout, or -errno on error. + */ +int shim_wait_cqe_timeout_in(struct io_uring* ring, + struct io_uring_cqe** cqe, + long timeout_ms) +{ + struct __kernel_timespec ts; + ts.tv_sec = timeout_ms / 1000; + ts.tv_nsec = (timeout_ms % 1000) * 1000000LL; // ms -> ns + + return io_uring_wait_cqe_timeout(ring, cqe, &ts); +} + +/** + * Non-blocking: peek up to 'count' CQEs. + * Returns number of CQEs written to cqes (0..count). + */ +int shim_peek_batch_cqe(struct io_uring* ring, struct io_uring_cqe** cqes, unsigned count) +{ + return io_uring_peek_batch_cqe(ring, cqes, count); +} + +/** Mark one CQE consumed. */ +void shim_cqe_seen(struct io_uring* ring, struct io_uring_cqe* cqe) +{ + io_uring_cqe_seen(ring, cqe); +} + +/** + * Advance CQ head by 'count' CQEs, for batched consumption + * after io_uring_peek_batch_cqe(). + */ +void shim_cq_advance(struct io_uring* ring, unsigned count) +{ + io_uring_cq_advance(ring, count); +} + +/** + * Combined submit + wait (single enter). No timeout. + * Returns number submitted or -errno. + */ +int shim_submit_and_wait(struct io_uring* ring, unsigned wait_nr) +{ + return io_uring_submit_and_wait(ring, wait_nr); +} + +/** + * Combined submit + wait with timeout, returning: + * - >=0 / 0 on success (see liburing docs for exact behavior) + * - -errno on error/timeout. + * + * This is the *liburing* helper, not raw syscall. + */ +int shim_submit_and_wait_timeout(struct io_uring *ring, + struct io_uring_cqe **cqes, + unsigned int wait_nr, + struct __kernel_timespec *ts) +{ + return io_uring_submit_and_wait_timeout(ring, cqes, wait_nr, ts, NULL); +} + +// ----------------------------------------------------------------------------- +// User-data helpers (64-bit opaque tag propagated SQE -> CQE) +// ----------------------------------------------------------------------------- + +/** Store a 64-bit user-data value in SQE. */ +void shim_sqe_set_data64(struct io_uring_sqe* sqe, unsigned long long data) +{ + io_uring_sqe_set_data64(sqe, data); +} + +/** Load 64-bit user-data from CQE. */ +unsigned long long shim_cqe_get_data64(const struct io_uring_cqe* cqe) +{ + return io_uring_cqe_get_data64(cqe); +} + +// ----------------------------------------------------------------------------- +// Multishot operations +// ----------------------------------------------------------------------------- + +/** + * Prepare multishot accept. Each CQE yields one accepted fd until the kernel stops. + * flags: typically SOCK_NONBLOCK | SOCK_CLOEXEC + */ +void shim_prep_multishot_accept(struct io_uring_sqe* sqe, int lfd, int flags) +{ + io_uring_prep_multishot_accept(sqe, lfd, NULL, NULL, flags); +} + +/** + * Prepare multishot recv that selects buffers from a registered buf-ring group. + * buf_group must match the bgid used in setup_buf_ring. + * + * flags is passed to recv(2). Commonly 0; you may use MSG_WAITALL, etc. if you know why. + */ +void shim_prep_recv_multishot_select(struct io_uring_sqe* sqe, int fd, unsigned buf_group, int flags) +{ + io_uring_prep_recv_multishot(sqe, fd, NULL, 0, flags); + + // BUFFER_SELECT tells kernel to pick a buffer from the ring for each recv completion. + sqe->flags |= IOSQE_BUFFER_SELECT; + sqe->buf_group = (uint16_t)buf_group; +} + +// ----------------------------------------------------------------------------- +// Buf-ring helpers +// ----------------------------------------------------------------------------- + +/** + * Create/register a buf-ring with 'entries' under buffer-group 'bgid'. + * + * On success: + * - returns buf_ring pointer + * - sets *ret_out = 0 + * + * On failure: + * - returns NULL + * - sets *ret_out = -errno + * + * After creation, populate using io_uring_buf_ring_add() and then + * io_uring_buf_ring_advance() to publish. + */ +struct io_uring_buf_ring* shim_setup_buf_ring(struct io_uring* ring, + unsigned entries, + unsigned bgid, + unsigned flags, + int* ret_out) +{ + return io_uring_setup_buf_ring(ring, entries, (int)bgid, flags, ret_out); +} + +/** + * Free/unregister a buf-ring. Caller must ensure no in-flight ops still reference it. + */ +void shim_free_buf_ring(struct io_uring* ring, + struct io_uring_buf_ring* br, + unsigned entries, + unsigned bgid) +{ + io_uring_free_buf_ring(ring, br, entries, (int)bgid); +} + +/** + * Stage a buffer into the producer view of the buf-ring. + * Not visible to kernel until shim_buf_ring_advance() is called. + * + * mask: usually entries - 1 (entries should be power-of-two). + * idx : monotonically increasing producer index (you handle wrap via mask). + */ +void shim_buf_ring_add(struct io_uring_buf_ring* br, + void* addr, + unsigned len, + unsigned short bid, + unsigned short mask, + unsigned idx) +{ + io_uring_buf_ring_add(br, addr, len, bid, mask, idx); +} + +/** Publish 'count' staged buffers to the kernel. */ +void shim_buf_ring_advance(struct io_uring_buf_ring* br, unsigned count) +{ + io_uring_buf_ring_advance(br, count); +} + +// ----------------------------------------------------------------------------- +// CQE helpers for buffer selection +// ----------------------------------------------------------------------------- + +/** Non-zero if CQE refers to a selected buffer (IORING_CQE_F_BUFFER). */ +int shim_cqe_has_buffer(const struct io_uring_cqe* cqe) +{ + return (cqe->flags & IORING_CQE_F_BUFFER) != 0; +} + +/** + * Extract buffer id (bid) used by kernel. + * Layout: flags contains bid in upper bits, shifted by IORING_CQE_BUFFER_SHIFT. + */ +unsigned shim_cqe_buffer_id(const struct io_uring_cqe* cqe) +{ + return cqe->flags >> IORING_CQE_BUFFER_SHIFT; +} + +// ----------------------------------------------------------------------------- +// Send / cancel +// ----------------------------------------------------------------------------- + +/** Prepare send(2). */ +void shim_prep_send(struct io_uring_sqe* sqe, + int fd, + const void* buf, + unsigned nbytes, + int flags) +{ + io_uring_prep_send(sqe, fd, buf, nbytes, flags); +} + +/** + * Prepare cancel by user-data (64-bit). + * Useful to cancel in-flight multishot recv when closing a connection. + */ +void shim_prep_cancel64(struct io_uring_sqe* sqe, + unsigned long long user_data, + int flags) +{ + io_uring_prep_cancel64(sqe, user_data, flags); +} + +// ----------------------------------------------------------------------------- +// Direct io_uring_enter wrapper (single syscall path with optional timeout) +// ----------------------------------------------------------------------------- + +/** + * shim_enter + * --------- + * Direct wrapper for the io_uring_enter syscall, supporting both: + * - Simple ABI when ts == NULL (arg=NULL, argsz=0) + * - Extended ABI when ts != NULL (IORING_ENTER_EXT_ARG + io_uring_getevents_arg) + * + * Why you want this: + * - You can do "submit + wait" in one syscall per loop, with an optional timeout. + * - Avoid liburing paths that might force extra syscalls or additional checks. + * + * Parameters mirror io_uring_enter: + * - to_submit : number of SQEs to submit (usually io_uring_sq_ready()) + * - min_complete : minimum CQEs to wait for (0 => don't wait) + * - flags : IORING_ENTER_* flags (GETEVENTS, SQ_WAKEUP, etc.) + * - ts : optional timeout (relative). If NULL, no timeout is applied. + * + * Returns: >=0 on success (kernel return), or -errno on failure. + */ +int shim_enter(struct io_uring* ring, + unsigned to_submit, + unsigned min_complete, + unsigned flags, + struct __kernel_timespec* ts) +{ + if (!ring) return -EINVAL; + + // Simple ABI: no timeout, arg=NULL and argsz=0. + if (ts == NULL) + { + return (int)syscall(__NR_io_uring_enter, + ring->ring_fd, + to_submit, + min_complete, + flags, + NULL, + 0); + } + + // Extended ABI: pass io_uring_getevents_arg with pointer to timespec. + // IMPORTANT: This requires IORING_ENTER_EXT_ARG in flags. + struct io_uring_getevents_arg arg; + memset(&arg, 0, sizeof(arg)); + arg.ts = (uint64_t)(uintptr_t)ts; + + return (int)syscall(__NR_io_uring_enter, + ring->ring_fd, + to_submit, + min_complete, + flags | IORING_ENTER_EXT_ARG, + &arg, + sizeof(arg)); +} + diff --git a/frameworks/CSharp/zerg/zerg-aot.dockerfile b/frameworks/CSharp/zerg/zerg-aot.dockerfile new file mode 100644 index 00000000000..80ec569904c --- /dev/null +++ b/frameworks/CSharp/zerg/zerg-aot.dockerfile @@ -0,0 +1,31 @@ +# Build +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build +RUN apk add --no-cache clang build-base zlib-dev linux-headers liburing-dev +WORKDIR /src +COPY silverlight/ ./silverlight/ + +# Build native shim +WORKDIR /src/silverlight +RUN clang -O2 -fPIC -shared uringshim.c -o liburingshim.so -luring + +# Publish AOT app +RUN dotnet publish -c Release \ + -r linux-musl-x64 \ + --self-contained true \ + -p:PublishAot=true \ + -p:OptimizationPreference=Speed \ + -p:GarbageCollectionAdaptationMode=0 \ + -o /app/out +RUN cp /src/silverlight/liburingshim.so /app/out/runtimes/linux-musl-x64/native/liburingshim.so 2>/dev/null; \ + cp /src/silverlight/liburingshim.so /app/out/liburingshim.so + +# Runtime (musl) +FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine +RUN apk add --no-cache liburing +ENV URLS=http://+:8080 \ + LD_LIBRARY_PATH=/app +WORKDIR /app +COPY --from=build /app/out ./ +RUN chmod +x ./silverlight +EXPOSE 8080 +ENTRYPOINT ["./silverlight"] diff --git a/frameworks/CSharp/zerg/zerg.dockerfile b/frameworks/CSharp/zerg/zerg.dockerfile new file mode 100644 index 00000000000..83dc6f12a52 --- /dev/null +++ b/frameworks/CSharp/zerg/zerg.dockerfile @@ -0,0 +1,23 @@ +# Build +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build +RUN apk add --no-cache clang build-base linux-headers liburing-dev +WORKDIR /src +COPY silverlight/ ./silverlight/ + +# Build native shim +WORKDIR /src/silverlight +RUN clang -O2 -fPIC -shared uringshim.c -o liburingshim.so -luring + +# Publish app +RUN dotnet publish -c Release -o /app/out +RUN cp /src/silverlight/liburingshim.so /app/out/runtimes/linux-musl-x64/native/liburingshim.so + +# Runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine +RUN apk add --no-cache liburing +ENV URLS=http://+:8080 \ + LD_LIBRARY_PATH=/app +WORKDIR /app +COPY --from=build /app/out ./ +EXPOSE 8080 +ENTRYPOINT ["dotnet", "silverlight.dll"]