Skip to content
Merged
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
44 changes: 44 additions & 0 deletions frameworks/zeemo/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# syntax=docker/dockerfile:1.7
#
# zeemo — Zig io_uring HTTP/1.1 server. Built statically against musl so the
# runtime image is a single binary on scratch.

FROM ubuntu:24.04 AS build
ARG ZIG_VERSION=0.16.0
ARG TARGETARCH
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl xz-utils \
&& rm -rf /var/lib/apt/lists/*

# Pull the official Zig toolchain. Pick the arch that matches the build
# platform; Buildx sets TARGETARCH to amd64/arm64.
RUN set -eu; \
case "${TARGETARCH:-amd64}" in \
amd64) ZIG_ARCH=x86_64 ;; \
arm64) ZIG_ARCH=aarch64 ;; \
*) echo "unsupported arch: ${TARGETARCH}" >&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
# Match Zig target to the container platform Docker is building.
# HttpArena's bench runner builds for amd64. For local OrbStack on Apple
# Silicon we want arm64 native (Rosetta has BSS-size bugs that abort our
# binary when emulating amd64).
RUN set -eu; \
case "${TARGETARCH:-amd64}" in \
amd64) ZIG_TARGET=x86_64-linux-musl ;; \
arm64) ZIG_TARGET=aarch64-linux-musl ;; \
esac; \
zig build -Dtarget="${ZIG_TARGET}" --release=fast

FROM scratch
COPY --from=build /src/zig-out/bin/zeemo /zeemo
EXPOSE 8080
ENTRYPOINT ["/zeemo"]
46 changes: 46 additions & 0 deletions frameworks/zeemo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# zeemo

Bare-metal Zig HTTP/1.1 server for the [HttpArena](https://www.http-arena.com/) benchmark.

- **Engine:** Linux `io_uring` (multishot accept, direct syscalls — no liburing wrapper)
- **Concurrency:** one process per allowed CPU via `SO_REUSEPORT`, each pinned with `sched_setaffinity`, shared-nothing
- **Parser:** hand-written incremental HTTP/1.1 (TCP fragmentation, `Content-Length`, `Transfer-Encoding: chunked`, keep-alive, pipelining)
- **Runtime:** ~370 KB static musl binary on `scratch`

Supported HttpArena profiles:

- `baseline` — `GET/POST /baseline11?a=…&b=…`, returns the integer sum
- `json` — `GET /json/{count}?m=…`, renders the first `count` items of `/data/dataset.json` with `total = price * quantity * m`

## Build

```sh
zig build --release=fast # native
zig build -Dtarget=x86_64-linux-musl --release=fast
```

## Run

```sh
docker build -t zeemo .
docker run --rm -p 8080:8080 \
--ulimit memlock=-1:-1 \
-v /path/to/dataset.json:/data/dataset.json:ro \
zeemo
```

On OrbStack the default seccomp profile blocks `io_uring_setup`; add
`--security-opt seccomp=unconfined` locally. The HttpArena bench machine
(Ubuntu 24.04) allows io_uring by default.

## Tests

```sh
zig test src/http.zig
zig test src/dataset.zig
zig test src/handlers.zig
```

`scripts/local-validate.sh` runs the HttpArena validation suite (17 checks
covering baseline, anti-cheat, TCP fragmentation, and JSON) against a
local container.
22 changes: 22 additions & 0 deletions frameworks/zeemo/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const std = @import("std");

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

const exe = b.addExecutable(.{
.name = "zeemo",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.strip = true,
}),
});
b.installArtifact(exe);

const run_step = b.step("run", "Run the server");
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
}
12 changes: 12 additions & 0 deletions frameworks/zeemo/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.{
.name = .zeemo,
.version = "0.1.0",
.fingerprint = 0x3afa68aae14cae32,
.minimum_zig_version = "0.16.0",
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
.dependencies = .{},
}
14 changes: 14 additions & 0 deletions frameworks/zeemo/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"display_name": "zeemo",
"language": "Zig",
"type": "engine",
"engine": "io_uring",
"description": "Bare-metal Zig HTTP/1.1 server on io_uring. One reactor process per logical CPU via SO_REUSEPORT, hand-written incremental parser, registered provided-buffer rings.",
"repo": "https://github.com/skylightis666/zeemo",
"enabled": true,
"tests": [
"baseline",
"json"
],
"maintainers": []
}
189 changes: 189 additions & 0 deletions frameworks/zeemo/src/dataset.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
const std = @import("std");

pub const ItemCount = 50;
pub const PrefixMax = 512;

pub const Item = struct {
/// Pre-rendered JSON object for this item, WITHOUT the closing `}`.
/// Caller appends `,"total":<n>}` per request.
prefix: []const u8,
/// price * quantity, pre-multiplied so per-request work is one ×m
/// followed by an integer-to-decimal print.
pq: u64,
};

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,
};
if (arr.items.len != ItemCount) return error.BadDataset;

const items = try aa.alloc(Item, ItemCount);
for (arr.items, 0..) |elem, i| {
const obj = switch (elem) {
.object => |o| o,
else => return error.BadDataset,
};
const price = jsonInt(obj.get("price") orelse return error.BadDataset);
const quantity = jsonInt(obj.get("quantity") orelse return error.BadDataset);

var buf: std.ArrayList(u8) = .empty;
try renderItemPrefix(&buf, aa, obj);
items[i] = .{
.prefix = try buf.toOwnedSlice(aa),
.pq = @as(u64, @intCast(price)) * @as(u64, @intCast(quantity)),
};
}

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);
try buf.ensureTotalCapacity(aa, 64 * 1024);
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 buf.toOwnedSlice(aa);
}

fn jsonInt(v: std.json.Value) i64 {
return switch (v) {
.integer => |n| n,
.float => |f| @intFromFloat(f),
else => 0,
};
}

/// Renders the JSON object for `obj` *without* the closing `}`. We can leave
/// the trailing comma off because the next thing appended is always
/// `,"total":...}` — the leading comma is already there.
fn renderItemPrefix(buf: *std.ArrayList(u8), aa: std.mem.Allocator, obj: std.json.ObjectMap) !void {
try buf.append(aa, '{');
var first = true;
var it = obj.iterator();
while (it.next()) |kv| {
if (!first) try buf.append(aa, ',');
first = false;
try writeString(buf, aa, kv.key_ptr.*);
try buf.append(aa, ':');
try writeValue(buf, aa, kv.value_ptr.*);
}
// Intentionally no closing `}` — caller appends `,"total":N}`.
}

fn writeValue(buf: *std.ArrayList(u8), aa: std.mem.Allocator, v: std.json.Value) !void {
switch (v) {
.null => try buf.appendSlice(aa, "null"),
.bool => |b| try buf.appendSlice(aa, if (b) "true" else "false"),
.integer => |n| try writeInt(buf, aa, n),
.float => |f| {
var tmp: [32]u8 = undefined;
const s = std.fmt.bufPrint(&tmp, "{d}", .{f}) catch unreachable;
try buf.appendSlice(aa, s);
},
.number_string => |ns| try buf.appendSlice(aa, ns),
.string => |s| try writeString(buf, aa, s),
.array => |arr| {
try buf.append(aa, '[');
for (arr.items, 0..) |e, i| {
if (i > 0) try buf.append(aa, ',');
try writeValue(buf, aa, e);
}
try buf.append(aa, ']');
},
.object => |o| {
try buf.append(aa, '{');
var first = true;
var it = o.iterator();
while (it.next()) |kv| {
if (!first) try buf.append(aa, ',');
first = false;
try writeString(buf, aa, kv.key_ptr.*);
try buf.append(aa, ':');
try writeValue(buf, aa, kv.value_ptr.*);
}
try buf.append(aa, '}');
},
}
}

fn writeInt(buf: *std.ArrayList(u8), aa: std.mem.Allocator, n: i64) !void {
var tmp: [24]u8 = undefined;
const s = std.fmt.bufPrint(&tmp, "{d}", .{n}) catch unreachable;
try buf.appendSlice(aa, s);
}

fn writeString(buf: *std.ArrayList(u8), aa: std.mem.Allocator, s: []const u8) !void {
try buf.append(aa, '"');
for (s) |c| {
switch (c) {
'"' => try buf.appendSlice(aa, "\\\""),
'\\' => try buf.appendSlice(aa, "\\\\"),
0x00...0x1f => {
var esc: [6]u8 = undefined;
_ = std.fmt.bufPrint(&esc, "\\u{x:0>4}", .{c}) catch unreachable;
try buf.appendSlice(aa, esc[0..6]);
},
else => try buf.append(aa, c),
}
}
try buf.append(aa, '"');
}

test "load dataset and assemble valid JSON" {
var ds = try load(std.testing.allocator, "../HttpArena/data/dataset.json");
defer ds.deinit();
try std.testing.expectEqual(@as(usize, ItemCount), ds.items.len);
// First item: Alpha Widget, price=328, quantity=15.
try std.testing.expectEqual(@as(u64, 328 * 15), ds.items[0].pq);
try std.testing.expect(std.mem.startsWith(u8, ds.items[0].prefix, "{"));

// The prefix + ",\"total\":N}" must parse as valid JSON and yield total
// = price * quantity * m.
const aa = std.testing.allocator;
const m: u64 = 3;
const total = ds.items[0].pq * m;
const tail = try std.fmt.allocPrint(aa, ",\"total\":{d}}}", .{total});
defer aa.free(tail);
const full = try std.mem.concat(aa, u8, &.{ ds.items[0].prefix, tail });
defer aa.free(full);

var parsed = try std.json.parseFromSlice(std.json.Value, aa, full, .{});
defer parsed.deinit();
const obj = parsed.value.object;
try std.testing.expectEqual(@as(i64, @intCast(total)), obj.get("total").?.integer);
try std.testing.expectEqual(@as(i64, 328), obj.get("price").?.integer);
try std.testing.expectEqualStrings("Alpha Widget", obj.get("name").?.string);
}
Loading