diff --git a/frameworks/touchsocket/.dockerignore b/frameworks/touchsocket/.dockerignore
new file mode 100644
index 000000000..cd42ee34e
--- /dev/null
+++ b/frameworks/touchsocket/.dockerignore
@@ -0,0 +1,2 @@
+bin/
+obj/
diff --git a/frameworks/touchsocket/ArenaPlugin.cs b/frameworks/touchsocket/ArenaPlugin.cs
new file mode 100644
index 000000000..72159943d
--- /dev/null
+++ b/frameworks/touchsocket/ArenaPlugin.cs
@@ -0,0 +1,162 @@
+using System.Text;
+
+using TouchSocket.Core;
+using TouchSocket.Http;
+
+using HttpMethod = TouchSocket.Http.HttpMethod;
+
+namespace TouchSocketArena;
+
+///
+/// The arena routes, answered from TouchSocket's HTTP plugin pipeline.
+///
+///
+/// A plugin is how TouchSocket exposes HTTP: it sees each request, answers the ones it recognises
+/// and calls the next one for the rest. Dispatch is on the path's first segment, which is what the
+/// component gives you - there is no router above it to declare routes with.
+///
+internal sealed class ArenaPlugin(Dataset dataset) : PluginBase, IHttpPlugin
+{
+ private static readonly Encoding Utf8 = new UTF8Encoding(false);
+
+ public async Task OnHttpRequest(IHttpSessionClient client, HttpContextEventArgs e)
+ {
+ var request = e.Context.Request;
+ var response = e.Context.Response;
+
+ var url = request.RelativeURL;
+
+ // Strip the query; TouchSocket parses it separately into Query.
+ var question = url.IndexOf('?');
+ var path = question < 0 ? url : url[..question];
+
+ switch (First(path, out var rest))
+ {
+ case "baseline11":
+ case "baseline2":
+ await BaselineAsync(request, response);
+ break;
+
+ case "json":
+ await JsonAsync(request, response, rest);
+ break;
+
+ case "pipeline":
+ await TextAsync(response, "ok");
+ break;
+
+ case "delay":
+ await DelayAsync(response, rest);
+ break;
+
+ default:
+ response.StatusCode = 404;
+ await response.AnswerAsync();
+ break;
+ }
+
+ e.Handled = true;
+ }
+
+ // GET /baseline11?a=1&b=2 - the sum as text. POST adds the body to it.
+ private async Task BaselineAsync(HttpRequest request, HttpResponse response)
+ {
+ var total = Number(request, "a") + Number(request, "b");
+
+ if (request.Method == HttpMethod.Post)
+ {
+ var body = await request.GetContentAsync();
+
+ if (body.Length > 0 && int.TryParse(Utf8.GetString(body.Span), out var fromBody))
+ {
+ total += fromBody;
+ }
+ }
+
+ await TextAsync(response, total.ToString());
+ }
+
+ // GET /delay/{ms} - answer after the wait, echoing the value back.
+ private async Task DelayAsync(HttpResponse response, string rest)
+ {
+ if (!int.TryParse(rest, out var ms) || ms < 0)
+ {
+ response.StatusCode = 404;
+ await response.AnswerAsync();
+ return;
+ }
+
+ if (ms > 0)
+ {
+ // Registers a timer and yields rather than holding a thread, so the waits in flight are
+ // bounded by memory instead of the pool.
+ await Task.Delay(ms);
+ }
+
+ await TextAsync(response, ms.ToString());
+ }
+
+ // GET /json/{count}?m={multiplier}
+ private async Task JsonAsync(HttpRequest request, HttpResponse response, string rest)
+ {
+ if (!int.TryParse(rest, out var count))
+ {
+ response.StatusCode = 404;
+ await response.AnswerAsync();
+ return;
+ }
+
+ var multiplier = int.TryParse(request.Query["m"], out var m) ? m : 1;
+
+ var accepted = request.Headers["Accept-Encoding"].ToString() ?? string.Empty;
+
+ var body = dataset.Render(count, multiplier,
+ accepted.Contains("br", StringComparison.OrdinalIgnoreCase),
+ accepted.Contains("gzip", StringComparison.OrdinalIgnoreCase),
+ out var encoding);
+
+ if (body is null)
+ {
+ response.StatusCode = 503;
+ await response.AnswerAsync();
+ return;
+ }
+
+ response.StatusCode = 200;
+ response.Headers.Add("Vary", "Accept-Encoding");
+
+ if (encoding is not null)
+ {
+ response.Headers.Add("Content-Encoding", encoding);
+ }
+
+ response.Content = new ReadonlyMemoryHttpContent(body, "application/json");
+ await response.AnswerAsync();
+ }
+
+ private static async Task TextAsync(HttpResponse response, string value)
+ {
+ response.StatusCode = 200;
+ response.Content = new StringHttpContent(value, Utf8, "text/plain");
+ await response.AnswerAsync();
+ }
+
+ private static int Number(HttpRequest request, string name)
+ => int.TryParse(request.Query[name], out var value) ? value : 0;
+
+ // The first path segment, with whatever follows it in `rest`.
+ private static string First(string path, out string rest)
+ {
+ var start = path.Length > 0 && path[0] == '/' ? 1 : 0;
+ var end = path.IndexOf('/', start);
+
+ if (end < 0)
+ {
+ rest = "";
+ return path[start..];
+ }
+
+ rest = path[(end + 1)..];
+ return path[start..end];
+ }
+}
diff --git a/frameworks/touchsocket/Dataset.cs b/frameworks/touchsocket/Dataset.cs
new file mode 100644
index 000000000..89d6ca947
--- /dev/null
+++ b/frameworks/touchsocket/Dataset.cs
@@ -0,0 +1,134 @@
+using System.Buffers;
+using System.IO.Compression;
+using System.Text.Json;
+
+namespace TouchSocketArena;
+
+///
+/// The dataset behind /json, parsed once at startup and serialized on every request.
+///
+///
+/// No precomputed responses: the workload asks for the same few shapes millions of times, so
+/// caching the finished bytes would turn the endpoint into a lookup table and stop measuring the
+/// server at all. Only scratch is reused - the serializer's buffer and the compressor's, both per
+/// thread and grown to their high-water mark.
+///
+public sealed class Dataset
+{
+ private readonly List- ? _items;
+
+ [ThreadStatic] private static ArrayBufferWriter? _json;
+ [ThreadStatic] private static byte[]? _compressed;
+
+ public Dataset()
+ {
+ var path = Environment.GetEnvironmentVariable("DATASET_PATH") ?? "/data/dataset.json";
+
+ if (File.Exists(path))
+ {
+ _items = JsonSerializer.Deserialize(File.ReadAllText(path), AppJsonContext.Default.ListItem);
+ }
+ }
+
+ public bool IsAvailable => _items is not null;
+
+ public int Count => _items?.Count ?? 0;
+
+ ///
+ /// Serializes the response for this count and multiplier, compressing it only when the client
+ /// said it would take one. Watson's Send takes a byte[], so the result is copied out of the
+ /// per-thread scratch rather than handed over as a span.
+ ///
+ public byte[]? Render(int count, int multiplier, bool wantsBrotli, bool wantsGzip, out string? encoding)
+ {
+ encoding = null;
+
+ if (_items is null)
+ {
+ return null;
+ }
+
+ var source = _items;
+
+ if (count > source.Count) count = source.Count;
+ if (count < 0) count = 0;
+
+ var items = new ProcessedItem[count];
+
+ for (var i = 0; i < count; i++)
+ {
+ var item = source[i];
+
+ items[i] = new ProcessedItem
+ {
+ Id = item.Id,
+ Name = item.Name,
+ Category = item.Category,
+ Price = item.Price,
+ Quantity = item.Quantity,
+ Active = item.Active,
+ Tags = item.Tags,
+ Rating = item.Rating,
+ Total = (long)item.Price * item.Quantity * multiplier
+ };
+ }
+
+ var buffer = _json ??= new ArrayBufferWriter(32 * 1024);
+ buffer.ResetWrittenCount();
+
+ using (var writer = new Utf8JsonWriter(buffer))
+ {
+ JsonSerializer.Serialize(writer, new ItemsResponse(items, count),
+ AppJsonContext.Default.ItemsResponseProcessedItem);
+ }
+
+ var body = buffer.WrittenSpan;
+
+ // Never compressed unless asked for: a response that arrives encoded without
+ // Accept-Encoding is exactly what the board's anti-cheat check looks for.
+ if (wantsBrotli)
+ {
+ encoding = "br";
+ return Compress(body, brotli: true).ToArray();
+ }
+
+ if (wantsGzip)
+ {
+ encoding = "gzip";
+ return Compress(body, brotli: false).ToArray();
+ }
+
+ return body.ToArray();
+ }
+
+ // Quality 1: the profile measures a server compressing a response, not the ratio a slow
+ // encoder can reach.
+ private static ReadOnlySpan Compress(ReadOnlySpan body, bool brotli)
+ {
+ var max = brotli
+ ? BrotliEncoder.GetMaxCompressedLength(body.Length)
+ : body.Length + (body.Length >> 2) + 64;
+
+ var scratch = _compressed;
+
+ if (scratch is null || scratch.Length < max)
+ {
+ _compressed = scratch = new byte[Math.Max(max, 32 * 1024)];
+ }
+
+ if (brotli)
+ {
+ BrotliEncoder.TryCompress(body, scratch, out var written, quality: 1, window: 22);
+ return scratch.AsSpan(0, written);
+ }
+
+ using var output = new MemoryStream(scratch, 0, scratch.Length, writable: true);
+
+ using (var gzip = new GZipStream(output, CompressionLevel.Fastest, leaveOpen: true))
+ {
+ gzip.Write(body);
+ }
+
+ return scratch.AsSpan(0, (int)output.Position);
+ }
+}
diff --git a/frameworks/touchsocket/Dockerfile b/frameworks/touchsocket/Dockerfile
new file mode 100644
index 000000000..6cf350feb
--- /dev/null
+++ b/frameworks/touchsocket/Dockerfile
@@ -0,0 +1,11 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /app
+COPY . .
+RUN dotnet publish -c Release -o out
+
+FROM mcr.microsoft.com/dotnet/runtime:10.0
+WORKDIR /app
+COPY --from=build /app/out .
+EXPOSE 8080
+
+ENTRYPOINT ["dotnet", "touchsocketarena.dll"]
diff --git a/frameworks/touchsocket/Program.cs b/frameworks/touchsocket/Program.cs
new file mode 100644
index 000000000..1d640cdd3
--- /dev/null
+++ b/frameworks/touchsocket/Program.cs
@@ -0,0 +1,29 @@
+using TouchSocketArena;
+
+using TouchSocket.Core;
+using TouchSocket.Http;
+using TouchSocket.Sockets;
+
+// ─────────────────────────────────────────────────────────────────────────────────────────────
+// touchsocket - TouchSocket.Http, the framework's HTTP/1.1 server component.
+//
+// Requests are answered from an IHttpPlugin, which is how TouchSocket exposes its HTTP pipeline:
+// the plugin sees each request, answers the ones it recognises and passes the rest along. h1
+// cleartext on :8080.
+// ─────────────────────────────────────────────────────────────────────────────────────────────
+
+var dataset = new Dataset();
+
+var port = int.TryParse(Environment.GetEnvironmentVariable("PORT"), out var p) ? p : 8080;
+
+var service = new HttpService();
+
+await service.SetupAsync(new TouchSocketConfig()
+ .SetListenIPHosts(port)
+ .ConfigurePlugins(a => a.Add(new ArenaPlugin(dataset))));
+
+await service.StartAsync();
+
+Console.WriteLine($"[touchsocket] :{port}, dataset={(dataset.IsAvailable ? dataset.Count + " items" : "absent")}");
+
+await Task.Delay(Timeout.Infinite);
diff --git a/frameworks/touchsocket/README.md b/frameworks/touchsocket/README.md
new file mode 100644
index 000000000..0b2585ba1
--- /dev/null
+++ b/frameworks/touchsocket/README.md
@@ -0,0 +1,57 @@
+# touchsocket
+
+[TouchSocket](https://github.com/RRQM/TouchSocket) — `TouchSocket.Http`, the HTTP/1.1 server
+component of the TouchSocket networking framework.
+
+Requests are answered from an `IHttpPlugin`, which is how TouchSocket exposes its HTTP pipeline:
+the plugin sees each request, answers the ones it recognises and passes the rest along. Dispatch is
+on the path's first segment — the component gives you the request, not a router to declare routes
+with.
+
+## Scope
+
+HTTP/1.1 cleartext on `:8080`, plus the async delay.
+
+| Profiles |
+|---|
+| baseline, pipelined, limited-conn, async, latency-1m, latency-10k, json-comp |
+
+## Notes
+
+- `/json` is serialized from the parsed model on **every** request and compressed only when the
+ client asked for it, at brotli quality 1. Nothing is answered from a precomputed body.
+- `/delay/{ms}` uses `Task.Delay`, which registers a timer and yields rather than holding a thread,
+ so waits in flight are bounded by memory.
+- The project is named `touchsocketarena` rather than `touchsocket`: a project named for the
+ package it depends on makes NuGet resolve a dependency cycle on itself (NU1108).
+
+## Status: disabled
+
+The entry is `enabled: false` pending a fix in TouchSocket.
+
+A request that arrives **split across TCP segments** *and* asks for `Connection: close` is answered
+with nothing: the socket is closed instead of the response being flushed. The same requests pass at
+every split offset when the connection is keep-alive, so it is the close path specifically, not
+request reassembly.
+
+Measured against `TouchSocket.Http` 4.3.6 on `.NET 10`, sweeping every split offset of one request:
+
+| request | result |
+|---------|--------|
+| `Connection: keep-alive` | 54/54 offsets answered correctly |
+| `Connection: close` | 20/73 offsets answered with nothing |
+
+Repro — no framework beyond TouchSocket is involved, and the handler does no parsing of its own:
+
+```python
+import socket, time
+req = b"GET /baseline11?a=13&b=42 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
+for split in (4, 34, 41):
+ s = socket.create_connection(("127.0.0.1", 8080))
+ s.sendall(req[:split]); time.sleep(0.01); s.sendall(req[split:])
+ print(split, s.recv(4096)[:40]) # b'' on the failing offsets
+ s.close()
+```
+
+The arena's fragmentation checks send exactly this shape, which is why they fail while ordinary
+`curl` traffic does not.
diff --git a/frameworks/touchsocket/Types.cs b/frameworks/touchsocket/Types.cs
new file mode 100644
index 000000000..49821955e
--- /dev/null
+++ b/frameworks/touchsocket/Types.cs
@@ -0,0 +1,48 @@
+using System.Text.Json.Serialization;
+
+namespace TouchSocketArena;
+
+/// An item as stored in the dataset file.
+public sealed class Item
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = "";
+ public string Category { get; set; } = "";
+ public int Price { get; set; }
+ public int Quantity { get; set; }
+ public bool Active { get; set; }
+ public List Tags { get; set; } = [];
+ public RatingInfo Rating { get; set; } = new();
+}
+
+/// A dataset item with the computed total the /json workload asks for.
+public sealed class ProcessedItem
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = "";
+ public string Category { get; set; } = "";
+ public int Price { get; set; }
+ public int Quantity { get; set; }
+ public bool Active { get; set; }
+ public List Tags { get; set; } = [];
+ public RatingInfo Rating { get; set; } = new();
+ public long Total { get; set; }
+}
+
+public sealed class RatingInfo
+{
+ public int Score { get; set; }
+ public int Count { get; set; }
+}
+
+public sealed record ItemsResponse(IReadOnlyList Items, int Count);
+
+// Source-generated so the JSON path costs no reflection at runtime, and the same
+// camelCase shape the other entries answer with.
+[JsonSerializable(typeof(ItemsResponse))]
+[JsonSerializable(typeof(List
- ))]
+[JsonSourceGenerationOptions(
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString)]
+public partial class AppJsonContext : JsonSerializerContext { }
diff --git a/frameworks/touchsocket/meta.json b/frameworks/touchsocket/meta.json
new file mode 100644
index 000000000..bcce1be87
--- /dev/null
+++ b/frameworks/touchsocket/meta.json
@@ -0,0 +1,22 @@
+{
+ "display_name": "touchsocket",
+ "language": "C#",
+ "type": "engine",
+ "engine": "touchsocket",
+ "description": "TouchSocket.Http, the HTTP/1.1 server component of the TouchSocket networking framework. Requests are answered from an IHttpPlugin, which is how TouchSocket exposes its HTTP pipeline: the plugin sees each request, answers the ones it recognises and passes the rest along. HTTP/1.1 cleartext on :8080. /json is serialized from the parsed model on every request and compressed only when the client asked for it, so nothing is answered from a precomputed body. Disabled pending a fix: a request that arrives split across TCP segments AND asks for Connection: close is answered with nothing - the socket is closed instead of the response being flushed. Fragmented requests with keep-alive pass at every offset, so it is the close path specifically.",
+ "repo": "https://github.com/RRQM/TouchSocket",
+ "enabled": false,
+ "tests": [
+ "baseline",
+ "pipelined",
+ "limited-conn",
+ "async",
+ "latency-1m",
+ "latency-10k",
+ "json-comp"
+ ],
+ "maintainers": [
+ "MDA2AV",
+ "RRQM"
+ ]
+}
diff --git a/frameworks/touchsocket/touchsocketarena.csproj b/frameworks/touchsocket/touchsocketarena.csproj
new file mode 100644
index 000000000..1d43aa65c
--- /dev/null
+++ b/frameworks/touchsocket/touchsocketarena.csproj
@@ -0,0 +1,15 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ true
+ true
+ touchsocketarena
+ TouchSocketArena
+
+
+
+
+