Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions frameworks/zeemo-tuned/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.zig-cache/
zig-out/
zig-pkg/
*.bak
34 changes: 34 additions & 0 deletions frameworks/zeemo-tuned/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
FROM ubuntu:24.04 AS build
ARG ZIG_VERSION=0.16.0
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl xz-utils \
libssl-dev pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Architecture comes from `uname -m` so the image matches the host arch.
RUN set -eu; \
ZIG_ARCH="$(uname -m)"; \
case "${ZIG_ARCH}" in \
x86_64|aarch64) ;; \
*) echo "unsupported arch: ${ZIG_ARCH}" >&2; exit 1 ;; \
esac; \
curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}.tar.xz" | tar -xJ -C /opt; \
mv "/opt/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}" /opt/zig
ENV PATH="/opt/zig:${PATH}"

WORKDIR /src
COPY build.zig build.zig.zon ./
COPY src ./src
# Native build (glibc + dynamic libssl/libcrypto). musl-static would need a
# from-source OpenSSL build; the runtime image carries libssl3 instead.
RUN zig build --release=fast

# Runtime: Ubuntu slim so libssl3 is present without extra setup. HttpArena
# mounts /data (dataset + static) and /certs (TLS cert + key) when running.
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends \
libssl3 ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /src/zig-out/bin/zeemo-tuned /zeemo-tuned
EXPOSE 8080 8081
ENTRYPOINT ["/zeemo-tuned"]
24 changes: 24 additions & 0 deletions frameworks/zeemo-tuned/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const std = @import("std");

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });

const zeemo_dep = b.dependency("zeemo", .{
.target = target,
});

const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.strip = true,
});
exe_mod.addImport("zeemo", zeemo_dep.module("zeemo"));

const exe = b.addExecutable(.{
.name = "zeemo-tuned",
.root_module = exe_mod,
});
b.installArtifact(exe);
}
48 changes: 48 additions & 0 deletions frameworks/zeemo-tuned/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = .zeemo_tuned,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
// Together with name, this represents a globally unique package
// identifier. This field is generated by the Zig toolchain when the
// package is first created, and then *never changes*. This allows
// unambiguous detection of one package being an updated version of
// another.
//
// When forking a Zig project, this id should be regenerated (delete the
// field and run `zig build`) if the upstream project is still maintained.
// Otherwise, the fork is *hostile*, attempting to take control over the
// original project's identity. Thus it is recommended to leave the comment
// on the following line intact, so that it shows up in code reviews that
// modify the field.
.fingerprint = 0xdd574928a6deee1b, // Changing this has security and trust implications.
// Tracks the earliest Zig version that the package considers to be a
// supported use case.
.minimum_zig_version = "0.16.0",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
.zeemo = .{
.url = "https://github.com/skylightis666/zeemo/archive/8b47c8e7f8387f7f05c08991df8f4efbd12580c8.tar.gz",
.hash = "zeemo-0.1.0-Mq5M4ebxAQCqKgmq_4VN2Hx4S4Gh7_1Z5WjRnVPW4PM7",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}
20 changes: 20 additions & 0 deletions frameworks/zeemo-tuned/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"display_name": "zeemo-tuned",
"language": "Zig",
"engine": "io_uring",
"type": "tuned",
"description": "First Zig tuned entry. Built on the zeemo Zig HTTP library (io_uring transport, multishot accept, fork-workers with SO_REUSEPORT). JSON responses are serialized per request from native struct data via zeemo's comptime serializer — no pre-rendered fragments. Static files are loaded into memory at startup and pre-baked into the full HTTP response per encoding variant.",
"repo": "https://github.com/skylightis666/zeemo",
"enabled": true,
"tests": [
"baseline",
"pipelined",
"limited-conn",
"json",
"json-comp",
"json-tls",
"static",
"upload"
],
"maintainers": ["skylightis666"]
}
104 changes: 104 additions & 0 deletions frameworks/zeemo-tuned/src/dataset.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! Dataset loader for the HttpArena `json` profile. Stores items as
//! native structs and lets the response handler serialize them per
//! request via zeemo's generic comptime serializer. No pre-rendered
//! response fragments — the `tuned` rules ban that shortcut.

const std = @import("std");
const linux = std.os.linux;

pub const Rating = struct {
score: i32,
count: i32,
};

pub const Item = struct {
id: i32,
name: []const u8,
category: []const u8,
price: i32,
quantity: i32,
active: bool,
tags: []const []const u8,
rating: Rating,
/// Computed per request as `price * quantity * m`. Stored on the value
/// the handler hands to the serializer so it lands as the last field.
total: i64 = 0,
};

pub const Dataset = struct {
items: []Item,
arena: std.heap.ArenaAllocator,

pub fn deinit(self: *Dataset) void {
self.arena.deinit();
}
};

pub fn load(gpa: std.mem.Allocator, path: []const u8) !Dataset {
var arena = std.heap.ArenaAllocator.init(gpa);
errdefer arena.deinit();
const aa = arena.allocator();

const raw = try readFileAlloc(aa, path, 4 * 1024 * 1024);
var parsed = try std.json.parseFromSlice(std.json.Value, aa, raw, .{});
defer parsed.deinit();

const arr = switch (parsed.value) {
.array => |a| a,
else => return error.BadDataset,
};

const items = try aa.alloc(Item, arr.items.len);
for (arr.items, 0..) |elem, i| {
const obj = switch (elem) {
.object => |o| o,
else => return error.BadDataset,
};
const tags_v = obj.get("tags") orelse return error.BadDataset;
const tags_arr = switch (tags_v) {
.array => |a| a,
else => return error.BadDataset,
};
const tags = try aa.alloc([]const u8, tags_arr.items.len);
for (tags_arr.items, 0..) |t, k| tags[k] = try aa.dupe(u8, t.string);
const rating_v = obj.get("rating") orelse return error.BadDataset;
const rating_obj = switch (rating_v) {
.object => |o| o,
else => return error.BadDataset,
};

items[i] = .{
.id = @intCast(obj.get("id").?.integer),
.name = try aa.dupe(u8, obj.get("name").?.string),
.category = try aa.dupe(u8, obj.get("category").?.string),
.price = @intCast(obj.get("price").?.integer),
.quantity = @intCast(obj.get("quantity").?.integer),
.active = obj.get("active").?.bool,
.tags = tags,
.rating = .{
.score = @intCast(rating_obj.get("score").?.integer),
.count = @intCast(rating_obj.get("count").?.integer),
},
};
}
return .{ .items = items, .arena = arena };
}

fn readFileAlloc(aa: std.mem.Allocator, path: []const u8, max: usize) ![]u8 {
var path_z: [std.posix.PATH_MAX]u8 = undefined;
if (path.len >= path_z.len) return error.NameTooLong;
@memcpy(path_z[0..path.len], path);
path_z[path.len] = 0;
const fd = try std.posix.openatZ(std.posix.AT.FDCWD, @ptrCast(&path_z), .{ .ACCMODE = .RDONLY }, 0);
defer _ = std.posix.system.close(fd);
var buf: std.ArrayList(u8) = .empty;
errdefer buf.deinit(aa);
while (buf.items.len < max) {
try buf.ensureUnusedCapacity(aa, 32 * 1024);
const dst = buf.unusedCapacitySlice();
const n = try std.posix.read(fd, dst);
if (n == 0) break;
buf.items.len += n;
}
return try buf.toOwnedSlice(aa);
}
134 changes: 134 additions & 0 deletions frameworks/zeemo-tuned/src/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//! HttpArena `tuned` entry built on the zeemo Zig HTTP library.
//!
//! Handlers go through zeemo's framework HTTP server (no raw sockets),
//! JSON is serialized per request from native struct data via zeemo's
//! comptime serializer (no pre-rendered fragments), and static files are
//! cached + pre-baked at startup (allowed for `tuned`).

const std = @import("std");
const zeemo = @import("zeemo");
const dataset = @import("dataset.zig");

var DS: dataset.Dataset = undefined;
var STATIC: zeemo.static.Dir = undefined;

const STATIC_FILES = struct {
const Entry = struct { name: []const u8, ct: []const u8, compress: bool };
const all = [_]Entry{
.{ .name = "reset.css", .ct = "text/css", .compress = true },
.{ .name = "layout.css", .ct = "text/css", .compress = true },
.{ .name = "theme.css", .ct = "text/css", .compress = true },
.{ .name = "components.css", .ct = "text/css", .compress = true },
.{ .name = "utilities.css", .ct = "text/css", .compress = true },
.{ .name = "analytics.js", .ct = "application/javascript", .compress = true },
.{ .name = "helpers.js", .ct = "application/javascript", .compress = true },
.{ .name = "app.js", .ct = "application/javascript", .compress = true },
.{ .name = "vendor.js", .ct = "application/javascript", .compress = true },
.{ .name = "router.js", .ct = "application/javascript", .compress = true },
.{ .name = "header.html", .ct = "text/html; charset=utf-8", .compress = true },
.{ .name = "footer.html", .ct = "text/html; charset=utf-8", .compress = true },
.{ .name = "regular.woff2", .ct = "font/woff2", .compress = false },
.{ .name = "bold.woff2", .ct = "font/woff2", .compress = false },
.{ .name = "logo.svg", .ct = "image/svg+xml", .compress = true },
.{ .name = "icon-sprite.svg", .ct = "image/svg+xml", .compress = true },
.{ .name = "hero.webp", .ct = "image/webp", .compress = false },
.{ .name = "thumb1.webp", .ct = "image/webp", .compress = false },
.{ .name = "thumb2.webp", .ct = "image/webp", .compress = false },
.{ .name = "manifest.json", .ct = "application/json", .compress = true },
};
};

pub fn main() !void {
var dba: std.heap.DebugAllocator(.{}) = .init;
defer _ = dba.deinit();
const gpa = dba.allocator();

DS = try dataset.load(gpa, "/data/dataset.json");
STATIC = zeemo.static.Dir.init(gpa, "/data/static");
for (STATIC_FILES.all) |f|
try STATIC.add(f.name, f.ct, .{ .compressible = f.compress });
zeemo.static.setDir(&STATIC);
std.log.info("zeemo-tuned: {d} items, {d} static files", .{ DS.items.len, STATIC.map.count() });

var server = zeemo.Server.init(gpa, .{
.port = 8080,
// tuned-allowed knobs:
.write_inline_bytes = 4 * 1024,
// Sized for HttpArena's 20 MiB upload profile. The recv buffer
// (parser_header_buf) collects raw bytes — headers + body — so
// it has to be ≥ body size + headers room. Both buffers are
// page_allocator-backed and lazily faulted: ~22 MiB virtual per
// slot, near-zero RSS unless an upload actually lands.
.parser_header_buf = 22 * 1024 * 1024,
.parser_body_buf = 20 * 1024 * 1024,
.big_buf_path_prefix = "/json/",
// json-tls profile: HTTPS on 8081, ALPN http/1.1, certs mounted
// by the runner at /certs.
.tls = .{
.cert_path = "/certs/server.crt",
.key_path = "/certs/server.key",
.port = 8081,
.alpn = "http/1.1",
},
});
defer server.deinit();

try server.get("/baseline11", baseline11);
try server.post("/baseline11", baseline11);
try server.get("/pipeline", pipeline);
try server.get("/json/:count", jsonHandler);
try server.post("/upload", uploadHandler);
try server.staticMount("/static/", "/data/static", zeemo.static.registerHandler());

try server.run();
}

fn baseline11(req: *const zeemo.Request, res: *zeemo.Response) !void {
var sum: i64 = 0;
if (req.queryInt("a", i64)) |a| sum += a;
if (req.queryInt("b", i64)) |b| sum += b;
if (req.method == .POST and req.body.len > 0) {
sum += std.fmt.parseInt(i64, req.body, 10) catch 0;
}
try res.printText("{d}", .{sum});
}

fn pipeline(_: *const zeemo.Request, res: *zeemo.Response) !void {
try res.text("ok");
}

fn uploadHandler(req: *const zeemo.Request, res: *zeemo.Response) !void {
// HttpArena's `upload` profile sends a 20 MiB POST body and expects
// the byte count back. The parser accumulates the full body in
// `req.body` before we get here.
try res.printText("{d}", .{req.body.len});
}

fn jsonHandler(req: *const zeemo.Request, res: *zeemo.Response) !void {
const count = try req.param("count", u8);
const m = req.queryInt("m", i64) orelse 1;
if (count == 0 or count > DS.items.len) {
res.status(400);
try res.text("bad count");
return;
}
var items: [50]dataset.Item = undefined;
for (0..count) |i| {
items[i] = DS.items[i];
items[i].total = @as(i64, items[i].price) * @as(i64, items[i].quantity) * m;
}
// json-comp profile sets Accept-Encoding: gzip — same handler, just
// gzip the body in place. json profile sends no Accept-Encoding and
// takes the plain branch.
if (req.accepts_gzip) {
try res.jsonGzipped(.{
.items = items[0..count],
.count = @as(u32, count),
});
} else {
try res.json(.{
.items = items[0..count],
.count = @as(u32, count),
});
}
}