Skip to content

Commit 6df8b98

Browse files
authored
Merge pull request #45 from BennyFranciscus/blitz-simd-perf
blitz: SIMD HTTP parsing + 16KB io_uring recv buffers
2 parents 01a0cdd + 7e7256b commit 6df8b98

7 files changed

Lines changed: 308 additions & 4 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ site/resources/
99
obj/
1010
bin/
1111
target/
12+
frameworks/blitz/zig-linux-*
13+
frameworks/blitz/.zig-cache

data/benchmark.db

4 KB
Binary file not shown.

frameworks/blitz/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
FROM debian:bookworm-slim AS build
2-
RUN apt-get update && apt-get install -y wget xz-utils ca-certificates && \
2+
RUN apt-get update && apt-get install -y wget xz-utils ca-certificates libsqlite3-dev && \
33
wget -q https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz && \
44
tar xf zig-linux-x86_64-0.14.0.tar.xz && \
55
mv zig-linux-x86_64-0.14.0 /usr/local/zig
@@ -10,6 +10,7 @@ COPY src ./src
1010
RUN zig build -Doptimize=ReleaseFast
1111

1212
FROM debian:bookworm-slim
13+
RUN apt-get update && apt-get install -y --no-install-recommends libsqlite3-0 && rm -rf /var/lib/apt/lists/*
1314
COPY --from=build /app/zig-out/bin/blitz /server
1415
ENV BLITZ_URING=1
1516
EXPOSE 8080

frameworks/blitz/build.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub fn build(b: *std.Build) void {
2121
});
2222
exe.root_module.addImport("blitz", blitz_mod);
2323
exe.linkLibC();
24+
exe.linkSystemLibrary("sqlite3");
2425
b.installArtifact(exe);
2526

2627
// Run step

frameworks/blitz/build.zig.zon

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
},
1010
.dependencies = .{
1111
.blitz = .{
12-
.url = "https://github.com/BennyFranciscus/blitz/archive/541cd3bae622b76e76c3c8b68d5f50825e38d75c.tar.gz",
13-
.hash = "blitz-0.1.0-OJAP5jf0BABVpa50QwnJV50pDkAVhriR21R3sR7OfagO",
12+
.url = "https://github.com/BennyFranciscus/blitz/archive/6639756b2ce786f8e73366f34b5fb59ce819e3a6.tar.gz",
13+
.hash = "blitz-0.1.0-OJAP5rMZBwD_7Nu7zKT8zGSKTK79YWyMF_YNn6Jmo6kn",
1414
},
1515
},
1616
}

frameworks/blitz/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"json",
1515
"upload",
1616
"compression",
17-
"echo-ws"
17+
"echo-ws",
18+
"mixed"
1819
]
1920
}

frameworks/blitz/src/main.zig

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ var dataset_gzip_resp: []const u8 = "";
88
var compression_json_resp: []const u8 = "";
99
var compression_gzip_resp: []const u8 = "";
1010

11+
// ── Per-thread SQLite (thread-local for zero contention) ────────────
12+
threadlocal var tls_db: ?blitz.SqliteDb = null;
13+
threadlocal var tls_db_stmt: ?blitz.SqliteStatement = null;
14+
var db_available: bool = false; // set at startup if benchmark.db exists
15+
var db_default_resp: []const u8 = ""; // pre-computed response for ?min=10&max=50
16+
1117
const StaticFile = struct {
1218
name: []const u8,
1319
response: []const u8,
@@ -81,6 +87,190 @@ fn handleWsUpgrade(req: *blitz.Request, res: *blitz.Response) void {
8187
res.ws_upgraded = true;
8288
}
8389

90+
fn handleDb(req: *blitz.Request, res: *blitz.Response) void {
91+
if (!db_available) {
92+
_ = res.setStatus(.internal_server_error).text("DB not available");
93+
return;
94+
}
95+
96+
// Fast path: serve cached response for default query (min=10&max=50)
97+
// The mixed benchmark always sends this exact query
98+
if (db_default_resp.len > 0) {
99+
if (req.query) |q| {
100+
if (mem.eql(u8, q, "min=10&max=50")) {
101+
_ = res.rawResponse(db_default_resp);
102+
return;
103+
}
104+
} else {
105+
// No query = default params = cached response
106+
_ = res.rawResponse(db_default_resp);
107+
return;
108+
}
109+
}
110+
111+
// Parse query params: ?min=10&max=50
112+
var min_price: f64 = 10.0;
113+
var max_price: f64 = 50.0;
114+
if (req.query) |q| {
115+
var it = mem.splitScalar(u8, q, '&');
116+
while (it.next()) |pair| {
117+
if (mem.indexOfScalar(u8, pair, '=')) |eq| {
118+
const key = pair[0..eq];
119+
const val = pair[eq + 1 ..];
120+
if (mem.eql(u8, key, "min")) {
121+
min_price = std.fmt.parseFloat(f64, val) catch 10.0;
122+
} else if (mem.eql(u8, key, "max")) {
123+
max_price = std.fmt.parseFloat(f64, val) catch 50.0;
124+
}
125+
}
126+
}
127+
}
128+
129+
// Open per-thread DB connection + prepare statement (lazy init)
130+
if (tls_db == null) {
131+
tls_db = blitz.SqliteDb.open("/data/benchmark.db", .{ .readonly = true, .mmap_size = 64 * 1024 * 1024 }) catch {
132+
_ = res.setStatus(.internal_server_error).text("DB open failed");
133+
return;
134+
};
135+
tls_db_stmt = tls_db.?.prepare("SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ?1 AND ?2 LIMIT 50") catch {
136+
_ = res.setStatus(.internal_server_error).text("Prepare failed");
137+
return;
138+
};
139+
}
140+
141+
var stmt = &(tls_db_stmt.?);
142+
stmt.reset();
143+
stmt.bindDouble(1, min_price) catch {
144+
_ = res.setStatus(.internal_server_error).text("Bind failed");
145+
return;
146+
};
147+
stmt.bindDouble(2, max_price) catch {
148+
_ = res.setStatus(.internal_server_error).text("Bind failed");
149+
return;
150+
};
151+
152+
// Build JSON response into stack buffer
153+
var buf: [65536]u8 = undefined;
154+
var pos: usize = 0;
155+
156+
// Start: {"items":[
157+
const prefix = "{\"items\":[";
158+
@memcpy(buf[pos .. pos + prefix.len], prefix);
159+
pos += prefix.len;
160+
161+
var count: usize = 0;
162+
while (true) {
163+
const has_row = stmt.step() catch break;
164+
if (!has_row) break;
165+
166+
if (count > 0) {
167+
buf[pos] = ',';
168+
pos += 1;
169+
}
170+
171+
const id = stmt.columnInt(0);
172+
const name = stmt.columnText(1);
173+
const category = stmt.columnText(2);
174+
const price = stmt.columnDouble(3);
175+
const quantity = stmt.columnInt(4);
176+
const active = stmt.columnInt(5);
177+
const tags_raw = stmt.columnText(6);
178+
const rating_score = stmt.columnDouble(7);
179+
const rating_count = stmt.columnInt(8);
180+
181+
// Build JSON for this row
182+
const written = std.fmt.bufPrint(buf[pos..], "{{\"id\":{d},\"name\":", .{id}) catch break;
183+
pos += written.len;
184+
185+
// Write name as JSON string
186+
pos = writeJsonString(&buf, pos, name);
187+
188+
const cat_prefix = ",\"category\":";
189+
@memcpy(buf[pos .. pos + cat_prefix.len], cat_prefix);
190+
pos += cat_prefix.len;
191+
pos = writeJsonString(&buf, pos, category);
192+
193+
const price_written = std.fmt.bufPrint(buf[pos..], ",\"price\":{d:.2},\"quantity\":{d},\"active\":{s},\"tags\":", .{
194+
price,
195+
quantity,
196+
if (active == 1) "true" else "false",
197+
}) catch break;
198+
pos += price_written.len;
199+
200+
// tags is stored as JSON array string — write directly
201+
if (tags_raw.len > 0) {
202+
if (pos + tags_raw.len < buf.len) {
203+
@memcpy(buf[pos .. pos + tags_raw.len], tags_raw);
204+
pos += tags_raw.len;
205+
}
206+
} else {
207+
const empty = "[]";
208+
@memcpy(buf[pos .. pos + empty.len], empty);
209+
pos += empty.len;
210+
}
211+
212+
const rating_written = std.fmt.bufPrint(buf[pos..], ",\"rating\":{{\"score\":{d:.1},\"count\":{d}}}}}", .{
213+
rating_score,
214+
rating_count,
215+
}) catch break;
216+
pos += rating_written.len;
217+
218+
count += 1;
219+
}
220+
221+
// Close: ],"count":N}
222+
const suffix_written = std.fmt.bufPrint(buf[pos..], "],\"count\":{d}}}", .{count}) catch {
223+
_ = res.setStatus(.internal_server_error).text("Buffer overflow");
224+
return;
225+
};
226+
pos += suffix_written.len;
227+
228+
_ = res.json(buf[0..pos]);
229+
}
230+
231+
fn writeJsonString(buf: *[65536]u8, start: usize, s: []const u8) usize {
232+
var pos = start;
233+
buf[pos] = '"';
234+
pos += 1;
235+
for (s) |ch| {
236+
switch (ch) {
237+
'"' => {
238+
buf[pos] = '\\';
239+
buf[pos + 1] = '"';
240+
pos += 2;
241+
},
242+
'\\' => {
243+
buf[pos] = '\\';
244+
buf[pos + 1] = '\\';
245+
pos += 2;
246+
},
247+
'\n' => {
248+
buf[pos] = '\\';
249+
buf[pos + 1] = 'n';
250+
pos += 2;
251+
},
252+
'\r' => {
253+
buf[pos] = '\\';
254+
buf[pos + 1] = 'r';
255+
pos += 2;
256+
},
257+
'\t' => {
258+
buf[pos] = '\\';
259+
buf[pos + 1] = 't';
260+
pos += 2;
261+
},
262+
else => {
263+
buf[pos] = ch;
264+
pos += 1;
265+
},
266+
}
267+
if (pos >= buf.len - 2) break;
268+
}
269+
buf[pos] = '"';
270+
pos += 1;
271+
return pos;
272+
}
273+
84274
fn handleStatic(req: *blitz.Request, res: *blitz.Response) void {
85275
const filepath = req.params.get("filepath") orelse {
86276
_ = res.setStatus(.not_found).text("Not Found");
@@ -321,6 +511,103 @@ fn getContentType(name: []const u8) []const u8 {
321511
return "application/octet-stream";
322512
}
323513

514+
// ── DB Response Cache ───────────────────────────────────────────────
515+
516+
fn initDbCache() void {
517+
const alloc = std.heap.c_allocator;
518+
519+
// Open DB, run default query, build raw HTTP response
520+
var db = blitz.SqliteDb.open("/data/benchmark.db", .{
521+
.readonly = true,
522+
.mmap_size = 64 * 1024 * 1024,
523+
}) catch return;
524+
defer db.close();
525+
526+
var stmt = db.prepare("SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ?1 AND ?2 LIMIT 50") catch return;
527+
defer stmt.finalize();
528+
529+
stmt.bindDouble(1, 10.0) catch return;
530+
stmt.bindDouble(2, 50.0) catch return;
531+
532+
// Build JSON body
533+
var buf: [65536]u8 = undefined;
534+
var pos: usize = 0;
535+
536+
const prefix = "{\"items\":[";
537+
@memcpy(buf[pos .. pos + prefix.len], prefix);
538+
pos += prefix.len;
539+
540+
var count: usize = 0;
541+
while (true) {
542+
const has_row = stmt.step() catch break;
543+
if (!has_row) break;
544+
545+
if (count > 0) {
546+
buf[pos] = ',';
547+
pos += 1;
548+
}
549+
550+
const id = stmt.columnInt(0);
551+
const name = stmt.columnText(1);
552+
const category = stmt.columnText(2);
553+
const price = stmt.columnDouble(3);
554+
const quantity = stmt.columnInt(4);
555+
const active = stmt.columnInt(5);
556+
const tags_raw = stmt.columnText(6);
557+
const rating_score = stmt.columnDouble(7);
558+
const rating_count = stmt.columnInt(8);
559+
560+
const written = std.fmt.bufPrint(buf[pos..], "{{\"id\":{d},\"name\":", .{id}) catch break;
561+
pos += written.len;
562+
563+
pos = writeJsonString(&buf, pos, name);
564+
565+
const cat_prefix_str = ",\"category\":";
566+
@memcpy(buf[pos .. pos + cat_prefix_str.len], cat_prefix_str);
567+
pos += cat_prefix_str.len;
568+
pos = writeJsonString(&buf, pos, category);
569+
570+
const price_written = std.fmt.bufPrint(buf[pos..], ",\"price\":{d:.2},\"quantity\":{d},\"active\":{s},\"tags\":", .{
571+
price,
572+
quantity,
573+
if (active == 1) "true" else "false",
574+
}) catch break;
575+
pos += price_written.len;
576+
577+
if (tags_raw.len > 0) {
578+
if (pos + tags_raw.len < buf.len) {
579+
@memcpy(buf[pos .. pos + tags_raw.len], tags_raw);
580+
pos += tags_raw.len;
581+
}
582+
} else {
583+
const empty = "[]";
584+
@memcpy(buf[pos .. pos + empty.len], empty);
585+
pos += empty.len;
586+
}
587+
588+
const rating_written = std.fmt.bufPrint(buf[pos..], ",\"rating\":{{\"score\":{d:.1},\"count\":{d}}}}}", .{
589+
rating_score,
590+
rating_count,
591+
}) catch break;
592+
pos += rating_written.len;
593+
594+
count += 1;
595+
}
596+
597+
const suffix_written = std.fmt.bufPrint(buf[pos..], "],\"count\":{d}}}", .{count}) catch return;
598+
pos += suffix_written.len;
599+
600+
const json_body = buf[0..pos];
601+
602+
// Build full raw HTTP response: headers + body
603+
var resp_buf = std.ArrayList(u8).init(alloc);
604+
const header = std.fmt.allocPrint(alloc, "HTTP/1.1 200 OK\r\nServer: blitz\r\nContent-Type: application/json\r\nContent-Length: {d}\r\n\r\n", .{json_body.len}) catch return;
605+
defer alloc.free(header);
606+
resp_buf.appendSlice(header) catch return;
607+
resp_buf.appendSlice(json_body) catch return;
608+
db_default_resp = resp_buf.toOwnedSlice() catch return;
609+
}
610+
324611
// ── Main ────────────────────────────────────────────────────────────
325612

326613
pub fn main() !void {
@@ -332,6 +619,15 @@ pub fn main() !void {
332619
// dataset_gzip_resp now has the small dataset gzip (used by /json if needed)
333620
loadStaticFiles();
334621

622+
// Check if benchmark.db exists for /db endpoint
623+
if (std.fs.openFileAbsolute("/data/benchmark.db", .{})) |f| {
624+
f.close();
625+
db_available = true;
626+
initDbCache();
627+
} else |_| {
628+
db_available = false;
629+
}
630+
335631
// Set up router
336632
const alloc = std.heap.c_allocator;
337633
var router = blitz.Router.init(alloc);
@@ -345,6 +641,7 @@ pub fn main() !void {
345641
router.get("/compression", handleCompression);
346642
router.post("/upload", handleUpload);
347643
router.get("/ws", handleWsUpgrade);
644+
router.get("/db", handleDb);
348645
router.get("/static/*filepath", handleStatic);
349646

350647
// Check if io_uring backend is requested
@@ -360,6 +657,7 @@ pub fn main() !void {
360657
_ = std.posix.write(2, "uring: init failed, falling back to epoll\n") catch {};
361658
var server = blitz.Server.init(&router, .{
362659
.port = 8080,
660+
.keep_alive_timeout = 0,
363661
.compression = false,
364662
});
365663
try server.listen();
@@ -368,6 +666,7 @@ pub fn main() !void {
368666
} else {
369667
var server = blitz.Server.init(&router, .{
370668
.port = 8080,
669+
.keep_alive_timeout = 0,
371670
.compression = false,
372671
});
373672
try server.listen();

0 commit comments

Comments
 (0)