Skip to content

Commit 566ec6f

Browse files
committed
genhttp
1 parent 6ef71c0 commit 566ec6f

79 files changed

Lines changed: 1200 additions & 249 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frameworks/genhttp/Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM mcr.microsoft.com/dotnet/sdk:10.0-preview-alpine AS build
2+
WORKDIR /source
3+
COPY genhttp.csproj .
4+
RUN dotnet restore -r linux-musl-x64
5+
COPY Program.cs .
6+
RUN dotnet publish -c Release -r linux-musl-x64 --self-contained -o /app
7+
8+
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-preview-alpine
9+
WORKDIR /app
10+
COPY --from=build /app .
11+
RUN apk add --no-cache libmsquic
12+
EXPOSE 8080 8443/tcp 8443/udp
13+
ENTRYPOINT ["./genhttp"]

frameworks/genhttp/Program.cs

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
using System.Net;
2+
using System.Security.Cryptography.X509Certificates;
3+
using System.Text.Json;
4+
using GenHTTP.Api.Content;
5+
using GenHTTP.Api.Protocol;
6+
using GenHTTP.Engine.Kestrel;
7+
using GenHTTP.Modules.Compression;
8+
using GenHTTP.Modules.Functional;
9+
using GenHTTP.Modules.IO;
10+
using GenHTTP.Modules.Layouting;
11+
using Microsoft.Data.Sqlite;
12+
13+
// JSON options
14+
var jsonOptions = new JsonSerializerOptions
15+
{
16+
PropertyNameCaseInsensitive = true,
17+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
18+
};
19+
20+
// Load small dataset — keep raw items for per-request processing
21+
var datasetPath = Environment.GetEnvironmentVariable("DATASET_PATH") ?? "/data/dataset.json";
22+
List<DatasetItem>? datasetItems = null;
23+
if (File.Exists(datasetPath))
24+
{
25+
datasetItems = JsonSerializer.Deserialize<List<DatasetItem>>(File.ReadAllText(datasetPath), jsonOptions);
26+
}
27+
28+
// Load large dataset for compression — pre-serialize to bytes
29+
byte[]? largeJsonBytes = null;
30+
var largePath = "/data/dataset-large.json";
31+
if (File.Exists(largePath))
32+
{
33+
var largeItems = JsonSerializer.Deserialize<List<DatasetItem>>(File.ReadAllText(largePath), jsonOptions);
34+
if (largeItems != null)
35+
{
36+
var processed = largeItems.Select(d => new ProcessedItem
37+
{
38+
Id = d.Id, Name = d.Name, Category = d.Category,
39+
Price = d.Price, Quantity = d.Quantity, Active = d.Active,
40+
Tags = d.Tags, Rating = d.Rating,
41+
Total = Math.Round(d.Price * d.Quantity, 2)
42+
}).ToList();
43+
largeJsonBytes = JsonSerializer.SerializeToUtf8Bytes(new { items = processed, count = processed.Count }, jsonOptions);
44+
}
45+
}
46+
47+
// Pre-load static files
48+
var staticFileMap = new Dictionary<string, (byte[] Data, string ContentType)>();
49+
var staticDir = "/data/static";
50+
if (Directory.Exists(staticDir))
51+
{
52+
var mimeTypes = new Dictionary<string, string>
53+
{
54+
{".css", "text/css"}, {".js", "application/javascript"}, {".html", "text/html"},
55+
{".woff2", "font/woff2"}, {".svg", "image/svg+xml"}, {".webp", "image/webp"}, {".json", "application/json"}
56+
};
57+
foreach (var file in Directory.GetFiles(staticDir))
58+
{
59+
var name = Path.GetFileName(file);
60+
var ext = Path.GetExtension(file);
61+
var ct = mimeTypes.GetValueOrDefault(ext, "application/octet-stream");
62+
staticFileMap[name] = (File.ReadAllBytes(file), ct);
63+
}
64+
}
65+
66+
// Open SQLite database
67+
SqliteConnection? dbConn = null;
68+
var dbPath = "/data/benchmark.db";
69+
if (File.Exists(dbPath))
70+
{
71+
dbConn = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
72+
dbConn.Open();
73+
using var pragma = dbConn.CreateCommand();
74+
pragma.CommandText = "PRAGMA mmap_size=268435456";
75+
pragma.ExecuteNonQuery();
76+
}
77+
78+
// Helper: sum query parameters
79+
static int SumQuery(IRequest request)
80+
{
81+
int sum = 0;
82+
foreach (var (_, value) in request.Query)
83+
{
84+
if (int.TryParse(value, out int n))
85+
sum += n;
86+
}
87+
return sum;
88+
}
89+
90+
// Helper: build a response from a byte array
91+
static IResponse ByteResponse(IRequest request, byte[] data, string contentType)
92+
{
93+
return request.Respond()
94+
.Content(new MemoryStream(data))
95+
.Type(new FlexibleContentType(contentType))
96+
.Length((ulong)data.Length)
97+
.Header("Server", "genhttp")
98+
.Build();
99+
}
100+
101+
// Build the handler tree
102+
var api = Inline.Create()
103+
.Get("/pipeline", (IRequest request) =>
104+
{
105+
return request.Respond()
106+
.Content(Resource.FromString("ok").Build())
107+
.Type(new FlexibleContentType("text/plain"))
108+
.Header("Server", "genhttp")
109+
.Build();
110+
})
111+
.Get("/baseline11", (IRequest request) =>
112+
{
113+
int sum = SumQuery(request);
114+
return request.Respond()
115+
.Content(Resource.FromString(sum.ToString()).Build())
116+
.Type(new FlexibleContentType("text/plain"))
117+
.Header("Server", "genhttp")
118+
.Build();
119+
})
120+
.Post("/baseline11", async (IRequest request) =>
121+
{
122+
int sum = SumQuery(request);
123+
if (request.Content != null)
124+
{
125+
using var reader = new StreamReader(request.Content);
126+
var body = await reader.ReadToEndAsync();
127+
if (int.TryParse(body.Trim(), out int b))
128+
sum += b;
129+
}
130+
return request.Respond()
131+
.Content(Resource.FromString(sum.ToString()).Build())
132+
.Type(new FlexibleContentType("text/plain"))
133+
.Header("Server", "genhttp")
134+
.Build();
135+
})
136+
.Get("/baseline2", (IRequest request) =>
137+
{
138+
int sum = SumQuery(request);
139+
return request.Respond()
140+
.Content(Resource.FromString(sum.ToString()).Build())
141+
.Type(new FlexibleContentType("text/plain"))
142+
.Header("Server", "genhttp")
143+
.Build();
144+
})
145+
.Get("/json", (IRequest request) =>
146+
{
147+
if (datasetItems == null)
148+
return request.Respond().Status(500, "No dataset").Build();
149+
var processed = new List<ProcessedItem>(datasetItems.Count);
150+
foreach (var d in datasetItems)
151+
{
152+
processed.Add(new ProcessedItem
153+
{
154+
Id = d.Id, Name = d.Name, Category = d.Category,
155+
Price = d.Price, Quantity = d.Quantity, Active = d.Active,
156+
Tags = d.Tags, Rating = d.Rating,
157+
Total = Math.Round(d.Price * d.Quantity, 2)
158+
});
159+
}
160+
var json = JsonSerializer.Serialize(new { items = processed, count = processed.Count }, jsonOptions);
161+
return request.Respond()
162+
.Content(Resource.FromString(json).Build())
163+
.Type(new FlexibleContentType("application/json"))
164+
.Header("Server", "genhttp")
165+
.Build();
166+
})
167+
.Get("/compression", (IRequest request) =>
168+
{
169+
if (largeJsonBytes == null)
170+
return request.Respond().Status(500, "No dataset").Build();
171+
return ByteResponse(request, largeJsonBytes, "application/json");
172+
})
173+
.Post("/upload", async (IRequest request) =>
174+
{
175+
using var ms = new MemoryStream();
176+
if (request.Content != null)
177+
await request.Content.CopyToAsync(ms);
178+
uint crc = Crc32Helper.Compute(ms.GetBuffer().AsSpan(0, (int)ms.Length));
179+
var hex = crc.ToString("x8");
180+
return request.Respond()
181+
.Content(Resource.FromString(hex).Build())
182+
.Type(new FlexibleContentType("text/plain"))
183+
.Header("Server", "genhttp")
184+
.Build();
185+
})
186+
.Get("/db", (IRequest request) =>
187+
{
188+
if (dbConn == null)
189+
return request.Respond().Status(500, "DB not available").Build();
190+
191+
double min = 10, max = 50;
192+
if (request.Query.TryGetValue("min", out var minStr) && double.TryParse(minStr, out double pmin))
193+
min = pmin;
194+
if (request.Query.TryGetValue("max", out var maxStr) && double.TryParse(maxStr, out double pmax))
195+
max = pmax;
196+
197+
using var cmd = dbConn.CreateCommand();
198+
cmd.CommandText = "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN @min AND @max LIMIT 50";
199+
cmd.Parameters.AddWithValue("@min", min);
200+
cmd.Parameters.AddWithValue("@max", max);
201+
using var reader = cmd.ExecuteReader();
202+
var items = new List<object>();
203+
while (reader.Read())
204+
{
205+
items.Add(new
206+
{
207+
id = reader.GetInt32(0),
208+
name = reader.GetString(1),
209+
category = reader.GetString(2),
210+
price = reader.GetDouble(3),
211+
quantity = reader.GetInt32(4),
212+
active = reader.GetInt32(5) == 1,
213+
tags = JsonSerializer.Deserialize<List<string>>(reader.GetString(6)),
214+
rating = new { score = reader.GetDouble(7), count = reader.GetInt32(8) },
215+
});
216+
}
217+
var json = JsonSerializer.Serialize(new { items, count = items.Count }, jsonOptions);
218+
return request.Respond()
219+
.Content(Resource.FromString(json).Build())
220+
.Type(new FlexibleContentType("application/json"))
221+
.Header("Server", "genhttp")
222+
.Build();
223+
});
224+
225+
// Static file handler — register each file as a sub-route in a layout
226+
var staticLayout = Layout.Create();
227+
foreach (var (name, (data, contentType)) in staticFileMap)
228+
{
229+
var fileData = data;
230+
var fileContentType = contentType;
231+
staticLayout.Add(name, Inline.Create()
232+
.Get((IRequest request) => ByteResponse(request, fileData, fileContentType)));
233+
}
234+
235+
var layout = Layout.Create()
236+
.Add(api)
237+
.Add("static", staticLayout);
238+
239+
// TLS configuration
240+
var certPath = Environment.GetEnvironmentVariable("TLS_CERT") ?? "/certs/server.crt";
241+
var keyPath = Environment.GetEnvironmentVariable("TLS_KEY") ?? "/certs/server.key";
242+
var hasCert = File.Exists(certPath) && File.Exists(keyPath);
243+
244+
var host = Host.Create()
245+
.Handler(layout)
246+
.Compression(CompressedContent.Default());
247+
248+
host.Bind(IPAddress.Any, 8080);
249+
250+
if (hasCert)
251+
{
252+
var cert = X509Certificate2.CreateFromPemFile(certPath, keyPath);
253+
host.Bind(IPAddress.Any, 8443, cert, enableQuic: true);
254+
}
255+
256+
await host.RunAsync();
257+
258+
// --- Data models ---
259+
260+
class DatasetItem
261+
{
262+
public int Id { get; set; }
263+
public string Name { get; set; } = "";
264+
public string Category { get; set; } = "";
265+
public double Price { get; set; }
266+
public int Quantity { get; set; }
267+
public bool Active { get; set; }
268+
public List<string> Tags { get; set; } = new();
269+
public RatingInfo Rating { get; set; } = new();
270+
}
271+
272+
class ProcessedItem
273+
{
274+
public int Id { get; set; }
275+
public string Name { get; set; } = "";
276+
public string Category { get; set; } = "";
277+
public double Price { get; set; }
278+
public int Quantity { get; set; }
279+
public bool Active { get; set; }
280+
public List<string> Tags { get; set; } = new();
281+
public RatingInfo Rating { get; set; } = new();
282+
public double Total { get; set; }
283+
}
284+
285+
class RatingInfo
286+
{
287+
public double Score { get; set; }
288+
public int Count { get; set; }
289+
}
290+
291+
static class Crc32Helper
292+
{
293+
private static readonly uint[][] T = new uint[8][];
294+
static Crc32Helper()
295+
{
296+
for (int s = 0; s < 8; s++) T[s] = new uint[256];
297+
for (uint i = 0; i < 256; i++)
298+
{
299+
uint c = i;
300+
for (int j = 0; j < 8; j++)
301+
c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1u)));
302+
T[0][i] = c;
303+
}
304+
for (uint i = 0; i < 256; i++)
305+
for (int s = 1; s < 8; s++)
306+
T[s][i] = (T[s-1][i] >> 8) ^ T[0][T[s-1][i] & 0xFF];
307+
}
308+
public static uint Compute(ReadOnlySpan<byte> data)
309+
{
310+
uint crc = 0xFFFFFFFF;
311+
int i = 0;
312+
while (i + 8 <= data.Length)
313+
{
314+
uint a = (uint)(data[i] | (data[i+1] << 8) | (data[i+2] << 16) | (data[i+3] << 24)) ^ crc;
315+
uint b = (uint)(data[i+4] | (data[i+5] << 8) | (data[i+6] << 16) | (data[i+7] << 24));
316+
crc = T[7][a & 0xFF] ^ T[6][(a >> 8) & 0xFF]
317+
^ T[5][(a >> 16) & 0xFF] ^ T[4][a >> 24]
318+
^ T[3][b & 0xFF] ^ T[2][(b >> 8) & 0xFF]
319+
^ T[1][(b >> 16) & 0xFF] ^ T[0][b >> 24];
320+
i += 8;
321+
}
322+
while (i < data.Length)
323+
crc = (crc >> 8) ^ T[0][(crc ^ data[i++]) & 0xFF];
324+
return crc ^ 0xFFFFFFFF;
325+
}
326+
}

frameworks/genhttp/genhttp.csproj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<ServerGarbageCollection>true</ServerGarbageCollection>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<PackageReference Include="GenHTTP.Core.Kestrel" Version="10.5.0" />
10+
<PackageReference Include="GenHTTP.Modules.Functional" Version="10.5.0" />
11+
<PackageReference Include="GenHTTP.Modules.IO" Version="10.5.0" />
12+
<PackageReference Include="GenHTTP.Modules.Layouting" Version="10.5.0" />
13+
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.3" />
14+
</ItemGroup>
15+
</Project>

frameworks/genhttp/meta.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"display_name": "genhttp",
3+
"language": "C#",
4+
"type": "realistic",
5+
"engine": "Kestrel",
6+
"description": "Lightweight embeddable C# web server using the Kestrel engine for HTTP/1.1, HTTP/2, and HTTP/3 support.",
7+
"repo": "https://github.com/Kaliumhexacyanoferrat/GenHTTP",
8+
"enabled": true,
9+
"tests": [
10+
"baseline",
11+
"pipelined",
12+
"limited-conn",
13+
"json",
14+
"upload",
15+
"compression",
16+
"noisy",
17+
"mixed",
18+
"baseline-h2",
19+
"static-h2",
20+
"baseline-h3",
21+
"static-h3"
22+
]
23+
}

frameworks/nginx/nginx.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ http {
4545
ssl_session_cache shared:SSL:10m;
4646

4747
http2_max_concurrent_streams 256;
48-
http2_max_requests 10000000;
48+
keepalive_requests 10000000;
4949

5050
add_header Alt-Svc 'h3=":8443"; ma=86400';
5151

0 commit comments

Comments
 (0)