diff --git a/frameworks/ioxide-0-4-169/.dockerignore b/frameworks/ioxide-0-4-169/.dockerignore
new file mode 100644
index 000000000..cd42ee34e
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/.dockerignore
@@ -0,0 +1,2 @@
+bin/
+obj/
diff --git a/frameworks/ioxide-0-4-169/Cache.cs b/frameworks/ioxide-0-4-169/Cache.cs
new file mode 100644
index 000000000..4a649246b
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/Cache.cs
@@ -0,0 +1,59 @@
+using ioxide;
+using ioxide.redis;
+using StackExchange.Redis;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace IoxideArena;
+
+/// The crud cache, so the backend can be swapped for benchmarking (CRUD_CACHE env var).
+internal interface ICrudCache
+{
+ ValueTask GetAsync(string key);
+ ValueTask SetExAsync(string key, string value, int seconds);
+ ValueTask DelAsync(string key);
+}
+
+/// ioxide.redis: per-reactor pooled connections, pipelined, on the ring (inline resume).
+internal sealed class IoxideRedisCache(RedisPool pool) : ICrudCache
+{
+ public ValueTask GetAsync(string key) => pool.GetAsync(key);
+ public ValueTask SetExAsync(string key, string value, int seconds) => pool.SetExAsync(key, value, seconds);
+ public async ValueTask DelAsync(string key) => await pool.DelAsync(key);
+}
+
+///
+/// In-process IMemoryCache, shared across reactors. Synchronous - GET/SET/REMOVE run inline on
+/// the reactor (no network, no thread pool), so a cache-hit never leaves the ring. The cache is
+/// shared so a PUT on any reactor is seen by every reactor's next read.
+///
+internal sealed class InProcCache(IMemoryCache cache) : ICrudCache
+{
+ // Single-item entries expire 200 ms after write, matching genhttp-11's IMemoryCache so the
+ // crud comparison is apples-to-apples. (The seconds arg is honored only by the Redis backends.)
+ private static readonly MemoryCacheEntryOptions Options =
+ new() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(200) };
+
+ public ValueTask GetAsync(string key)
+ => new(cache.TryGetValue(key, out string? value) ? value : null);
+
+ public ValueTask SetExAsync(string key, string value, int seconds)
+ {
+ cache.Set(key, value, Options);
+ return ValueTask.CompletedTask;
+ }
+
+ public ValueTask DelAsync(string key)
+ {
+ cache.Remove(key);
+ return ValueTask.CompletedTask;
+ }
+}
+
+/// StackExchange.Redis: one shared multiplexer, off-ring (thread-pool completions).
+internal sealed class StackExchangeCache(IDatabase db) : ICrudCache
+{
+ public async ValueTask GetAsync(string key) => await db.StringGetAsync(key);
+ public ValueTask SetExAsync(string key, string value, int seconds)
+ => new(db.StringSetAsync(key, value, TimeSpan.FromSeconds(seconds)));
+ public ValueTask DelAsync(string key) => new(db.KeyDeleteAsync(key));
+}
diff --git a/frameworks/ioxide-0-4-169/Crud.cs b/frameworks/ioxide-0-4-169/Crud.cs
new file mode 100644
index 000000000..4a7a619cd
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/Crud.cs
@@ -0,0 +1,259 @@
+using System.Buffers.Text;
+using System.Text;
+using System.Text.Json;
+using ioxide.pg;
+
+namespace IoxideArena;
+
+internal enum CrudKind { None, List, GetOne, Create, Update }
+
+///
+/// /crud/items - the realistic REST profile. The parser stashes the operation here (method, id,
+/// query, body); the handler runs it against Postgres with cache-aside on single-item reads
+/// (X-Cache: MISS/HIT, invalidated on PUT). SQL is parameterized and auto-prepared - constant
+/// statements so Postgres plans each once, values passed as params.
+///
+internal sealed unsafe partial class HttpSession
+{
+ public CrudKind PendingCrud;
+ public bool PendingCrudClose;
+
+ private int _crudId;
+ private string _crudCategory = "";
+ private int _crudPage = 1, _crudLimit = 10;
+ private byte[] _crudBody = [];
+ private int _crudBodyLen;
+
+ // -- routing (synchronous, in Respond) --------------------------------
+
+ private void RouteCrud(ReadOnlySpan method, ReadOnlySpan path, ReadOnlySpan query, ReadOnlySpan body, bool close)
+ {
+ PendingCrudClose = close;
+ bool isPost = method.SequenceEqual("POST"u8);
+ bool isPut = method.SequenceEqual("PUT"u8);
+
+ ReadOnlySpan rest = path[("/crud/items".Length)..]; // "" or "/{id}"
+ if (rest.IsEmpty)
+ {
+ if (isPost) { StashBody(body); PendingCrud = CrudKind.Create; }
+ else { ParseListParams(query); PendingCrud = CrudKind.List; }
+ return;
+ }
+ if (rest[0] == (byte)'/')
+ {
+ Utf8Parser.TryParse(rest[1..], out _crudId, out _);
+ if (isPut) { StashBody(body); PendingCrud = CrudKind.Update; }
+ else { PendingCrud = CrudKind.GetOne; }
+ }
+ }
+
+ private void StashBody(ReadOnlySpan body)
+ {
+ if (_crudBody.Length < body.Length) _crudBody = new byte[Math.Max(body.Length, 1024)];
+ body.CopyTo(_crudBody);
+ _crudBodyLen = body.Length;
+ }
+
+ private void ParseListParams(ReadOnlySpan query)
+ {
+ _crudCategory = ""; _crudPage = 1; _crudLimit = 10;
+ while (query.Length > 0)
+ {
+ int amp = query.IndexOf((byte)'&');
+ ReadOnlySpan kv = amp >= 0 ? query[..amp] : query;
+ int eq = kv.IndexOf((byte)'=');
+ if (eq >= 0)
+ {
+ ReadOnlySpan k = kv[..eq];
+ ReadOnlySpan v = kv[(eq + 1)..];
+ if (k.SequenceEqual("category"u8)) _crudCategory = Encoding.ASCII.GetString(v);
+ else if (k.SequenceEqual("page"u8)) { Utf8Parser.TryParse(v, out int p, out _); _crudPage = Math.Max(1, p); }
+ else if (k.SequenceEqual("limit"u8)) { Utf8Parser.TryParse(v, out int l, out _); _crudLimit = Math.Clamp(l, 1, 100); }
+ }
+ if (amp < 0) break;
+ query = query[(amp + 1)..];
+ }
+ }
+
+ // -- SQL (built from the stashed op) ----------------------------------
+
+ // Parameterized crud statements. The SQL is constant, so each connection's prepared-statement
+ // cache hits every time after the first - Postgres plans each once and the values travel as
+ // params (no per-request parse/plan, no escaping). List is a plain index range scan over
+ // idx_items_category_id with no count(*) OVER(): the validator only needs total > 0 and the spec
+ // defines total as the returned page size (load-more semantics), so the window scan was waste.
+ // The args are stack spans built in these non-async helpers, consumed into the send buffer before
+ // any await - so the span never lands on an async state machine.
+ private const string SqlList =
+ "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count " +
+ "FROM items WHERE category = $1 ORDER BY id LIMIT $2 OFFSET $3";
+ private const string SqlItem =
+ "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count " +
+ "FROM items WHERE id = $1";
+ private const string SqlInsert =
+ "INSERT INTO items (id, name, category, price, quantity, active, tags, rating_score, rating_count) " +
+ "VALUES ($1, $2, $3, $4, $5, false, '[]'::jsonb, 0, 0) " +
+ "ON CONFLICT (id) DO UPDATE SET name = excluded.name, category = excluded.category, " +
+ "price = excluded.price, quantity = excluded.quantity";
+ private const string SqlUpdate =
+ "UPDATE items SET name = $1, category = $2, price = $3, quantity = $4 WHERE id = $5";
+
+ public ValueTask SubmitCrudList(PgPool pool, PgRowHandler onRow)
+ {
+ ReadOnlySpan args =
+ [PgParam.Text(_crudCategory), PgParam.Int(_crudLimit), PgParam.Int((_crudPage - 1) * _crudLimit)];
+ return pool.QueryAsync(SqlList, args, onRow);
+ }
+
+ public ValueTask SubmitCrudItem(PgPool pool, PgRowHandler onRow)
+ {
+ ReadOnlySpan args = [PgParam.Int(_crudId)];
+ return pool.QueryAsync(SqlItem, args, onRow);
+ }
+
+ public ValueTask SubmitCrudInsert(PgPool pool)
+ {
+ (int id, string name, string category, int price, int quantity) = ParseItemBody();
+ ReadOnlySpan args =
+ [PgParam.Int(id), PgParam.Text(name), PgParam.Text(category), PgParam.Int(price), PgParam.Int(quantity)];
+ return pool.QueryAsync(SqlInsert, args);
+ }
+
+ public ValueTask SubmitCrudUpdate(PgPool pool)
+ {
+ (_, string name, string category, int price, int quantity) = ParseItemBody();
+ ReadOnlySpan args =
+ [PgParam.Text(name), PgParam.Text(category), PgParam.Int(price), PgParam.Int(quantity), PgParam.Int(_crudId)];
+ return pool.QueryAsync(SqlUpdate, args);
+ }
+
+ public string CacheKey() => $"crud:item:{_crudId}";
+
+ private (int Id, string Name, string Category, int Price, int Quantity) ParseItemBody()
+ {
+ int id = _crudId, price = 0, quantity = 0;
+ string name = "", category = "";
+ var reader = new Utf8JsonReader(_crudBody.AsSpan(0, _crudBodyLen));
+ string prop = "";
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.PropertyName) prop = reader.GetString() ?? "";
+ else switch (prop)
+ {
+ case "id" when reader.TokenType == JsonTokenType.Number: id = reader.GetInt32(); break;
+ case "name" when reader.TokenType == JsonTokenType.String: name = reader.GetString() ?? ""; break;
+ case "category" when reader.TokenType == JsonTokenType.String: category = reader.GetString() ?? ""; break;
+ case "price" when reader.TokenType == JsonTokenType.Number: price = reader.GetInt32(); break;
+ case "quantity" when reader.TokenType == JsonTokenType.Number: quantity = reader.GetInt32(); break;
+ }
+ }
+ return (id, name, category, price, quantity);
+ }
+
+
+ // -- list response ----------------------------------------------------
+
+ private int _crudClOff, _crudBodyStart;
+ private bool _crudFirstRow;
+ private int _crudRows;
+
+ public void BeginCrudList()
+ {
+ AppendOut("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: "u8);
+ _crudClOff = OutLen;
+ AppendOut("00000000\r\n"u8);
+ if (PendingCrudClose) AppendOut("Connection: close\r\n"u8);
+ AppendOut("\r\n"u8);
+ _crudBodyStart = OutLen;
+ AppendOut("{\"items\":["u8);
+ _crudFirstRow = true;
+ _crudRows = 0;
+ }
+
+ public void AppendCrudRow(PgRow row)
+ {
+ if (!_crudFirstRow) AppendOut(","u8);
+ _crudFirstRow = false;
+ _crudRows++;
+ AppendItem(row);
+ }
+
+ public void EndCrudList()
+ {
+ AppendOut("],\"total\":"u8); AppendLong(_crudRows);
+ AppendOut(",\"page\":"u8); AppendLong(_crudPage);
+ AppendOut(",\"count\":"u8); AppendLong(_crudRows);
+ AppendOut("}"u8);
+ BackfillLength(_crudClOff, OutLen - _crudBodyStart, 8);
+ }
+
+ // -- single item (cache-aside) ----------------------------------------
+
+ private byte[] _itemJson = [];
+ private int _itemJsonLen;
+ public bool CrudItemFound { get; private set; }
+
+ // Row handler for the single-item query: build the item JSON into a scratch.
+ public void CaptureCrudItem(PgRow row)
+ {
+ byte[] savedOut = Out; int savedLen = OutLen;
+ Out = _itemJson; OutLen = 0;
+ AppendItem(row);
+ _itemJson = Out; _itemJsonLen = OutLen;
+ Out = savedOut; OutLen = savedLen;
+ CrudItemFound = true;
+ }
+
+ public void ResetCrudItem() => CrudItemFound = false;
+
+ public ReadOnlySpan CrudItemBody() => _itemJson.AsSpan(0, _itemJsonLen);
+
+ /// Write the single-item 200 with the X-Cache marker; body is freshly built or cached.
+ public void WriteCrudItemResponse(ReadOnlySpan jsonBody, bool cacheHit)
+ {
+ AppendOut("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nX-Cache: "u8);
+ AppendOut(cacheHit ? "HIT"u8 : "MISS"u8);
+ AppendOut("\r\nContent-Length: "u8);
+ AppendLong(jsonBody.Length);
+ if (PendingCrudClose) AppendOut("\r\nConnection: close"u8);
+ AppendOut("\r\n\r\n"u8);
+ AppendOut(jsonBody);
+ }
+
+ public void WriteCrud404()
+ {
+ AppendOut("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n"u8);
+ if (PendingCrudClose) AppendOut("Connection: close\r\n"u8);
+ AppendOut("\r\n"u8);
+ }
+
+ public void WriteCrudStatus(ReadOnlySpan statusLine)
+ {
+ AppendOut(statusLine);
+ if (PendingCrudClose) AppendOut("Connection: close\r\n"u8);
+ AppendOut("\r\n"u8);
+ }
+
+ public void WriteCrudUnavailable() =>
+ WriteCrudStatus("HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\n"u8);
+
+ // Item object shared by list rows and single item (mirrors the async-db shape).
+ private void AppendItem(PgRow row)
+ {
+ AppendOut("{\"id\":"u8); AppendOut(row.Field(0));
+ AppendOut(",\"name\":\""u8); AppendOut(row.Field(1));
+ AppendOut("\",\"category\":\""u8); AppendOut(row.Field(2));
+ AppendOut("\",\"price\":"u8); AppendOut(row.Field(3));
+ AppendOut(",\"quantity\":"u8); AppendOut(row.Field(4));
+ AppendOut(row.Field(5).SequenceEqual("t"u8) ? ",\"active\":true"u8 : ",\"active\":false"u8);
+ AppendOut(",\"tags\":"u8); AppendOut(row.Field(6));
+ AppendOut(",\"rating\":{\"score\":"u8); AppendOut(row.Field(7));
+ AppendOut(",\"count\":"u8); AppendOut(row.Field(8));
+ AppendOut("}}"u8);
+ }
+
+ private void BackfillLength(int offset, int value, int digits)
+ {
+ for (int d = offset + digits - 1; d >= offset; d--) { Out[d] = (byte)('0' + value % 10); value /= 10; }
+ }
+}
diff --git a/frameworks/ioxide-0-4-169/Dockerfile b/frameworks/ioxide-0-4-169/Dockerfile
new file mode 100644
index 000000000..e5527d906
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/Dockerfile
@@ -0,0 +1,24 @@
+# Pinned, and that is the whole point of this entry. The floating
+# 11.0-preview tag moved from preview.6 (published 2026-07-28) to preview.7
+# (2026-08-28), and the 4.1M baseline was measured on 2026-08-09 -- i.e. on
+# preview.6. Leaving it floating makes both arms of the comparison run today's
+# runtime, which is what hid the variable the first time round.
+# SDK and runtime version their tags differently: 11.0.1xx vs 11.0.0.
+ARG DOTNET_SDK_TAG=11.0.100-preview.6
+ARG DOTNET_RUNTIME_TAG=11.0.0-preview.6
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_SDK_TAG} AS build
+WORKDIR /source
+COPY ioxide-arena.csproj ./
+RUN dotnet restore
+COPY . .
+RUN dotnet publish -c Release --no-self-contained -o /app/out
+
+# ioxide drives io_uring through direct libc syscalls (no liburing). The bench
+# harness runs containers with --security-opt seccomp=unconfined (required for
+# io_uring_setup/enter); engine="io_uring" makes validate.sh enable it too.
+ARG DOTNET_RUNTIME_TAG
+FROM mcr.microsoft.com/dotnet/runtime:${DOTNET_RUNTIME_TAG}
+WORKDIR /app
+COPY --from=build /app/out ./
+EXPOSE 8080 8081 8443 8443/udp
+ENTRYPOINT ["dotnet", "ioxide-arena.dll"]
diff --git a/frameworks/ioxide-0-4-169/Handler.cs b/frameworks/ioxide-0-4-169/Handler.cs
new file mode 100644
index 000000000..3695b6cfa
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/Handler.cs
@@ -0,0 +1,265 @@
+using ioxide;
+using ioxide.file;
+using ioxide.pg;
+using ioxide.tls;
+using ioxide.http2;
+using ioxide.utils;
+
+namespace IoxideArena;
+
+internal static class Handler
+{
+ private static int _slab = 16 * 1024;
+ private static Dataset _dataSet = Dataset.Empty;
+ private static StaticAssets? _staticAssets;
+ private static Precompressed? _precompressed;
+ private static bool _hasPg;
+ private static bool _hasTls;
+ private static bool _hasCache;
+
+ public static void Init(ServerConfig config, Dataset ds, StaticAssets? assets, Precompressed? precompressed, bool hasPg, bool hasTls, bool hasCache)
+ {
+ _slab = config.Tcp!.WriteSlabSize;
+ _dataSet = ds;
+ _staticAssets = assets;
+ _precompressed = precompressed;
+ _hasPg = hasPg;
+ _hasTls = hasTls;
+ _hasCache = hasCache;
+ }
+
+ public static async Task HandleAsync(Reactor reactor, TcpConnection conn)
+ {
+ if (conn.ListenerPort == 8443)
+ {
+ await ServeH2Async(reactor, conn);
+ return;
+ }
+
+ var httpSession = new HttpSession(_dataSet, _staticAssets, _precompressed);
+ PgPool? pool = _hasPg ? reactor.GetService() : null;
+ ICrudCache? cache = _hasCache ? reactor.GetService() : null;
+ PgRowHandler rowSink = httpSession.AppendDbRow; // async-db rows
+ PgRowHandler listSink = httpSession.AppendCrudRow; // crud list rows
+ PgRowHandler itemSink = httpSession.CaptureCrudItem; // crud single item
+ TlsSession? tls = null;
+
+ try
+ {
+ if (_hasTls && conn.ListenerPort == 8081)
+ {
+ // Handshake over the ring, then kTLS TX: outbound writes below are plaintext
+ // and the kernel produces the records. Inbound stays userspace: each slice
+ // decrypts through the session. The client's first request can ride in with its
+ // Finished flight, so feed it before the loop parks on a read.
+ tls = await reactor.GetService().AcceptAsync(conn);
+ httpSession.Feed(tls.DrainPlaintext());
+ }
+
+ // Send-first: respond to whatever is already parsed (a request bundled
+ // with the TLS handshake, or a prior read) before parking on the next
+ // read. A read-first loop would deadlock on the bundled-request case.
+ while (true)
+ {
+ // /async-db parks the parser: run the query (inline on this reactor's
+ // ring via ioxide.pg), stream rows into Out, then resume the carry -
+ // pipelined requests behind it are served in order.
+ while (httpSession.PendingDb)
+ {
+ httpSession.PendingDb = false;
+ if (pool != null)
+ {
+ httpSession.BeginDbResponse();
+ await pool.QueryRowsAsync(httpSession.PendingDbSql(), rowSink);
+ httpSession.EndDbResponse();
+ }
+ else
+ {
+ httpSession.WriteDbUnavailable();
+ }
+
+ if (httpSession.PendingDbClose) httpSession.WantClose = true;
+ else httpSession.ResumeFeed();
+ }
+
+ while (httpSession.PendingCrud != CrudKind.None)
+ {
+ CrudKind kind = httpSession.PendingCrud;
+ httpSession.PendingCrud = CrudKind.None;
+
+ if (pool == null)
+ {
+ httpSession.WriteCrudUnavailable();
+ }
+ else switch (kind)
+ {
+ case CrudKind.List:
+ httpSession.BeginCrudList();
+ await httpSession.SubmitCrudList(pool, listSink);
+ httpSession.EndCrudList();
+ break;
+
+ case CrudKind.GetOne:
+ string key = httpSession.CacheKey();
+ string? cached = cache != null ? await cache.GetAsync(key) : null;
+ if (cached != null)
+ {
+ httpSession.WriteCrudItemResponse(System.Text.Encoding.UTF8.GetBytes(cached), cacheHit: true);
+ }
+ else
+ {
+ httpSession.ResetCrudItem();
+ await httpSession.SubmitCrudItem(pool, itemSink);
+ if (httpSession.CrudItemFound)
+ {
+ if (cache != null)
+ await cache.SetExAsync(key, System.Text.Encoding.UTF8.GetString(httpSession.CrudItemBody()), 1);
+ httpSession.WriteCrudItemResponse(httpSession.CrudItemBody(), cacheHit: false);
+ }
+ else
+ {
+ httpSession.WriteCrud404();
+ }
+ }
+ break;
+
+ case CrudKind.Create:
+ await httpSession.SubmitCrudInsert(pool);
+ httpSession.WriteCrudStatus("HTTP/1.1 201 Created\r\nContent-Length: 0\r\n"u8);
+ break;
+
+ case CrudKind.Update:
+ await httpSession.SubmitCrudUpdate(pool);
+ if (cache != null) await cache.DelAsync(httpSession.CacheKey());
+ httpSession.WriteCrudStatus("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n"u8);
+ break;
+ }
+
+ if (httpSession.PendingCrudClose) httpSession.WantClose = true;
+ else httpSession.ResumeFeed();
+ }
+
+ // Baked static responses go straight to the wire (not through Out) - no extra copy,
+ // and Out never grows to the largest asset, so per-connection memory stays flat
+ // under load. Sent before Out, which preserves order (Direct is only set when it was
+ // the first response of the batch).
+ if (httpSession.HasDirect)
+ {
+ int dsent = 0;
+ while (dsent < httpSession.DirectLen)
+ {
+ int dchunk = Math.Min(httpSession.DirectLen - dsent, _slab);
+ WriteDirect(conn, httpSession, dsent, dchunk);
+ await conn.FlushAsync();
+ dsent += dchunk;
+ }
+ httpSession.ClearDirect();
+ }
+
+ int sent = 0;
+ while (sent < httpSession.OutLen)
+ {
+ int chunk = Math.Min(httpSession.OutLen - sent, _slab);
+ conn.Write(httpSession.Out.AsSpan(sent, chunk));
+
+ // The static header is the tail of Out, so the file goes into the slab right
+ // behind it and header + body leave in ONE flush - no reader buffer and no
+ // copy of the body, which is the whole point of ReadFileAsync. The slab grows
+ // to fit; ioxide.file only hands out a descriptor and a length.
+ if (sent + chunk == httpSession.OutLen && httpSession.PendingStaticFd != 0)
+ {
+ int n = await conn.ReadFileAsync(httpSession.PendingStaticFd,
+ (int)httpSession.PendingStaticLen, fileOffset: 0);
+ conn.AdvanceWrite(n);
+ httpSession.PendingStaticFd = 0;
+ if (httpSession.PendingStaticClose) httpSession.WantClose = true;
+ }
+
+ await conn.FlushAsync();
+ sent += chunk;
+ }
+ httpSession.OutLen = 0;
+ httpSession.PendingStaticFd = 0;
+
+ if (httpSession.WantClose || (tls?.Closed ?? false))
+ return;
+
+ RecvSnapshot snap = await conn.ReadAsync();
+ FeedSlices(httpSession, conn, tls, snap);
+ if (snap.IsClosed)
+ {
+ httpSession.WantClose = true;
+ }
+ else
+ {
+ conn.ResetRead();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[{Thread.CurrentThread.Name}] http handler crash fd={conn.ClientFd}: {ex}");
+ }
+ finally
+ {
+ tls?.Dispose();
+ conn.DecRef();
+ }
+ }
+
+ // Copy one slab-sized slice of the direct (baked static) response into the connection's write
+ // slab - managed precompressed buffer or native identity response. Kept in a sync unsafe helper
+ // so the native pointer never crosses an await in the async handler.
+ private static unsafe void WriteDirect(TcpConnection conn, HttpSession s, int off, int len)
+ {
+ if (s.DirectBytes != null)
+ {
+ conn.Write(s.DirectBytes.AsSpan(off, len));
+ }
+ else
+ {
+ conn.Write(new ReadOnlySpan((void*)(s.DirectPtr + off), len));
+ }
+ }
+
+ private static async Task ServeH2Async(Reactor reactor, TcpConnection conn)
+ {
+ TlsSession? session = null;
+ try
+ {
+ session = await reactor.GetService().Service.AcceptAsync(conn);
+ await using var pipe = new TlsConnectionDualPipe(conn, session, ownsSession: false);
+ await new Http2Connection(pipe).RunBufferedAsync(Multiplexed.RouteH2);
+ }
+ catch
+ {
+ // probe / handshake fault
+ }
+ finally
+ {
+ session?.Dispose();
+ conn.DecRef();
+ }
+ }
+
+ private static unsafe void FeedSlices(HttpSession s, TcpConnection conn, TlsSession? tls, in RecvSnapshot snap)
+ {
+ while (conn.TryGetItem(snap, out SpscRecvRing.Item item))
+ {
+ if (!item.HasBuffer)
+ {
+ continue;
+ }
+ if (tls != null)
+ {
+ s.Feed(tls.Decrypt(item.Ptr, item.Len));
+ }
+ else
+ {
+ s.Feed(item.AsSpan());
+ }
+
+ conn.ReturnBuffer(in item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/frameworks/ioxide-0-4-169/HttpSession.cs b/frameworks/ioxide-0-4-169/HttpSession.cs
new file mode 100644
index 000000000..e5486618f
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/HttpSession.cs
@@ -0,0 +1,739 @@
+using System.Buffers.Text;
+using System.IO.Compression;
+using System.Text;
+using System.Text.Json;
+using ioxide.file;
+using ioxide.pg;
+
+namespace IoxideArena;
+
+///
+/// Hand-rolled HTTP/1.1: accumulates inbound bytes, parses complete requests
+/// (request line, headers, Content-Length + chunked bodies, keep-alive,
+/// pipelining, fragmented reads), and appends responses to .
+///
+internal sealed unsafe partial class HttpSession
+{
+ private readonly Dataset _ds;
+ private readonly StaticAssets? _assets;
+ private readonly Precompressed? _precompressed;
+ private byte[] _carry = new byte[2048];
+ private int _carryLen;
+
+ public byte[] Out = new byte[4096];
+ public int OutLen;
+ public bool WantClose;
+
+ // A baked static response sent straight to the connection, bypassing Out. This avoids copying
+ // the (up to ~66 KB) asset into the per-connection Out buffer and stops Out from ballooning to
+ // the largest asset under load (which was inflating CPU + memory on the static profile). Set
+ // only when it's the first response of a batch (OutLen == 0); otherwise the asset rides Out so
+ // pipelined ordering is preserved. Either a managed buffer (precompressed) or a native span
+ // (ioxide.file's identity baked response).
+ public byte[]? DirectBytes;
+ public nint DirectPtr;
+ public int DirectLen;
+ public bool HasDirect => DirectLen > 0;
+ public void ClearDirect() { DirectBytes = null; DirectPtr = 0; DirectLen = 0; }
+
+ public HttpSession(Dataset ds, StaticAssets? assets, Precompressed? precompressed)
+ {
+ _ds = ds;
+ _assets = assets;
+ _precompressed = precompressed;
+ }
+
+ // /async-db parks the parser here; the handler runs the query and resumes.
+ public bool PendingDb;
+ public bool PendingDbClose;
+
+ // A static file the handler must read off the ring. ioxide.file no longer bakes HTTP
+ // responses (0.4.167 made it io_uring reads only), so the header is framed here and the body
+ // is read straight into the connection's write slab by the handler - no intermediate buffer
+ // and no copy. Legal on the TLS ports too, because kTLS transmit is on: the slab is supposed
+ // to hold plaintext there and the kernel makes the records on send.
+ public int PendingStaticFd;
+ public long PendingStaticLen;
+ public bool PendingStaticClose;
+ private long _dbMin = 10, _dbMax = 50;
+ private int _dbLimit = 50;
+ private int _dbClOff;
+ private bool _dbFirstRow;
+ private int _dbRows;
+
+ // /upload streams its body: bytes are counted as they arrive, never buffered whole.
+ public long PendingUploadRemaining;
+ private long _uploadTotal;
+ private bool _uploadClose;
+
+ public void ResumeFeed() => Pump();
+
+ public void Feed(ReadOnlySpan data)
+ {
+ // While draining a large upload, count the bytes and drop them - the body is never buffered.
+ if (PendingUploadRemaining > 0)
+ {
+ int take = (int)Math.Min(PendingUploadRemaining, (long)data.Length);
+ PendingUploadRemaining -= take;
+ if (PendingUploadRemaining > 0)
+ {
+ return; // more body still to come; nothing buffered
+ }
+ FinishUpload(); // last byte counted - write the byte-count response
+ data = data[take..]; // any remainder is the start of the next request
+ if (data.IsEmpty)
+ {
+ return;
+ }
+ }
+ AppendCarry(data);
+ Pump();
+ }
+
+ // The streamed upload's body is fully counted; emit the 200 with the total byte count.
+ private void FinishUpload()
+ {
+ Span num = stackalloc byte[20];
+ Utf8Formatter.TryFormat(_uploadTotal, num, out int n);
+ WriteResp(num[..n], _uploadClose);
+
+ if (_uploadClose)
+ {
+ WantClose = true;
+ }
+ }
+
+ private void Pump()
+ {
+ int pos = 0;
+ while (!PendingDb && PendingUploadRemaining == 0
+ && TryOne(_carry.AsSpan(pos, _carryLen - pos), out int consumed, out bool close))
+ {
+ pos += consumed;
+ if (close && !PendingDb)
+ {
+ WantClose = true; break;
+ }
+ }
+ if (pos > 0)
+ {
+ int rem = _carryLen - pos;
+ if (rem > 0)
+ {
+ Array.Copy(_carry, pos, _carry, 0, rem);
+ }
+ _carryLen = rem;
+ }
+ }
+
+ /// Parse one request from buf; append its response to Out. Returns false if
+ /// the request isn't fully buffered yet.
+ private bool TryOne(ReadOnlySpan buf, out int consumed, out bool close)
+ {
+ consumed = 0;
+ close = false;
+ bool acceptBr = false;
+ bool acceptGzip = false;
+
+ int he = buf.IndexOf("\r\n\r\n"u8);
+ if (he < 0) return false;
+ ReadOnlySpan head = buf[..he];
+
+ int rlEnd = head.IndexOf("\r\n"u8);
+ if (rlEnd < 0) rlEnd = head.Length;
+ ReadOnlySpan reqLine = head[..rlEnd];
+
+ ReadOnlySpan method = default;
+ ReadOnlySpan target = default;
+ int sp1 = reqLine.IndexOf((byte)' ');
+ if (sp1 >= 0)
+ {
+ method = reqLine[..sp1];
+ ReadOnlySpan rest = reqLine[(sp1 + 1)..];
+ int sp2 = rest.IndexOf((byte)' ');
+ target = sp2 >= 0 ? rest[..sp2] : rest;
+ }
+
+ // A POST /upload body is streamed (counted), not buffered - detect it before reading the body.
+ int qix = target.IndexOf((byte)'?');
+ bool isUpload = method.SequenceEqual("POST"u8)
+ && (qix >= 0 ? target[..qix] : target).SequenceEqual("/upload"u8);
+
+ int contentLength = -1;
+ bool chunked = false;
+ ReadOnlySpan hdrs = head[Math.Min(rlEnd + 2, head.Length)..];
+ while (hdrs.Length > 0)
+ {
+ int nl = hdrs.IndexOf("\r\n"u8);
+ ReadOnlySpan line = nl >= 0 ? hdrs[..nl] : hdrs;
+ int colon = line.IndexOf((byte)':');
+ if (colon >= 0)
+ {
+ ReadOnlySpan name = line[..colon];
+ ReadOnlySpan val = Trim(line[(colon + 1)..]);
+ if (CiEq(name, "content-length"u8))
+ {
+ if (Utf8Parser.TryParse(val, out int cl, out _)) contentLength = cl;
+ }
+ else if (CiEq(name, "transfer-encoding"u8) && CiContains(val, "chunked"u8))
+ {
+ chunked = true;
+ }
+ else if (CiEq(name, "connection"u8) && CiEq(val, "close"u8))
+ {
+ close = true;
+ }
+ else if (CiEq(name, "accept-encoding"u8))
+ {
+ if (CiContains(val, "br"u8)) acceptBr = true;
+ if (CiContains(val, "gzip"u8)) acceptGzip = true;
+ }
+ }
+ if (nl < 0) break;
+ hdrs = hdrs[(nl + 2)..];
+ }
+
+ int bodyStart = he + 4;
+ long bodyInt;
+ int total;
+ ReadOnlySpan body = default;
+ int bodyLen = 0;
+ if (chunked)
+ {
+ if (!DecodeChunked(buf[bodyStart..], out bodyInt, out int used)) return false;
+ total = bodyStart + used;
+ }
+ else if (contentLength > 0)
+ {
+ if (isUpload && buf.Length < bodyStart + contentLength)
+ {
+ // Stream it: count the body bytes already here, then drain the rest across reads so
+ // memory stays bounded regardless of upload size (genhttp does the same). Defer the
+ // close and the response until the body is fully counted.
+ _uploadTotal = contentLength;
+ PendingUploadRemaining = contentLength - (buf.Length - bodyStart);
+ _uploadClose = close;
+ close = false;
+ consumed = buf.Length;
+ return true;
+ }
+ if (buf.Length < bodyStart + contentLength) return false;
+ body = buf.Slice(bodyStart, contentLength);
+ bodyLen = contentLength;
+ bodyInt = ParseLoose(body);
+ total = bodyStart + contentLength;
+ }
+ else
+ {
+ bodyInt = 0;
+ total = bodyStart;
+ }
+
+ Respond(method, target, body, bodyLen, bodyInt, close, acceptBr, acceptGzip);
+ consumed = total;
+ return true;
+ }
+
+ private void Respond(ReadOnlySpan method, ReadOnlySpan target, ReadOnlySpan body, int bodyLen, long bodyInt, bool close, bool acceptBr, bool acceptGzip)
+ {
+ int q = target.IndexOf((byte)'?');
+ ReadOnlySpan path = q >= 0 ? target[..q] : target;
+ ReadOnlySpan query = q >= 0 ? target[(q + 1)..] : default;
+
+ if (path.SequenceEqual("/pipeline"u8))
+ {
+ WriteResp("ok"u8, close);
+ }
+ else if (path.StartsWith("/json/"u8))
+ {
+ ReadOnlySpan tail = path[6..];
+ if (Utf8Parser.TryParse(tail, out int count, out int used) && used == tail.Length
+ && count >= 1 && count <= _ds.Count)
+ {
+ JsonResp(count, ParseM(query), close, acceptBr);
+ }
+ else
+ {
+ Write404(close);
+ }
+ }
+ else if (path.StartsWith("/static/"u8))
+ {
+ // Content negotiation is HTTP, so it lives here: serve the best precompressed
+ // variant the client accepts (br > gzip), else ioxide.file's identity baked response,
+ // else 404. Precompressed responses already carry Content-Encoding and Vary.
+ byte[]? pre = _precompressed?.Negotiate(path[7..], acceptBr, acceptGzip);
+ if (pre != null)
+ {
+ if (OutLen == 0 && !HasDirect) { DirectBytes = pre; DirectLen = pre.Length; }
+ else AppendOut(pre);
+ }
+ else if (_assets != null && _assets.TryGet(path[7..], out AssetCache.Asset asset))
+ {
+ // Header now, body later: the handler reads the file into the write slab right
+ // after this header lands in it, so both leave in ONE flush with no copy of the
+ // body anywhere. Only one file can be pending per batch, so a second static hit
+ // in the same pipeline falls through to the header-only path and is served on the
+ // next pass.
+ AppendOut("HTTP/1.1 200 OK\r\nContent-Type: "u8);
+ AppendOut(MimeFor(asset.Path));
+ AppendOut("\r\nContent-Length: "u8);
+ Span len = stackalloc byte[20];
+ Utf8Formatter.TryFormat(asset.Length, len, out int lw);
+ AppendOut(len[..lw]);
+ AppendOut(close ? "\r\nConnection: close\r\n\r\n"u8 : "\r\n\r\n"u8);
+
+ PendingStaticFd = asset.Fd;
+ PendingStaticLen = asset.Length;
+ PendingStaticClose = close;
+ }
+ else
+ {
+ Write404(close);
+ }
+ }
+ else if (path.SequenceEqual("/async-db"u8))
+ {
+ ParseDbParams(query);
+ PendingDb = true;
+ PendingDbClose = close;
+ }
+ else if (path.SequenceEqual("/upload"u8))
+ {
+ Span num = stackalloc byte[16];
+ Utf8Formatter.TryFormat(bodyLen, num, out int n);
+ WriteResp(num[..n], close);
+ }
+ else if (path.StartsWith("/crud/items"u8))
+ {
+ RouteCrud(method, path, query, body, close);
+ }
+ else
+ {
+ long sum = SumAB(query) + bodyInt;
+ Span num = stackalloc byte[24];
+ Utf8Formatter.TryFormat(sum, num, out int n);
+ WriteResp(num[..n], close);
+ }
+ }
+
+ // ioxide.file hands back a descriptor and a length; the content type is HTTP's business, so
+ // it is decided here rather than baked into the asset.
+ private static ReadOnlySpan MimeFor(string path) => Path.GetExtension(path) switch
+ {
+ ".html" => "text/html"u8,
+ ".css" => "text/css"u8,
+ ".js" => "application/javascript"u8,
+ ".json" => "application/json"u8,
+ ".svg" => "image/svg+xml"u8,
+ ".png" => "image/png"u8,
+ ".jpg" => "image/jpeg"u8,
+ ".webp" => "image/webp"u8,
+ ".txt" => "text/plain"u8,
+ _ => "application/octet-stream"u8,
+ };
+
+ private void WriteResp(ReadOnlySpan body, bool close)
+ {
+ AppendOut("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: "u8);
+ Span num = stackalloc byte[16];
+ Utf8Formatter.TryFormat(body.Length, num, out int n);
+ AppendOut(num[..n]);
+ AppendOut(close ? "\r\nConnection: close\r\n\r\n"u8 : "\r\n\r\n"u8);
+ AppendOut(body);
+ }
+
+ private byte[]? _jsonScratch;
+ private byte[]? _brotli;
+
+ private void JsonResp(int count, long m, bool close, bool acceptBr)
+ {
+ _jsonScratch ??= new byte[16 * 1024]; // allocated on first /json, not per connection
+
+ // Build the body first (into the scratch) so headers carry an exact length.
+ byte[] savedOut = Out;
+ int savedLen = OutLen;
+ Out = _jsonScratch;
+ OutLen = 0;
+ WriteJsonBody(count, m);
+ _jsonScratch = Out; // may have been resized by AppendOut
+ int bodyLen = OutLen;
+ Out = savedOut;
+ OutLen = savedLen;
+
+ AppendOut("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n"u8);
+
+ if (acceptBr)
+ {
+ // json-comp: per-request brotli (fast quality) - never compressed
+ // without Accept-Encoding, per the anti-cheat check.
+ int max = BrotliEncoder.GetMaxCompressedLength(bodyLen);
+ _brotli ??= new byte[16 * 1024];
+ if (_brotli.Length < max)
+ Array.Resize(ref _brotli, Math.Max(max, _brotli.Length * 2));
+ BrotliEncoder.TryCompress(_jsonScratch.AsSpan(0, bodyLen), _brotli, out int written,
+ quality: 1, window: 22);
+
+ AppendOut("Content-Encoding: br\r\nContent-Length: "u8);
+ AppendLong(written);
+ AppendOut(close ? "\r\nConnection: close\r\n\r\n"u8 : "\r\n\r\n"u8);
+ AppendOut(_brotli.AsSpan(0, written));
+ }
+ else
+ {
+ AppendOut("Content-Length: "u8);
+ AppendLong(bodyLen);
+ AppendOut(close ? "\r\nConnection: close\r\n\r\n"u8 : "\r\n\r\n"u8);
+ AppendOut(_jsonScratch.AsSpan(0, bodyLen));
+ }
+ }
+
+ // Serialize from the parsed model on every request - no precomputed fragments.
+ private void WriteJsonBody(int count, long m)
+ {
+ AppendOut("{\"items\":["u8);
+ for (int i = 0; i < count; i++)
+ {
+ if (i > 0) AppendOut(","u8);
+ ref readonly Item it = ref _ds.Items[i];
+ AppendOut("{\"id\":"u8);
+ AppendLong(it.Id);
+ AppendOut(",\"name\":\""u8);
+ AppendOut(it.Name);
+ AppendOut("\",\"category\":\""u8);
+ AppendOut(it.Category);
+ AppendOut("\",\"price\":"u8);
+ AppendLong(it.Price);
+ AppendOut(",\"quantity\":"u8);
+ AppendLong(it.Quantity);
+ AppendOut(it.Active ? ",\"active\":true,\"tags\":["u8 : ",\"active\":false,\"tags\":["u8);
+ for (int t = 0; t < it.Tags.Length; t++)
+ {
+ if (t > 0) AppendOut(","u8);
+ AppendOut("\""u8);
+ AppendOut(it.Tags[t]);
+ AppendOut("\""u8);
+ }
+ AppendOut("],\"rating\":{\"score\":"u8);
+ AppendLong(it.Score);
+ AppendOut(",\"count\":"u8);
+ AppendLong(it.RatingCount);
+ AppendOut("},\"total\":"u8);
+ AppendLong(it.Price * it.Quantity * m);
+ AppendOut("}"u8);
+ }
+ AppendOut("],\"count\":"u8);
+ AppendLong(count);
+ AppendOut("}"u8);
+ }
+
+ private void Write404(bool close)
+ {
+ AppendOut("HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 9\r\n"u8);
+ if (close) AppendOut("Connection: close\r\n"u8);
+ AppendOut("\r\nNot Found"u8);
+ }
+
+ private void AppendLong(long v)
+ {
+ Span num = stackalloc byte[20];
+ Utf8Formatter.TryFormat(v, num, out int n);
+ AppendOut(num[..n]);
+ }
+
+ private static long ParseM(ReadOnlySpan query)
+ {
+ while (query.Length > 0)
+ {
+ int amp = query.IndexOf((byte)'&');
+ ReadOnlySpan kv = amp >= 0 ? query[..amp] : query;
+ if (kv.Length >= 2 && kv[0] == (byte)'m' && kv[1] == (byte)'=')
+ {
+ Utf8Parser.TryParse(kv[2..], out long m, out _);
+ return m;
+ }
+ if (amp < 0) break;
+ query = query[(amp + 1)..];
+ }
+ return 1;
+ }
+
+ private static long SumAB(ReadOnlySpan query)
+ {
+ long a = 0, b = 0;
+ while (query.Length > 0)
+ {
+ int amp = query.IndexOf((byte)'&');
+ ReadOnlySpan kv = amp >= 0 ? query[..amp] : query;
+ int eq = kv.IndexOf((byte)'=');
+ if (eq >= 0)
+ {
+ ReadOnlySpan k = kv[..eq];
+ ReadOnlySpan v = kv[(eq + 1)..];
+ if (k.SequenceEqual("a"u8)) a = ParseLoose(v);
+ else if (k.SequenceEqual("b"u8)) b = ParseLoose(v);
+ }
+ if (amp < 0) break;
+ query = query[(amp + 1)..];
+ }
+ return a + b;
+ }
+
+ /// Decode a chunked body into an integer. Returns false if the terminating
+ /// 0-chunk isn't fully buffered. Bodies in these profiles are tiny.
+ private static bool DecodeChunked(ReadOnlySpan buf, out long bodyInt, out int used)
+ {
+ bodyInt = 0;
+ used = 0;
+ Span body = stackalloc byte[256];
+ int blen = 0;
+ int pos = 0;
+ while (true)
+ {
+ int nl = buf[pos..].IndexOf("\r\n"u8);
+ if (nl < 0) return false;
+ if (!ParseHex(buf.Slice(pos, nl), out int size)) return false;
+ pos += nl + 2;
+ if (size == 0)
+ {
+ int end = buf[pos..].IndexOf("\r\n"u8); // final CRLF (no trailers)
+ if (end < 0) return false;
+ used = pos + end + 2;
+ bodyInt = ParseLoose(body[..blen]);
+ return true;
+ }
+ if (buf.Length < pos + size + 2) return false;
+ if (blen + size <= body.Length)
+ {
+ buf.Slice(pos, size).CopyTo(body[blen..]);
+ blen += size;
+ }
+ pos += size;
+ if (!buf.Slice(pos, 2).SequenceEqual("\r\n"u8)) return false;
+ pos += 2;
+ }
+ }
+
+ // ── /async-db ────────────────────────────────────────────────────────────
+
+ private void ParseDbParams(ReadOnlySpan query)
+ {
+ _dbMin = 10; _dbMax = 50; _dbLimit = 50;
+ while (query.Length > 0)
+ {
+ int amp = query.IndexOf((byte)'&');
+ ReadOnlySpan kv = amp >= 0 ? query[..amp] : query;
+ int eq = kv.IndexOf((byte)'=');
+ if (eq >= 0)
+ {
+ ReadOnlySpan k = kv[..eq];
+ ReadOnlySpan v = kv[(eq + 1)..];
+ if (k.SequenceEqual("min"u8)) _dbMin = ParseLoose(v);
+ else if (k.SequenceEqual("max"u8)) _dbMax = ParseLoose(v);
+ else if (k.SequenceEqual("limit"u8)) _dbLimit = Math.Clamp((int)ParseLoose(v), 1, 50);
+ }
+ if (amp < 0) break;
+ query = query[(amp + 1)..];
+ }
+ }
+
+ public string PendingDbSql() =>
+ $"SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count " +
+ $"FROM items WHERE price BETWEEN {_dbMin} AND {_dbMax} LIMIT {_dbLimit}";
+
+ public void BeginDbResponse()
+ {
+ AppendOut("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: "u8);
+ _dbClOff = OutLen;
+ AppendOut("000000\r\n"u8);
+ if (PendingDbClose) AppendOut("Connection: close\r\n"u8);
+ AppendOut("\r\n"u8);
+ _dbBodyStart = OutLen;
+ AppendOut("{\"items\":["u8);
+ _dbFirstRow = true;
+ _dbRows = 0;
+ }
+
+ private int _dbBodyStart;
+
+ // Streams straight from the driver's receive buffer: numbers and the JSONB
+ // tags array are already valid JSON text, so they append verbatim.
+ public void AppendDbRow(PgRow row)
+ {
+ if (!_dbFirstRow) AppendOut(","u8);
+ _dbFirstRow = false;
+ _dbRows++;
+
+ AppendOut("{\"id\":"u8); AppendOut(row.Field(0));
+ AppendOut(",\"name\":\""u8); AppendOut(row.Field(1));
+ AppendOut("\",\"category\":\""u8); AppendOut(row.Field(2));
+ AppendOut("\",\"price\":"u8); AppendOut(row.Field(3));
+ AppendOut(",\"quantity\":"u8); AppendOut(row.Field(4));
+ AppendOut(row.Field(5).SequenceEqual("t"u8) ? ",\"active\":true"u8 : ",\"active\":false"u8);
+ AppendOut(",\"tags\":"u8); AppendOut(row.Field(6));
+ AppendOut(",\"rating\":{\"score\":"u8); AppendOut(row.Field(7));
+ AppendOut(",\"count\":"u8); AppendOut(row.Field(8));
+ AppendOut("}}"u8);
+ }
+
+ public void EndDbResponse()
+ {
+ AppendOut("],\"count\":"u8);
+ AppendLong(_dbRows);
+ AppendOut("}"u8);
+
+ int v = OutLen - _dbBodyStart;
+ for (int d = _dbClOff + 5; d >= _dbClOff; d--) { Out[d] = (byte)('0' + v % 10); v /= 10; }
+ }
+
+ public void WriteDbUnavailable()
+ {
+ AppendOut("HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\n\r\n"u8);
+ if (PendingDbClose) WantClose = true;
+ }
+
+ // ── byte helpers ─────────────────────────────────────────────────────────
+ private void AppendCarry(ReadOnlySpan d)
+ {
+ if (_carry.Length < _carryLen + d.Length)
+ Array.Resize(ref _carry, Math.Max(_carryLen + d.Length, _carry.Length * 2));
+ d.CopyTo(_carry.AsSpan(_carryLen));
+ _carryLen += d.Length;
+ }
+
+ private void AppendOut(ReadOnlySpan d)
+ {
+ if (Out.Length < OutLen + d.Length)
+ Array.Resize(ref Out, Math.Max(OutLen + d.Length, Out.Length * 2));
+ d.CopyTo(Out.AsSpan(OutLen));
+ OutLen += d.Length;
+ }
+
+ private static ReadOnlySpan Trim(ReadOnlySpan b)
+ {
+ int s = 0, e = b.Length;
+ while (s < e && (b[s] == (byte)' ' || b[s] == (byte)'\t')) s++;
+ while (e > s && (b[e - 1] == (byte)' ' || b[e - 1] == (byte)'\t')) e--;
+ return b[s..e];
+ }
+
+ private static bool CiEq(ReadOnlySpan a, ReadOnlySpan b)
+ {
+ if (a.Length != b.Length) return false;
+ for (int i = 0; i < a.Length; i++)
+ if (Lower(a[i]) != Lower(b[i])) return false;
+ return true;
+ }
+
+ private static bool CiContains(ReadOnlySpan h, ReadOnlySpan n)
+ {
+ if (n.Length == 0 || h.Length < n.Length) return false;
+ for (int i = 0; i + n.Length <= h.Length; i++)
+ if (CiEq(h.Slice(i, n.Length), n)) return true;
+ return false;
+ }
+
+ private static byte Lower(byte c) => (byte)(c >= 'A' && c <= 'Z' ? c + 32 : c);
+
+ private static long ParseLoose(ReadOnlySpan s)
+ {
+ int i = 0;
+ while (i < s.Length && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r' || s[i] == '\n')) i++;
+ bool neg = false;
+ if (i < s.Length && s[i] == '-') { neg = true; i++; }
+ long n = 0;
+ while (i < s.Length && s[i] >= '0' && s[i] <= '9') { n = n * 10 + (s[i] - '0'); i++; }
+ return neg ? -n : n;
+ }
+
+ private static bool ParseHex(ReadOnlySpan b, out int val)
+ {
+ val = 0;
+ bool any = false;
+ foreach (byte c in b)
+ {
+ int d;
+ if (c >= '0' && c <= '9') d = c - '0';
+ else if (c >= 'a' && c <= 'f') d = c - 'a' + 10;
+ else if (c >= 'A' && c <= 'F') d = c - 'A' + 10;
+ else if (c == ';' || c == ' ') break;
+ else return any;
+ val = val * 16 + d;
+ any = true;
+ }
+ return any;
+ }
+}
+
+///
+/// A dataset item parsed into its model fields (string values stored as UTF-8).
+/// The json handler serializes these field-by-field on every request.
+///
+internal readonly struct Item
+{
+ public readonly long Id, Price, Quantity, Score, RatingCount;
+ public readonly bool Active;
+ public readonly byte[] Name, Category;
+ public readonly byte[][] Tags;
+
+ public Item(long id, byte[] name, byte[] category, long price, long quantity,
+ bool active, byte[][] tags, long score, long ratingCount)
+ {
+ Id = id; Name = name; Category = category; Price = price; Quantity = quantity;
+ Active = active; Tags = tags; Score = score; RatingCount = ratingCount;
+ }
+}
+
+///
+/// Dataset for the json profile — items parsed into model fields at startup so
+/// the handler serializes the full JSON from the model on every request (no
+/// precomputed / cached response fragments). Read-only after load, shared across
+/// reactor threads. String values are clean ASCII in the bench dataset, so the
+/// handler emits them without escaping.
+///
+internal sealed class Dataset
+{
+ public readonly Item[] Items;
+ public int Count => Items.Length;
+
+ public static readonly Dataset Empty = new(Array.Empty- ());
+
+ private Dataset(Item[] items) { Items = items; }
+
+ public static Dataset Load(string path)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(File.ReadAllBytes(path));
+ JsonElement root = doc.RootElement;
+ int n = root.GetArrayLength();
+ var items = new Item[n];
+ int i = 0;
+ foreach (JsonElement e in root.EnumerateArray())
+ {
+ JsonElement rating = e.GetProperty("rating");
+ JsonElement tagsEl = e.GetProperty("tags");
+ var tags = new byte[tagsEl.GetArrayLength()][];
+ int t = 0;
+ foreach (JsonElement tag in tagsEl.EnumerateArray())
+ tags[t++] = Encoding.UTF8.GetBytes(tag.GetString() ?? "");
+ items[i++] = new Item(
+ e.GetProperty("id").GetInt64(),
+ Encoding.UTF8.GetBytes(e.GetProperty("name").GetString() ?? ""),
+ Encoding.UTF8.GetBytes(e.GetProperty("category").GetString() ?? ""),
+ e.GetProperty("price").GetInt64(),
+ e.GetProperty("quantity").GetInt64(),
+ e.GetProperty("active").GetBoolean(),
+ tags,
+ rating.GetProperty("score").GetInt64(),
+ rating.GetProperty("count").GetInt64());
+ }
+ return new Dataset(items);
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[minima] dataset load failed ({path}): {ex.Message}");
+ return Empty;
+ }
+ }
+}
diff --git a/frameworks/ioxide-0-4-169/Multiplexed.cs b/frameworks/ioxide-0-4-169/Multiplexed.cs
new file mode 100644
index 000000000..66c5dd2c8
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/Multiplexed.cs
@@ -0,0 +1,193 @@
+using System.Text;
+using ioxide.http2;
+using ioxide.nghttp3;
+using ioxide.tls;
+
+using Headers = System.ReadOnlySpan, System.ReadOnlyMemory>>;
+
+namespace IoxideArena;
+
+/// The h2 TLS context (ALPN "h2", port 8443), held per reactor next to the h1 one.
+internal sealed record H2Tls(TlsService Service);
+
+///
+/// Routes shared by the h2 (:8443/tcp) and h3 (:8443/udp) servers: /baseline2 and /static/*.
+/// Static assets are cached at startup with their precompressed .br/.gz variants and content
+/// type, and served by Accept-Encoding - the same negotiation the h1 static path does.
+///
+internal static class Multiplexed
+{
+ private readonly record struct Asset(byte[] Body, byte[]? Br, byte[]? Gz, byte[] Type);
+
+ private static readonly Dictionary Assets = new(StringComparer.Ordinal);
+ private static Dictionary.AlternateLookup> _lookup;
+
+ private static readonly byte[] ContentType = "content-type"u8.ToArray();
+ private static readonly byte[] ContentEncoding = "content-encoding"u8.ToArray();
+ private static readonly byte[] AcceptEncoding = "accept-encoding"u8.ToArray();
+ private static readonly byte[] BrToken = "br"u8.ToArray();
+ private static readonly byte[] GzipToken = "gzip"u8.ToArray();
+ private static readonly byte[] TextPlain = "text/plain"u8.ToArray();
+ private static readonly byte[] NotFound = "not found"u8.ToArray();
+
+ public static void Init(string? staticRoot)
+ {
+ if (staticRoot != null)
+ {
+ var acc = new Dictionary(StringComparer.Ordinal);
+
+ foreach (var file in Directory.EnumerateFiles(staticRoot, "*", SearchOption.AllDirectories))
+ {
+ var rel = Path.GetRelativePath(staticRoot, file).Replace('\\', '/');
+ var bytes = File.ReadAllBytes(file);
+
+ if (rel.EndsWith(".br", StringComparison.Ordinal))
+ {
+ var b = rel[..^3];
+ acc[b] = acc.GetValueOrDefault(b) with { Br = bytes };
+ }
+ else if (rel.EndsWith(".gz", StringComparison.Ordinal))
+ {
+ var b = rel[..^3];
+ acc[b] = acc.GetValueOrDefault(b) with { Gz = bytes };
+ }
+ else
+ {
+ acc[rel] = acc.GetValueOrDefault(rel) with { Body = bytes };
+ }
+ }
+
+ foreach (var (name, v) in acc)
+ {
+ if (v.Body != null)
+ {
+ Assets[name] = new Asset(v.Body, v.Br, v.Gz, TypeFor(name));
+ }
+ }
+ }
+
+ _lookup = Assets.GetAlternateLookup>();
+ }
+
+ public static Http2Response RouteH2(Http2Request request)
+ {
+ var (status, body, type, encoding) = Route(request.Path.Span, request.Headers.AsSpan());
+ var response = new Http2Response { Status = status, Body = body };
+ response.Headers.Add(ContentType, type);
+ if (encoding != null)
+ {
+ response.Headers.Add(ContentEncoding, encoding);
+ }
+ return response;
+ }
+
+ public static Nghttp3Response RouteH3(Nghttp3Request request)
+ {
+ var (status, body, type, encoding) = Route(request.Path.Span, request.Headers.AsSpan());
+ var response = new Nghttp3Response { Status = status, Body = body };
+ response.Headers.Add(ContentType, type);
+ if (encoding != null)
+ {
+ response.Headers.Add(ContentEncoding, encoding);
+ }
+ return response;
+ }
+
+ private static (int Status, byte[] Body, byte[] Type, byte[]? Encoding) Route(ReadOnlySpan path, Headers headers)
+ {
+ if (path.StartsWith("/baseline2"u8))
+ {
+ return (200, Encoding.ASCII.GetBytes(SumQuery(path).ToString()), TextPlain, null);
+ }
+
+ if (path.StartsWith("/static/"u8))
+ {
+ var name = path[8..];
+ int q = name.IndexOf((byte)'?');
+ if (q >= 0)
+ {
+ name = name[..q];
+ }
+
+ Span chars = stackalloc char[name.Length];
+ Ascii.ToUtf16(name, chars, out int written);
+
+ if (_lookup.TryGetValue(chars[..written], out var asset))
+ {
+ var (body, encoding) = Negotiate(headers, asset);
+ return (200, body, asset.Type, encoding);
+ }
+ }
+
+ return (404, NotFound, TextPlain, null);
+ }
+
+ // Serve the precompressed variant the client accepts, br preferred over gzip, else identity.
+ private static (byte[] Body, byte[]? Encoding) Negotiate(Headers headers, in Asset asset)
+ {
+ bool br = false, gz = false;
+
+ foreach (var header in headers)
+ {
+ // h2/h3 header names are lowercase by spec, so an ordinal compare is enough.
+ if (header.Key.Span.SequenceEqual(AcceptEncoding))
+ {
+ var value = header.Value.Span;
+ br = value.IndexOf(BrToken) >= 0;
+ gz = value.IndexOf(GzipToken) >= 0;
+ break;
+ }
+ }
+
+ if (br && asset.Br != null)
+ {
+ return (asset.Br, BrToken);
+ }
+ if (gz && asset.Gz != null)
+ {
+ return (asset.Gz, GzipToken);
+ }
+ return (asset.Body, null);
+ }
+
+ // "?a=1&b=1" - every value after '=' up to the next '&' is an int; anything else is 0.
+ private static long SumQuery(ReadOnlySpan path)
+ {
+ int q = path.IndexOf((byte)'?');
+ if (q < 0)
+ {
+ return 0;
+ }
+
+ long sum = 0;
+ var rest = path[(q + 1)..];
+
+ while (!rest.IsEmpty)
+ {
+ int amp = rest.IndexOf((byte)'&');
+ var pair = amp < 0 ? rest : rest[..amp];
+ rest = amp < 0 ? default : rest[(amp + 1)..];
+
+ int eq = pair.IndexOf((byte)'=');
+ if (eq >= 0 && System.Buffers.Text.Utf8Parser.TryParse(pair[(eq + 1)..], out long value, out _))
+ {
+ sum += value;
+ }
+ }
+
+ return sum;
+ }
+
+ private static byte[] TypeFor(string name) => Path.GetExtension(name) switch
+ {
+ ".html" => "text/html"u8.ToArray(),
+ ".css" => "text/css"u8.ToArray(),
+ ".js" => "text/javascript"u8.ToArray(),
+ ".json" => "application/json"u8.ToArray(),
+ ".svg" => "image/svg+xml"u8.ToArray(),
+ ".webp" => "image/webp"u8.ToArray(),
+ ".png" => "image/png"u8.ToArray(),
+ ".woff2" => "font/woff2"u8.ToArray(),
+ _ => TextPlain,
+ };
+}
diff --git a/frameworks/ioxide-0-4-169/Precompressed.cs b/frameworks/ioxide-0-4-169/Precompressed.cs
new file mode 100644
index 000000000..0b4d5224d
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/Precompressed.cs
@@ -0,0 +1,89 @@
+using System.Buffers;
+using System.Text;
+
+namespace IoxideArena;
+
+///
+/// Precompressed static variants, baked in the HTTP entry - ioxide.file serves the identity bytes;
+/// content negotiation is HTTP, so it lives here, not in the runtime. For each base file that has a
+/// .br/.gz sibling the whole response (base content-type, Content-Encoding, Vary, Content-Length,
+/// body) is baked once at startup and chosen per request by Accept-Encoding (br > gzip). A request
+/// the client can't take compressed falls back to ioxide.file's identity asset.
+///
+internal sealed class Precompressed
+{
+ private readonly record struct Variant(byte[]? Br, byte[]? Gz);
+
+ private readonly Dictionary _byPath = new(StringComparer.Ordinal);
+ private readonly Dictionary.AlternateLookup> _lookup;
+
+ public int Count { get; }
+
+ public Precompressed(string staticDir)
+ {
+ string root = Path.GetFullPath(staticDir);
+ foreach (string path in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
+ {
+ if (path.EndsWith(".br", StringComparison.Ordinal) || path.EndsWith(".gz", StringComparison.Ordinal))
+ {
+ continue; // bases only; .br/.gz are looked up as siblings
+ }
+ string url = "/" + Path.GetRelativePath(root, path).Replace('\\', '/');
+ string contentType = MimeFor(path);
+ byte[]? br = File.Exists(path + ".br") ? Bake(File.ReadAllBytes(path + ".br"), contentType, "br") : null;
+ byte[]? gz = File.Exists(path + ".gz") ? Bake(File.ReadAllBytes(path + ".gz"), contentType, "gzip") : null;
+ if (br != null || gz != null)
+ {
+ _byPath[url] = new Variant(br, gz);
+ }
+ }
+ _lookup = _byPath.GetAlternateLookup>();
+ Count = _byPath.Count;
+ }
+
+ /// Best accepted precompressed response for the URL (br > gzip), or null to use identity.
+ public byte[]? Negotiate(ReadOnlySpan urlPath, bool acceptBr, bool acceptGzip)
+ {
+ if (urlPath.Length is 0 or > 1024)
+ {
+ return null;
+ }
+ Span chars = stackalloc char[urlPath.Length];
+ if (Ascii.ToUtf16(urlPath, chars, out int n) != OperationStatus.Done)
+ {
+ return null;
+ }
+ if (!_lookup.TryGetValue(chars[..n], out Variant v))
+ {
+ return null;
+ }
+ if (acceptBr && v.Br != null) return v.Br;
+ if (acceptGzip && v.Gz != null) return v.Gz;
+ return null;
+ }
+
+ private static byte[] Bake(byte[] body, string contentType, string encoding)
+ {
+ string head = $"HTTP/1.1 200 OK\r\nContent-Type: {contentType}\r\n" +
+ $"Content-Encoding: {encoding}\r\nVary: Accept-Encoding\r\nContent-Length: {body.Length}\r\n\r\n";
+ byte[] header = Encoding.ASCII.GetBytes(head);
+ var response = new byte[header.Length + body.Length];
+ header.CopyTo(response, 0);
+ body.CopyTo(response, header.Length);
+ return response;
+ }
+
+ private static string MimeFor(string path) => Path.GetExtension(path) switch
+ {
+ ".html" => "text/html",
+ ".css" => "text/css",
+ ".js" => "application/javascript",
+ ".json" => "application/json",
+ ".svg" => "image/svg+xml",
+ ".png" => "image/png",
+ ".webp" => "image/webp",
+ ".woff2" => "font/woff2",
+ ".txt" => "text/plain",
+ _ => "application/octet-stream",
+ };
+}
diff --git a/frameworks/ioxide-0-4-169/Program.cs b/frameworks/ioxide-0-4-169/Program.cs
new file mode 100644
index 000000000..a72773235
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/Program.cs
@@ -0,0 +1,227 @@
+using System.Net;
+using System.Runtime.InteropServices;
+using ioxide;
+using ioxide.utils;
+using ioxide.pg;
+using ioxide.file;
+using ioxide.tls;
+using ioxide.nghttp3;
+using ioxide.ngtcp2;
+using ioxide.redis;
+using StackExchange.Redis;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace IoxideArena;
+
+///
+/// ioxide - the ioxide runtime (consumed as its published NuGet packages) serving the H1
+/// profiles. The engine is untouched; the HTTP/1.1 handler (request line, headers,
+/// Content-Length + chunked bodies, keep-alive, pipelining, fragmented reads) is hand-written
+/// on the raw recv/send API. No HTTP framework.
+///
+/// Endpoints:
+/// GET/POST /baseline11?a=&b= -> text/plain "a + b (+ body)"
+/// GET /pipeline -> text/plain "ok"
+/// GET /json/{count}?m=N -> application/json, total = price*quantity*N
+/// GET /static/{file} -> baked asset snapshots (ioxide.file)
+/// GET /async-db?min=&max=&limit= -> Postgres seq scan via ioxide.pg (SCRAM-SHA-256)
+///
+internal static class Program
+{
+ // Held for the process lifetime so the registrations aren't garbage-collected.
+ private static PosixSignalRegistration? _sigTerm;
+ private static PosixSignalRegistration? _sigInt;
+
+ private static int Main()
+ {
+ // Exit promptly on `docker stop` (SIGTERM) instead of lingering until SIGKILL. The bench
+ // harness restarts the framework per profile but keeps ONE Postgres for the whole run, so a
+ // slow teardown leaves this server's ~PoolSize*reactors backends occupying connection slots
+ // while the next profile's server eagerly opens its own pool against the same Postgres.
+ // Exiting at once closes our sockets so Postgres reaps those backends before the handoff.
+ _sigTerm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, ctx => { ctx.Cancel = true; Environment.Exit(0); });
+ _sigInt = PosixSignalRegistration.Create(PosixSignal.SIGINT, ctx => { ctx.Cancel = true; Environment.Exit(0); });
+
+ // One reactor per core, capped at 64 so a hyperthreaded box (ProcessorCount counts logical
+ // CPUs, e.g. 128 on 64 cores + SMT) doesn't oversubscribe. IOXIDE_REACTORS overrides.
+ int reactors = Math.Min(Environment.ProcessorCount, 64);
+ if (int.TryParse(Environment.GetEnvironmentVariable("IOXIDE_REACTORS"), out int r) && r > 0)
+ reactors = r;
+ Console.WriteLine($"[ioxide] ProcessorCount={Environment.ProcessorCount}, reactors={reactors}");
+
+ ushort port = 8080;
+ if (ushort.TryParse(Environment.GetEnvironmentVariable("IOXIDE_PORT"), out ushort p) && p > 0)
+ port = p;
+
+ // TLS on :8081 when the harness mounts certs (json-tls profile).
+ string certPath = Environment.GetEnvironmentVariable("TLS_CERT") ?? "/certs/server.crt";
+ string keyPath = Environment.GetEnvironmentVariable("TLS_KEY") ?? "/certs/server.key";
+ bool tls = File.Exists(certPath) && File.Exists(keyPath);
+
+ // Recv buffer ring, env-tunable: the upload profile moves large bodies, so each recv slice is
+ // capped at the buffer size - bigger buffers mean far fewer slices (CQEs + returns) for the
+ // same bytes. recvKb * ringEntries is the reserved recv memory per reactor.
+ int recvKb = int.TryParse(Environment.GetEnvironmentVariable("IOXIDE_RECV_KB"), out int rk) && rk > 0 ? rk : 16;
+ int ringEntries = int.TryParse(Environment.GetEnvironmentVariable("IOXIDE_RING_ENTRIES"), out int re) && re > 0 ? re : 256;
+
+ // h1 TLS terminates on :8081, h2 on :8443/tcp, h3 on :8443/udp - all OpenSSL in
+ // userspace, the fastest backend on loopback.
+ using var quic = tls ? new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"]) : null;
+
+ var config = new ServerConfig
+ {
+ ReactorCount = reactors,
+ RecvBufferSize = recvKb * 1024,
+ RecvSlots = ringEntries,
+ Tcp = new TcpOptions
+ {
+ Port = port,
+ ExtraPorts = tls ? [(ushort)8081, (ushort)8443] : [],
+ // 128 KB so a static response fits one slab and the handler sends it without chunk-flushing.
+ WriteSlabSize = 128 * 1024,
+ },
+ Quic = quic == null ? null : new QuicOptions
+ {
+ Port = 8443,
+ LocalCidLength = 8,
+ ConnectionFactory = quic.CreateFactory(),
+ },
+ };
+
+ var dsPath = Environment.GetEnvironmentVariable("IOXIDE_DATASET") ?? "/data/dataset.json";
+ var dataset = Dataset.Load(dsPath);
+
+ // Static assets: every file under the root opened ONCE, descriptors shared across
+ // reactors and read positionally off the ring. Nothing is cached in memory and no HTTP is
+ // baked - the response header is framed in HttpSession and the body is read straight into
+ // the connection's write slab, so header and body leave in one flush with no copy.
+ var staticRoot = Environment.GetEnvironmentVariable("IOXIDE_STATIC") ?? "/data/static";
+ StaticAssets? assets = Directory.Exists(staticRoot)
+ ? new StaticAssets(staticRoot) // descriptors only; bodies are read off the ring per request
+ : null;
+ // Precompressed variants are baked here (HTTP), not in ioxide.file.
+ Precompressed? precompressed = Directory.Exists(staticRoot) ? new Precompressed(staticRoot) : null;
+
+ // Postgres: DATABASE_URL=postgres://user:pass@host:port/db (validation/benchmark sidecar).
+ PgOptions? pg = null;
+ var dbUrl = Environment.GetEnvironmentVariable("DATABASE_URL");
+ if (!string.IsNullOrEmpty(dbUrl))
+ {
+ var uri = new Uri(dbUrl);
+ string[] userInfo = uri.UserInfo.Split(':', 2);
+ int maxConn = int.TryParse(Environment.GetEnvironmentVariable("DATABASE_MAX_CONN"), out int mc) ? mc : 256;
+
+ pg = new PgOptions
+ {
+ Host = ResolveIPv4(uri.Host),
+ Port = (ushort)(uri.Port > 0 ? uri.Port : 5432),
+ User = userInfo[0],
+ Password = userInfo.Length > 1 ? userInfo[1] : null,
+ Database = uri.AbsolutePath.TrimStart('/'),
+ PoolSize = Math.Clamp(maxConn / reactors, 1, 8),
+ };
+ }
+
+ // Redis: REDIS_URL=redis://host:port (crud cache-aside sidecar).
+ RedisOptions? redis = null;
+ var redisUrl = Environment.GetEnvironmentVariable("REDIS_URL");
+ if (!string.IsNullOrEmpty(redisUrl))
+ {
+ var uri = new Uri(redisUrl);
+ redis = new RedisOptions
+ {
+ Host = ResolveIPv4(uri.Host),
+ Port = (ushort)(uri.Port > 0 ? uri.Port : 6379),
+ PoolSize = 4,
+ };
+ }
+
+ // Crud cache backend (CRUD_CACHE): inproc (default) = one shared IMemoryCache, fully
+ // in-process and inline on the reactor (no network, no thread pool); ioxide = ioxide.redis
+ // (per-reactor, pipelined, on the ring); stackexchange = StackExchange.Redis (one shared
+ // multiplexer, off-ring). The Redis backends need REDIS_URL; without it they fall back to inproc.
+ string cacheBackend = Environment.GetEnvironmentVariable("CRUD_CACHE") ?? "inproc";
+ if ((cacheBackend == "ioxide" || cacheBackend == "stackexchange") && redis == null)
+ {
+ cacheBackend = "inproc";
+ }
+ IMemoryCache? memCache = cacheBackend == "inproc" ? new MemoryCache(new MemoryCacheOptions()) : null;
+ ConnectionMultiplexer? mux = cacheBackend == "stackexchange"
+ ? ConnectionMultiplexer.Connect($"{redis!.Host}:{redis.Port}")
+ : null;
+
+ Console.WriteLine($"[ioxide] {config.ReactorCount} reactors on :{config.Tcp!.Port} " +
+ $"(dataset={dataset.Count} items, static={(assets?.Count ?? 0)} files ({(precompressed?.Count ?? 0)} precompressed), " +
+ $"pg={(pg != null ? $"{pg.Host}:{pg.Port}/{pg.Database} pool={pg.PoolSize}" : "off")}, " +
+ $"tls={(tls ? "h1 :8081, h2 :8443 (full ktls), h3 udp:8443" : "off")}, " +
+ $"cache={(pg != null ? cacheBackend : "off")})");
+
+ Multiplexed.Init(Directory.Exists(staticRoot) ? staticRoot : null);
+ Handler.Init(config, dataset, assets, precompressed, pg != null, tls, pg != null);
+
+ var threads = new Thread[config.ReactorCount];
+ for (int i = 0; i < config.ReactorCount; i++)
+ {
+ var reactor = new Reactor(i, config);
+ var pgOptions = pg;
+ var redisOptions = redis;
+
+ reactor.OnStart = reactorInstance =>
+ {
+ if (pgOptions != null)
+ {
+ PgPool.Start(reactorInstance, pgOptions);
+
+ // Crud cache, shared across reactors (inproc/stackexchange) or per-reactor (ioxide).
+ ICrudCache cache = cacheBackend switch
+ {
+ "ioxide" => new IoxideRedisCache(RedisPool.Start(reactorInstance, redisOptions!)),
+ "stackexchange" => new StackExchangeCache(mux!.GetDatabase()),
+ _ => new InProcCache(memCache!),
+ };
+ reactorInstance.AddService(cache);
+ }
+ if (tls)
+ {
+ // Full kTLS for h1 and h2: the kernel makes the records on send and decrypts
+ // inbound, so recv delivers plaintext straight into ring memory. RX is
+ // experimental (about one first connection in twelve fails the handoff).
+ TlsService.Start(reactorInstance, new TlsOptions
+ { CertificatePath = certPath, KeyPath = keyPath, KernelTx = true, KernelRx = true });
+ reactorInstance.AddService(new H2Tls(TlsService.Start(reactorInstance,
+ new TlsOptions { CertificatePath = certPath, KeyPath = keyPath, KernelTx = true, KernelRx = true, Alpn = ["h2"] },
+ register: false)));
+ }
+ };
+
+ reactor.TcpHandle = Handler.HandleAsync;
+ if (tls)
+ {
+ reactor.QuicHandle = (_, qc) => new Nghttp3Connection(qc).RunBufferedAsync(Multiplexed.RouteH3);
+ }
+ threads[i] = new Thread(reactor.Run) { Name = $"reactor-{i}", IsBackground = false };
+ threads[i].Start();
+ }
+ foreach (var t in threads)
+ {
+ t.Join();
+ }
+
+ return 0;
+ }
+
+ // RingSocket dials IPv4 literals; resolve names (e.g. "localhost") once, at startup.
+ private static string ResolveIPv4(string host)
+ {
+ if (IPAddress.TryParse(host, out _)) return host;
+ foreach (var addr in Dns.GetHostAddresses(host))
+ {
+ if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
+ {
+ return addr.ToString();
+ }
+ }
+
+ return "127.0.0.1";
+ }
+}
diff --git a/frameworks/ioxide-0-4-169/README.md b/frameworks/ioxide-0-4-169/README.md
new file mode 100644
index 000000000..15694a38c
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/README.md
@@ -0,0 +1,73 @@
+# ioxide-0.4.169
+
+The `ioxide` entry exactly as it stood at `cc794d06`, on ioxide **0.4.169** —
+the last version measured at 4.1M req/s on baseline. It exists only to separate
+a library regression from a change in the machine.
+
+**Disabled on purpose.** It never sends FIN on `Connection: close`, which is why
+the live entry was disabled at #1289 and fixed in 0.7.210, so it does not pass
+`validate.sh`. `benchmark.sh` does not check `enabled`, and the workflow's
+`enabled` filter only applies to `framework=all`, so it can still be benchmarked
+by name.
+
+## The measurement it is meant to settle
+
+| | baseline-4096 | CPU | %CPU per krps |
+|---|---|---|---|
+| 0.4.169 (2026-08-09) | 4,108,420 | 6401.5% | 1.558 |
+| 0.7.211 (2026-08-28) | 3,896,541 | 5983.6% | 1.535 |
+
+Throughput fell ~5% while CPU fell ~6.5%, so cost per request did not rise — the
+box is doing less total work rather than more work per request. No measurement
+exists for 0.7.210 on its own; the drop is attributed across the version bump
+and the entry's own changes together.
+
+Run this and the live entry back to back in the same session. Same numbers as
+today's ioxide means the machine moved; ~4.1M again means it is the library.
+
+## The runtime was the uncontrolled variable
+
+The first version of this entry pinned ioxide and left the base image on the
+floating `11.0-preview` tag — so both arms of the comparison ran the same
+runtime and the old entry regressed too, which proves nothing about the library.
+
+That tag moved:
+
+| tag | published | resolves to |
+|---|---|---|
+| `11.0.0-preview.6` | 2026-07-28 | 11.0.0-preview.6.26359.118 |
+| `11.0.0-preview.7` | 2026-08-28 | 11.0.0-preview.7.26381.103 |
+
+The 4.1M baseline was measured on **2026-08-09**, when `11.0-preview` was
+preview.6. The 3.9M was measured on **2026-08-28**, the day preview.7 shipped.
+
+So this entry pins both images. `DOTNET_SDK_TAG` and `DOTNET_RUNTIME_TAG` are
+separate build args because the two repositories version differently:
+`11.0.100-preview.N` for the SDK, `11.0.0-preview.N` for the runtime.
+
+Run it as-is (preview.6) against the same ioxide on preview.7 — override with
+`--build-arg DOTNET_SDK_TAG=11.0.100-preview.7 --build-arg
+DOTNET_RUNTIME_TAG=11.0.0-preview.7` — and the runtime is the only thing that
+differs.
+
+## Not the FIN fix
+
+The baseline workload never sends `Connection: close` — zero occurrences in
+`get.raw`, `post_cl.raw` and `post_chunked.raw` — and the `shutdown(fd, SHUT_WR)`
+added in 0.7.210 runs at connection teardown, outside the request loop. With
+`req_per_conn=0` it fires once per connection, not per request.
+
+The baseline profile spec (`1|0|0-31,64-95|512,4096|`) is byte-identical between
+the two dates, and `gcannon.sh`, `system.sh` and `framework.sh` are unchanged in
+that window, so the load side is not the variable either.
+
+## Restored verbatim
+
+Every source file is `git show cc794d06:frameworks/ioxide/`. Two
+deliberate deviations:
+
+- `nuget.config` keeps only nuget.org. The original also listed a local feed at
+ a developer path for then-unpublished packages; 0.4.169 is published, and the
+ path does not exist on the runner.
+- `tests` is trimmed to `baseline` and `json`. The original also listed `static`,
+ `crud`, `api-4` and `api-16`, which are no longer profiles (#1331, #1374).
diff --git a/frameworks/ioxide-0-4-169/ioxide-arena.csproj b/frameworks/ioxide-0-4-169/ioxide-arena.csproj
new file mode 100644
index 000000000..2e8b89771
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/ioxide-arena.csproj
@@ -0,0 +1,28 @@
+
+
+
+ Exe
+ net11.0
+ enable
+ enable
+ true
+
+ IoxideArena
+ ioxide-arena
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frameworks/ioxide-0-4-169/meta.json b/frameworks/ioxide-0-4-169/meta.json
new file mode 100644
index 000000000..7e24dc6cf
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/meta.json
@@ -0,0 +1,16 @@
+{
+ "display_name": "ioxide-0.4.169",
+ "language": "C#",
+ "type": "engine",
+ "engine": "io_uring",
+ "description": "The ioxide entry exactly as it stood at cc794d06 on ioxide 0.4.169, kept only as an A/B reference for the baseline regression. Disabled: it never sends FIN on Connection: close, which is why the live entry was disabled at #1289 and fixed in 0.7.210, so it does not pass validate.sh. Benchmark it by name to separate a library regression from a change in the box.",
+ "repo": "https://github.com/MDA2AV/ioxide",
+ "enabled": false,
+ "tests": [
+ "baseline",
+ "json"
+ ],
+ "maintainers": [
+ "MDA2AV"
+ ]
+}
diff --git a/frameworks/ioxide-0-4-169/nuget.config b/frameworks/ioxide-0-4-169/nuget.config
new file mode 100644
index 000000000..765346e53
--- /dev/null
+++ b/frameworks/ioxide-0-4-169/nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+