Skip to content

Commit 176b002

Browse files
Zix 0.4.x ws (#872)
* zix-ws ignore .zig-cache & zig-out * zix-ws WebSocket on 0.4.x-rc1 * zix-ws metadata * zix-ws build script * zix-ws build script * zix-ws 0.4.x-rc1 x86_64 musl alpine * sync to attempt rc2 test 1 * using git with 2 repositories attempt * finalizing 0.4.x-rc2 * bump: zix 0.4.x * Benchmark results: zix-ws --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 4e0617e commit 176b002

19 files changed

Lines changed: 343 additions & 0 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ frameworks/zix/vendor
2222
frameworks/zix-grpc/.zig-cache
2323
frameworks/zix-grpc/zig-out
2424
frameworks/zix-grpc/vendor
25+
frameworks/zix-ws/.zig-cache
26+
frameworks/zix-ws/zig-out
27+
frameworks/zix-ws/vendor
2528

2629
# IDE settings
2730
*.user

frameworks/zix-ws/Dockerfile

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# syntax=docker/dockerfile:1.7
2+
3+
FROM alpine:3.20 AS build
4+
ARG RETRY=6
5+
ARG TARGETARCH
6+
ARG RETRY_DELAY=3
7+
ARG ZIG_VERSION=0.16.0
8+
ARG ZIX_VERSION=0.4.x
9+
RUN apk add --no-cache ca-certificates curl git tar xz
10+
11+
RUN set -eu; \
12+
case "${TARGETARCH:-amd64}" in \
13+
amd64) ZIG_ARCH=x86_64 ;; \
14+
arm64) ZIG_ARCH=aarch64 ;; \
15+
*) echo "unsupported arch: ${TARGETARCH}" >&2; exit 1 ;; \
16+
esac; \
17+
curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}.tar.xz" \
18+
| tar -xJ -C /opt; \
19+
mv "/opt/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}" /opt/zig
20+
ENV PATH="/opt/zig:${PATH}"
21+
22+
# Vendor zix 0.4.x, separate layer so source-only rebuilds skip the fetch.
23+
# The Http1 raw engine work this image needs (large-body drain plus the per-worker
24+
# response cache used by the /json endpoint) must be present on the 0.4.x branch.
25+
# Four ordered attempts before giving up: curl the archive tarball from github then
26+
# codeberg, then a shallow git clone from github then codeberg. The github archive
27+
# redirects to codeload.github.com (which the benchmark runner may not resolve), so
28+
# curl can fall through to the codeberg tarball, and git clone talks to github.com
29+
# and codeberg.org directly as the deeper fallback. RETRY and RETRY_DELAY bound
30+
# every attempt.
31+
RUN set -eu; \
32+
fetch() { \
33+
rm -rf /src/vendor/zix; mkdir -p /src/vendor/zix; \
34+
curl -fsSL --retry ${RETRY} --retry-delay ${RETRY_DELAY} --retry-all-errors "$1" -o /tmp/zix.tar.gz \
35+
&& tar -xz --strip-components=1 -C /src/vendor/zix -f /tmp/zix.tar.gz; \
36+
}; \
37+
clone() { \
38+
attempt=0; \
39+
while [ "${attempt}" -lt "${RETRY}" ]; do \
40+
rm -rf /src/vendor/zix; \
41+
git clone --depth 1 --branch "${ZIX_VERSION}" "$1" /src/vendor/zix && return 0; \
42+
attempt=$((attempt + 1)); \
43+
sleep "${RETRY_DELAY}"; \
44+
done; \
45+
return 1; \
46+
}; \
47+
fetch "https://github.com/prothegee/zix/archive/refs/heads/${ZIX_VERSION}.tar.gz" \
48+
|| { echo "FAILED: curl ${RETRY} times from github" >&2; \
49+
fetch "https://codeberg.org/prothegee/zix/archive/${ZIX_VERSION}.tar.gz" \
50+
|| { echo "FAILED: curl ${RETRY} times from codeberg" >&2; \
51+
clone "https://github.com/prothegee/zix.git" \
52+
|| { echo "FAILED: git clone ${RETRY} times from github" >&2; \
53+
clone "https://codeberg.org/prothegee/zix.git" \
54+
|| { echo "FAILED: git clone ${RETRY} times from codeberg" >&2; exit 1; }; }; }; }
55+
56+
WORKDIR /src
57+
COPY build.zig build.zig.zon ./
58+
COPY src ./src
59+
RUN set -eu; \
60+
case "${TARGETARCH:-amd64}" in \
61+
amd64) ZIG_TARGET=x86_64-linux-musl; ZIG_CPU=x86_64_v3 ;; \
62+
arm64) ZIG_TARGET=aarch64-linux-musl; ZIG_CPU=baseline ;; \
63+
esac; \
64+
zig build -Dtarget="${ZIG_TARGET}" -Dcpu="${ZIG_CPU}" --release=fast
65+
66+
FROM alpine:3.20
67+
COPY --from=build /src/zig-out/bin/zix-ws /zix-ws
68+
EXPOSE 8080
69+
ENTRYPOINT ["/zix-ws"]

frameworks/zix-ws/build.zig

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const std = @import("std");
2+
3+
pub fn build(b: *std.Build) void {
4+
const target = b.standardTargetOptions(.{});
5+
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });
6+
7+
const zix_dep = b.dependency("zix", .{ .target = target, .optimize = optimize });
8+
const zix_mod = zix_dep.module("zix");
9+
10+
const exe = b.addExecutable(.{
11+
.name = "zix-ws",
12+
.root_module = b.createModule(.{
13+
.root_source_file = b.path("src/main.zig"),
14+
.target = target,
15+
.optimize = optimize,
16+
.strip = true,
17+
}),
18+
});
19+
exe.root_module.addImport("zix", zix_mod);
20+
b.installArtifact(exe);
21+
22+
const run_step = b.step("run", "Run the server");
23+
const run_cmd = b.addRunArtifact(exe);
24+
if (b.args) |args| run_cmd.addArgs(args);
25+
run_step.dependOn(&run_cmd.step);
26+
}

frameworks/zix-ws/build.zig.zon

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.{
2+
.name = .zix_ws_arena,
3+
.version = "0.1.0",
4+
.fingerprint = 0xba3a8c57d2738130,
5+
.minimum_zig_version = "0.16.0",
6+
.paths = .{
7+
"build.zig",
8+
"build.zig.zon",
9+
"src",
10+
},
11+
.dependencies = .{
12+
.zix = .{
13+
.path = "vendor/zix",
14+
},
15+
},
16+
}

frameworks/zix-ws/meta.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"display_name": "zix-ws",
3+
"language": "Zig",
4+
"type": "engine",
5+
"engine": "zix",
6+
"description": "Zig WebSocket echo server on the zix.Http1 raw engine (no std.http). Shared-nothing: each worker runs its own SO_REUSEPORT multishot accept plus io_uring completion loop and owns its connections. The WebSocket upgrade and echo are engine-owned, frames echoed on readiness with a pipelined burst coalesced into one write.",
7+
"repo": "https://codeberg.org/prothegee/zix",
8+
"enabled": true,
9+
"tests": [
10+
"echo-ws",
11+
"echo-ws-pipeline"
12+
],
13+
"maintainers": ["prothegee"]
14+
}

frameworks/zix-ws/src/main.zig

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//! HttpArena: zix-ws
2+
//! zix version: 0.4.x
3+
//!
4+
//! zix HttpArena WebSocket entry point.
5+
//!
6+
//! Intent: demonstrate the engine-owned WebSocket path of zix.Http1 (EPOLL
7+
//! dispatch model) against the HttpArena echo and echo-pipeline suites.
8+
//!
9+
//! Design choices:
10+
//! - GET /ws upgrades, then zix.Http1.WebSocket.serve drives the echo loop
11+
//! inside the engine: frames are echoed on readiness and a pipelined burst is
12+
//! coalesced into a single write.
13+
//! - No response cache: echo is per-connection, not a broadcast fanout, so there
14+
//! is nothing to precompute or share across connections.
15+
16+
const std = @import("std");
17+
const zix = @import("zix");
18+
19+
// --------------------------------------------------------- //
20+
21+
const PORT: u16 = 8080;
22+
const LISTEN_IP: []const u8 = "::";
23+
const DISPATCH_MODEL: zix.Http1.DispatchModel = .URING;
24+
const KERNEL_BACKLOG: u31 = 16 * 1024;
25+
const MAX_RECV_BUF: usize = 4 * 1024;
26+
const WS_RECV_BUF: usize = 32 * 1024;
27+
const MAX_HEADERS: u8 = 16;
28+
const WORKERS: usize = 0;
29+
30+
// --------------------------------------------------------- //
31+
32+
fn badRequest(fd: std.posix.fd_t) void {
33+
zix.Http1.writeSimple(fd, 400, "text/plain", "bad request") catch {};
34+
}
35+
36+
fn notFound(fd: std.posix.fd_t) void {
37+
zix.Http1.writeSimple(fd, 404, "text/plain", "Not Found") catch {};
38+
}
39+
40+
// --------------------------------------------------------- //
41+
42+
// Echo every text/binary frame back. Ping/close are handled by the engine.
43+
fn wsOnFrame(fd: std.posix.fd_t, opcode: u8, payload: []const u8) void {
44+
zix.Http1.WebSocket.send(fd, @enumFromInt(opcode), payload) catch {};
45+
}
46+
47+
// GET /ws : WebSocket upgrade then engine-owned echo.
48+
fn wsHandler(head: *const zix.Http1.ParsedHead, body: []const u8, fd: std.posix.fd_t) void {
49+
_ = body;
50+
51+
const upgrade_val = zix.Http1.getHeader(head, "upgrade") orelse "";
52+
const ws_key = zix.Http1.getHeader(head, "sec-websocket-key");
53+
54+
if (!std.ascii.eqlIgnoreCase(upgrade_val, "websocket") or ws_key == null) {
55+
return badRequest(fd);
56+
}
57+
58+
zix.Http1.WebSocket.serve(fd, ws_key.?, wsOnFrame) catch {
59+
zix.Http1.writeSimple(fd, 500, "text/plain", "handshake failed") catch {};
60+
return;
61+
};
62+
}
63+
64+
// --------------------------------------------------------- //
65+
66+
fn dispatch(head: *const zix.Http1.ParsedHead, body: []const u8, fd: std.posix.fd_t) void {
67+
if (std.mem.eql(u8, head.path, "/ws")) return wsHandler(head, body, fd);
68+
69+
notFound(fd);
70+
}
71+
72+
// --------------------------------------------------------- //
73+
74+
pub fn main(process: std.process.Init) !void {
75+
// Elevate scheduling priority (setpriority -19). Fails silently when the
76+
// process lacks CAP_SYS_NICE, so no special capability is required for correctness.
77+
_ = std.os.linux.syscall3(.setpriority, 0, 0, @as(usize, @bitCast(@as(isize, -19))));
78+
79+
var server = zix.Http1.Server.init(dispatch, .{
80+
.io = process.io,
81+
.ip = LISTEN_IP,
82+
.port = PORT,
83+
.dispatch_model = DISPATCH_MODEL,
84+
.kernel_backlog = KERNEL_BACKLOG,
85+
.max_recv_buf = MAX_RECV_BUF,
86+
.ws_recv_buf = WS_RECV_BUF,
87+
.max_headers = MAX_HEADERS,
88+
.workers = WORKERS,
89+
.send_date_header = false,
90+
});
91+
defer server.deinit();
92+
93+
try server.run();
94+
}

site/data/echo-ws-16384.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,5 +613,24 @@
613613
"status_3xx": 0,
614614
"status_4xx": 0,
615615
"status_5xx": 0
616+
},
617+
{
618+
"framework": "zix-ws",
619+
"language": "Zig",
620+
"rps": 4232201,
621+
"avg_latency": "3.81ms",
622+
"p99_latency": "4.43ms",
623+
"cpu": "6083.2%",
624+
"memory": "416MiB",
625+
"connections": 16384,
626+
"threads": 64,
627+
"duration": "5s",
628+
"pipeline": 1,
629+
"bandwidth": "28.64MB/s",
630+
"reconnects": 0,
631+
"status_2xx": 21161008,
632+
"status_3xx": 0,
633+
"status_4xx": 0,
634+
"status_5xx": 0
616635
}
617636
]

site/data/echo-ws-4096.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,5 +613,24 @@
613613
"status_3xx": 0,
614614
"status_4xx": 0,
615615
"status_5xx": 0
616+
},
617+
{
618+
"framework": "zix-ws",
619+
"language": "Zig",
620+
"rps": 4513022,
621+
"avg_latency": "907us",
622+
"p99_latency": "1.21ms",
623+
"cpu": "6420.2%",
624+
"memory": "243MiB",
625+
"connections": 4096,
626+
"threads": 64,
627+
"duration": "5s",
628+
"pipeline": 1,
629+
"bandwidth": "30.12MB/s",
630+
"reconnects": 0,
631+
"status_2xx": 22565110,
632+
"status_3xx": 0,
633+
"status_4xx": 0,
634+
"status_5xx": 0
616635
}
617636
]

site/data/echo-ws-512.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,5 +613,24 @@
613613
"status_3xx": 0,
614614
"status_4xx": 0,
615615
"status_5xx": 0
616+
},
617+
{
618+
"framework": "zix-ws",
619+
"language": "Zig",
620+
"rps": 4279997,
621+
"avg_latency": "119us",
622+
"p99_latency": "210us",
623+
"cpu": "6362.1%",
624+
"memory": "189MiB",
625+
"connections": 512,
626+
"threads": 64,
627+
"duration": "5s",
628+
"pipeline": 1,
629+
"bandwidth": "28.56MB/s",
630+
"reconnects": 0,
631+
"status_2xx": 21399989,
632+
"status_3xx": 0,
633+
"status_4xx": 0,
634+
"status_5xx": 0
616635
}
617636
]

site/data/echo-ws-pipeline-16384.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,5 +435,24 @@
435435
"status_3xx": 0,
436436
"status_4xx": 0,
437437
"status_5xx": 0
438+
},
439+
{
440+
"framework": "zix-ws",
441+
"language": "Zig",
442+
"rps": 53628657,
443+
"avg_latency": "4.70ms",
444+
"p99_latency": "8.62ms",
445+
"cpu": "5884.0%",
446+
"memory": "442MiB",
447+
"connections": 16384,
448+
"threads": 64,
449+
"duration": "5s",
450+
"pipeline": 16,
451+
"bandwidth": "358.29MB/s",
452+
"reconnects": 0,
453+
"status_2xx": 268143289,
454+
"status_3xx": 0,
455+
"status_4xx": 0,
456+
"status_5xx": 0
438457
}
439458
]

0 commit comments

Comments
 (0)