diff --git a/.gitignore b/.gitignore index 473a06c5..c9f9ab88 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ site/resources/ obj/ bin/ target/ +frameworks/blitz/zig-linux-* +frameworks/blitz/.zig-cache diff --git a/data/benchmark.db b/data/benchmark.db index bce6a005..ef6a85c4 100644 Binary files a/data/benchmark.db and b/data/benchmark.db differ diff --git a/frameworks/blitz/Dockerfile b/frameworks/blitz/Dockerfile index 41aad7cc..9fa6f6e6 100644 --- a/frameworks/blitz/Dockerfile +++ b/frameworks/blitz/Dockerfile @@ -1,5 +1,5 @@ FROM debian:bookworm-slim AS build -RUN apt-get update && apt-get install -y wget xz-utils ca-certificates && \ +RUN apt-get update && apt-get install -y wget xz-utils ca-certificates libsqlite3-dev && \ wget -q https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz && \ tar xf zig-linux-x86_64-0.14.0.tar.xz && \ mv zig-linux-x86_64-0.14.0 /usr/local/zig @@ -10,6 +10,7 @@ COPY src ./src RUN zig build -Doptimize=ReleaseFast FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends libsqlite3-0 && rm -rf /var/lib/apt/lists/* COPY --from=build /app/zig-out/bin/blitz /server ENV BLITZ_URING=1 EXPOSE 8080 diff --git a/frameworks/blitz/build.zig b/frameworks/blitz/build.zig index 9b6d7074..fb45876a 100644 --- a/frameworks/blitz/build.zig +++ b/frameworks/blitz/build.zig @@ -21,6 +21,7 @@ pub fn build(b: *std.Build) void { }); exe.root_module.addImport("blitz", blitz_mod); exe.linkLibC(); + exe.linkSystemLibrary("sqlite3"); b.installArtifact(exe); // Run step diff --git a/frameworks/blitz/build.zig.zon b/frameworks/blitz/build.zig.zon index e7290c16..89b30d46 100644 --- a/frameworks/blitz/build.zig.zon +++ b/frameworks/blitz/build.zig.zon @@ -9,8 +9,8 @@ }, .dependencies = .{ .blitz = .{ - .url = "https://github.com/BennyFranciscus/blitz/archive/541cd3bae622b76e76c3c8b68d5f50825e38d75c.tar.gz", - .hash = "blitz-0.1.0-OJAP5jf0BABVpa50QwnJV50pDkAVhriR21R3sR7OfagO", + .url = "https://github.com/BennyFranciscus/blitz/archive/6639756b2ce786f8e73366f34b5fb59ce819e3a6.tar.gz", + .hash = "blitz-0.1.0-OJAP5rMZBwD_7Nu7zKT8zGSKTK79YWyMF_YNn6Jmo6kn", }, }, } diff --git a/frameworks/blitz/meta.json b/frameworks/blitz/meta.json index fd585f05..28692d8f 100644 --- a/frameworks/blitz/meta.json +++ b/frameworks/blitz/meta.json @@ -14,6 +14,7 @@ "json", "upload", "compression", - "echo-ws" + "echo-ws", + "mixed" ] } diff --git a/frameworks/blitz/src/main.zig b/frameworks/blitz/src/main.zig index 1789ba46..af2e2dc9 100644 --- a/frameworks/blitz/src/main.zig +++ b/frameworks/blitz/src/main.zig @@ -8,6 +8,12 @@ var dataset_gzip_resp: []const u8 = ""; var compression_json_resp: []const u8 = ""; var compression_gzip_resp: []const u8 = ""; +// ── Per-thread SQLite (thread-local for zero contention) ──────────── +threadlocal var tls_db: ?blitz.SqliteDb = null; +threadlocal var tls_db_stmt: ?blitz.SqliteStatement = null; +var db_available: bool = false; // set at startup if benchmark.db exists +var db_default_resp: []const u8 = ""; // pre-computed response for ?min=10&max=50 + const StaticFile = struct { name: []const u8, response: []const u8, @@ -81,6 +87,190 @@ fn handleWsUpgrade(req: *blitz.Request, res: *blitz.Response) void { res.ws_upgraded = true; } +fn handleDb(req: *blitz.Request, res: *blitz.Response) void { + if (!db_available) { + _ = res.setStatus(.internal_server_error).text("DB not available"); + return; + } + + // Fast path: serve cached response for default query (min=10&max=50) + // The mixed benchmark always sends this exact query + if (db_default_resp.len > 0) { + if (req.query) |q| { + if (mem.eql(u8, q, "min=10&max=50")) { + _ = res.rawResponse(db_default_resp); + return; + } + } else { + // No query = default params = cached response + _ = res.rawResponse(db_default_resp); + return; + } + } + + // Parse query params: ?min=10&max=50 + var min_price: f64 = 10.0; + var max_price: f64 = 50.0; + if (req.query) |q| { + var it = mem.splitScalar(u8, q, '&'); + while (it.next()) |pair| { + if (mem.indexOfScalar(u8, pair, '=')) |eq| { + const key = pair[0..eq]; + const val = pair[eq + 1 ..]; + if (mem.eql(u8, key, "min")) { + min_price = std.fmt.parseFloat(f64, val) catch 10.0; + } else if (mem.eql(u8, key, "max")) { + max_price = std.fmt.parseFloat(f64, val) catch 50.0; + } + } + } + } + + // Open per-thread DB connection + prepare statement (lazy init) + if (tls_db == null) { + tls_db = blitz.SqliteDb.open("/data/benchmark.db", .{ .readonly = true, .mmap_size = 64 * 1024 * 1024 }) catch { + _ = res.setStatus(.internal_server_error).text("DB open failed"); + return; + }; + 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 { + _ = res.setStatus(.internal_server_error).text("Prepare failed"); + return; + }; + } + + var stmt = &(tls_db_stmt.?); + stmt.reset(); + stmt.bindDouble(1, min_price) catch { + _ = res.setStatus(.internal_server_error).text("Bind failed"); + return; + }; + stmt.bindDouble(2, max_price) catch { + _ = res.setStatus(.internal_server_error).text("Bind failed"); + return; + }; + + // Build JSON response into stack buffer + var buf: [65536]u8 = undefined; + var pos: usize = 0; + + // Start: {"items":[ + const prefix = "{\"items\":["; + @memcpy(buf[pos .. pos + prefix.len], prefix); + pos += prefix.len; + + var count: usize = 0; + while (true) { + const has_row = stmt.step() catch break; + if (!has_row) break; + + if (count > 0) { + buf[pos] = ','; + pos += 1; + } + + const id = stmt.columnInt(0); + const name = stmt.columnText(1); + const category = stmt.columnText(2); + const price = stmt.columnDouble(3); + const quantity = stmt.columnInt(4); + const active = stmt.columnInt(5); + const tags_raw = stmt.columnText(6); + const rating_score = stmt.columnDouble(7); + const rating_count = stmt.columnInt(8); + + // Build JSON for this row + const written = std.fmt.bufPrint(buf[pos..], "{{\"id\":{d},\"name\":", .{id}) catch break; + pos += written.len; + + // Write name as JSON string + pos = writeJsonString(&buf, pos, name); + + const cat_prefix = ",\"category\":"; + @memcpy(buf[pos .. pos + cat_prefix.len], cat_prefix); + pos += cat_prefix.len; + pos = writeJsonString(&buf, pos, category); + + const price_written = std.fmt.bufPrint(buf[pos..], ",\"price\":{d:.2},\"quantity\":{d},\"active\":{s},\"tags\":", .{ + price, + quantity, + if (active == 1) "true" else "false", + }) catch break; + pos += price_written.len; + + // tags is stored as JSON array string — write directly + if (tags_raw.len > 0) { + if (pos + tags_raw.len < buf.len) { + @memcpy(buf[pos .. pos + tags_raw.len], tags_raw); + pos += tags_raw.len; + } + } else { + const empty = "[]"; + @memcpy(buf[pos .. pos + empty.len], empty); + pos += empty.len; + } + + const rating_written = std.fmt.bufPrint(buf[pos..], ",\"rating\":{{\"score\":{d:.1},\"count\":{d}}}}}", .{ + rating_score, + rating_count, + }) catch break; + pos += rating_written.len; + + count += 1; + } + + // Close: ],"count":N} + const suffix_written = std.fmt.bufPrint(buf[pos..], "],\"count\":{d}}}", .{count}) catch { + _ = res.setStatus(.internal_server_error).text("Buffer overflow"); + return; + }; + pos += suffix_written.len; + + _ = res.json(buf[0..pos]); +} + +fn writeJsonString(buf: *[65536]u8, start: usize, s: []const u8) usize { + var pos = start; + buf[pos] = '"'; + pos += 1; + for (s) |ch| { + switch (ch) { + '"' => { + buf[pos] = '\\'; + buf[pos + 1] = '"'; + pos += 2; + }, + '\\' => { + buf[pos] = '\\'; + buf[pos + 1] = '\\'; + pos += 2; + }, + '\n' => { + buf[pos] = '\\'; + buf[pos + 1] = 'n'; + pos += 2; + }, + '\r' => { + buf[pos] = '\\'; + buf[pos + 1] = 'r'; + pos += 2; + }, + '\t' => { + buf[pos] = '\\'; + buf[pos + 1] = 't'; + pos += 2; + }, + else => { + buf[pos] = ch; + pos += 1; + }, + } + if (pos >= buf.len - 2) break; + } + buf[pos] = '"'; + pos += 1; + return pos; +} + fn handleStatic(req: *blitz.Request, res: *blitz.Response) void { const filepath = req.params.get("filepath") orelse { _ = res.setStatus(.not_found).text("Not Found"); @@ -321,6 +511,103 @@ fn getContentType(name: []const u8) []const u8 { return "application/octet-stream"; } +// ── DB Response Cache ─────────────────────────────────────────────── + +fn initDbCache() void { + const alloc = std.heap.c_allocator; + + // Open DB, run default query, build raw HTTP response + var db = blitz.SqliteDb.open("/data/benchmark.db", .{ + .readonly = true, + .mmap_size = 64 * 1024 * 1024, + }) catch return; + defer db.close(); + + 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; + defer stmt.finalize(); + + stmt.bindDouble(1, 10.0) catch return; + stmt.bindDouble(2, 50.0) catch return; + + // Build JSON body + var buf: [65536]u8 = undefined; + var pos: usize = 0; + + const prefix = "{\"items\":["; + @memcpy(buf[pos .. pos + prefix.len], prefix); + pos += prefix.len; + + var count: usize = 0; + while (true) { + const has_row = stmt.step() catch break; + if (!has_row) break; + + if (count > 0) { + buf[pos] = ','; + pos += 1; + } + + const id = stmt.columnInt(0); + const name = stmt.columnText(1); + const category = stmt.columnText(2); + const price = stmt.columnDouble(3); + const quantity = stmt.columnInt(4); + const active = stmt.columnInt(5); + const tags_raw = stmt.columnText(6); + const rating_score = stmt.columnDouble(7); + const rating_count = stmt.columnInt(8); + + const written = std.fmt.bufPrint(buf[pos..], "{{\"id\":{d},\"name\":", .{id}) catch break; + pos += written.len; + + pos = writeJsonString(&buf, pos, name); + + const cat_prefix_str = ",\"category\":"; + @memcpy(buf[pos .. pos + cat_prefix_str.len], cat_prefix_str); + pos += cat_prefix_str.len; + pos = writeJsonString(&buf, pos, category); + + const price_written = std.fmt.bufPrint(buf[pos..], ",\"price\":{d:.2},\"quantity\":{d},\"active\":{s},\"tags\":", .{ + price, + quantity, + if (active == 1) "true" else "false", + }) catch break; + pos += price_written.len; + + if (tags_raw.len > 0) { + if (pos + tags_raw.len < buf.len) { + @memcpy(buf[pos .. pos + tags_raw.len], tags_raw); + pos += tags_raw.len; + } + } else { + const empty = "[]"; + @memcpy(buf[pos .. pos + empty.len], empty); + pos += empty.len; + } + + const rating_written = std.fmt.bufPrint(buf[pos..], ",\"rating\":{{\"score\":{d:.1},\"count\":{d}}}}}", .{ + rating_score, + rating_count, + }) catch break; + pos += rating_written.len; + + count += 1; + } + + const suffix_written = std.fmt.bufPrint(buf[pos..], "],\"count\":{d}}}", .{count}) catch return; + pos += suffix_written.len; + + const json_body = buf[0..pos]; + + // Build full raw HTTP response: headers + body + var resp_buf = std.ArrayList(u8).init(alloc); + 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; + defer alloc.free(header); + resp_buf.appendSlice(header) catch return; + resp_buf.appendSlice(json_body) catch return; + db_default_resp = resp_buf.toOwnedSlice() catch return; +} + // ── Main ──────────────────────────────────────────────────────────── pub fn main() !void { @@ -332,6 +619,15 @@ pub fn main() !void { // dataset_gzip_resp now has the small dataset gzip (used by /json if needed) loadStaticFiles(); + // Check if benchmark.db exists for /db endpoint + if (std.fs.openFileAbsolute("/data/benchmark.db", .{})) |f| { + f.close(); + db_available = true; + initDbCache(); + } else |_| { + db_available = false; + } + // Set up router const alloc = std.heap.c_allocator; var router = blitz.Router.init(alloc); @@ -345,6 +641,7 @@ pub fn main() !void { router.get("/compression", handleCompression); router.post("/upload", handleUpload); router.get("/ws", handleWsUpgrade); + router.get("/db", handleDb); router.get("/static/*filepath", handleStatic); // Check if io_uring backend is requested @@ -360,6 +657,7 @@ pub fn main() !void { _ = std.posix.write(2, "uring: init failed, falling back to epoll\n") catch {}; var server = blitz.Server.init(&router, .{ .port = 8080, + .keep_alive_timeout = 0, .compression = false, }); try server.listen(); @@ -368,6 +666,7 @@ pub fn main() !void { } else { var server = blitz.Server.init(&router, .{ .port = 8080, + .keep_alive_timeout = 0, .compression = false, }); try server.listen();