diff --git a/frameworks/zix-ws/.gitignore b/frameworks/zix-ws/.gitignore new file mode 100644 index 000000000..595d20d1a --- /dev/null +++ b/frameworks/zix-ws/.gitignore @@ -0,0 +1,4 @@ +.zig-cache +zig-out +zig-package +vendor diff --git a/frameworks/zix-ws/Dockerfile b/frameworks/zix-ws/Dockerfile index 13da3e499..c5c70ce0f 100644 --- a/frameworks/zix-ws/Dockerfile +++ b/frameworks/zix-ws/Dockerfile @@ -4,21 +4,26 @@ FROM alpine:3.20 AS build ARG RETRY=6 ARG TARGETARCH ARG RETRY_DELAY=3 +ARG TIMEOUT_SEC=180 ARG ZIG_VERSION=0.16.0 -ARG ZIX_VERSION=0.4.x-rc3 -RUN apk add --no-cache ca-certificates curl git tar xz +ARG ZIX_VERSION=0.5.x-rc1 +RUN apk add --no-cache ca-certificates curl git tar xz openssl +WORKDIR /server 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" \ + curl -fSL -m ${TIMEOUT_SEC} "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}" +COPY build.zig build.zig.zon ./ +COPY src ./src + # Vendor zix X.Y.Z, separate layer so source-only rebuilds skip the fetch. # The Http1 raw engine work this image needs (large-body drain plus the per-worker # response cache used by the /json endpoint) must be present on the X.Y.Z branch. @@ -30,15 +35,15 @@ ENV PATH="/opt/zig:${PATH}" # every attempt. RUN set -eu; \ fetch() { \ - rm -rf /src/vendor/zix; mkdir -p /src/vendor/zix; \ - curl -fsSL --retry ${RETRY} --retry-delay ${RETRY_DELAY} --retry-all-errors "$1" -o /tmp/zix.tar.gz \ - && tar -xz --strip-components=1 -C /src/vendor/zix -f /tmp/zix.tar.gz; \ + rm -rf /server/vendor/zix; mkdir -p /server/vendor/zix; \ + curl -fSL --retry ${RETRY} --retry-delay ${RETRY_DELAY} --retry-all-errors "$1" -o /tmp/zix.tar.gz \ + && tar -xz --strip-components=1 -C /server/vendor/zix -f /tmp/zix.tar.gz; \ }; \ clone() { \ attempt=0; \ while [ "${attempt}" -lt "${RETRY}" ]; do \ - rm -rf /src/vendor/zix; \ - git clone --depth 1 --branch "${ZIX_VERSION}" "$1" /src/vendor/zix && return 0; \ + rm -rf /server/vendor/zix; \ + git clone --depth 1 --branch "${ZIX_VERSION}" "$1" /server/vendor/zix && return 0; \ attempt=$((attempt + 1)); \ sleep "${RETRY_DELAY}"; \ done; \ @@ -53,17 +58,17 @@ RUN set -eu; \ clone "https://codeberg.org/prothegee/zix.git" \ || { echo "FAILED: git clone ${RETRY} times from codeberg" >&2; exit 1; }; }; }; } -WORKDIR /src -COPY build.zig build.zig.zon ./ -COPY src ./src +# +aes+pclmul: x86_64_v3 omits AES-NI / PCLMUL, +# so zix TLS would compile the ~40x slower software AES-GCM. +# +adx speeds the RSA / Montgomery path. Every x86_64_v3 CPU has them, so it is safe. RUN set -eu; \ case "${TARGETARCH:-amd64}" in \ amd64) ZIG_TARGET=x86_64-linux-musl; ZIG_CPU=x86_64_v3 ;; \ arm64) ZIG_TARGET=aarch64-linux-musl; ZIG_CPU=baseline ;; \ esac; \ - zig build -Dtarget="${ZIG_TARGET}" -Dcpu="${ZIG_CPU}" --release=fast + zig build -Dtarget="${ZIG_TARGET}" -Dcpu="${ZIG_CPU}+aes+pclmul+adx" --release=fast FROM alpine:3.20 -COPY --from=build /src/zig-out/bin/zix-ws /zix-ws +COPY --from=build /server/zig-out/bin/zix-ws /zix-ws EXPOSE 8080 ENTRYPOINT ["/zix-ws"] diff --git a/frameworks/zix-ws/meta.json b/frameworks/zix-ws/meta.json index aa33e4c45..c091b5486 100644 --- a/frameworks/zix-ws/meta.json +++ b/frameworks/zix-ws/meta.json @@ -1,9 +1,9 @@ { - "display_name": "zix-ws", + "display_name": "zix", "language": "Zig", "type": "engine", - "engine": "zix", - "description": "Zig WebSocket echo server on the zix.Http1 raw engine (no std.http). Shared-nothing by design: each worker owns its own SO_REUSEPORT multishot accept, io_uring completion ring, and connections, with no shared state or locking across cores. The upgrade and echo are engine-owned: frames are read from a shared per-worker provided-buffer ring (an idle connection holds no buffer), and a pipelined burst is coalesced into one write.", + "engine": "zix.Http1 WebSocket URING Dispatch Model", + "description": "Zig WebSocket echo on the zix.Http1 engine-owned WebSocket path, .URING dispatch: GET /ws upgrades, WebSocket.serve echoes inside the engine, a pipelined burst coalesced into one write.", "repo": "https://codeberg.org/prothegee/zix", "enabled": true, "tests": [ diff --git a/frameworks/zix-ws/src/main.zig b/frameworks/zix-ws/src/main.zig index 05adac321..23cb27ff9 100644 --- a/frameworks/zix-ws/src/main.zig +++ b/frameworks/zix-ws/src/main.zig @@ -1,57 +1,36 @@ //! HttpArena: zix-ws //! -//! zix HttpArena WebSocket entry point. -//! -//! Intent: demonstrate the engine-owned WebSocket path of zix.Http1 (EPOLL -//! dispatch model) against the HttpArena echo and echo-pipeline suites. -//! -//! Design choices: -//! - GET /ws upgrades, then zix.Http1.WebSocket.serve drives the echo loop -//! inside the engine: frames are echoed on readiness and a pipelined burst is -//! coalesced into a single write. -//! - No response cache: echo is per-connection, not a broadcast fanout, so there -//! is nothing to precompute or share across connections. +//! zix.Http1 engine-owned WebSocket (.URING) +//! against the HttpArena echo and echo-pipeline suites. +//! GET /ws upgrades, +//! then WebSocket.serve drives the echo loop inside the engine (frames echoed on +//! readiness, a pipelined burst coalesced into one write). +//! No response cache: echo is per-connection, nothing to precompute or share. const std = @import("std"); const zix = @import("zix"); // --------------------------------------------------------- // +const IP: []const u8 = "::"; const PORT: u16 = 8080; -const LISTEN_IP: []const u8 = "::"; const DISPATCH_MODEL: zix.Http1.DispatchModel = .URING; -const KERNEL_BACKLOG: u31 = 16 * 1024; - -/// Per-machine tuning profile (ADR-041 increment 5): .lean for the 12-thread / -/// 32 GB dev box, .throughput for the 64-core / 251 GB competition box. Only the -/// HTTP handshake recv buffer differs (the WS frame buffer is already 32 KiB). -/// Select .throughput for the 64-core deployment. -const Profile = enum { lean, throughput }; -const PROFILE: Profile = .lean; - -const MAX_RECV_BUF: usize = switch (PROFILE) { - .lean => 4 * 1024, - .throughput => 16 * 1024, -}; -const WS_RECV_BUF: usize = 32 * 1024; -const MAX_HEADERS: u8 = 16; -const WORKERS: usize = 0; // --------------------------------------------------------- // fn badRequest(fd: std.posix.fd_t) void { - zix.Http1.writeSimple(fd, 400, "text/plain", "bad request") catch {}; + zix.Http1.sendSimpleFD(fd, 400, "text/plain", "bad request") catch {}; } fn notFound(fd: std.posix.fd_t) void { - zix.Http1.writeSimple(fd, 404, "text/plain", "Not Found") catch {}; + zix.Http1.sendSimpleFD(fd, 404, "text/plain", "Not Found") catch {}; } // --------------------------------------------------------- // // Echo every text/binary frame back. Ping/close are handled by the engine. fn wsOnFrame(fd: std.posix.fd_t, opcode: u8, payload: []const u8) void { - zix.Http1.WebSocket.send(fd, @enumFromInt(opcode), payload) catch {}; + zix.Http1.WebSocket.sendFD(fd, @enumFromInt(opcode), payload) catch {}; } // GET /ws : WebSocket upgrade then engine-owned echo. @@ -66,7 +45,7 @@ fn wsHandler(head: *const zix.Http1.ParsedHead, body: []const u8, fd: std.posix. } zix.Http1.WebSocket.serve(fd, ws_key.?, wsOnFrame) catch { - zix.Http1.writeSimple(fd, 500, "text/plain", "handshake failed") catch {}; + zix.Http1.sendSimpleFD(fd, 500, "text/plain", "handshake failed") catch {}; return; }; } @@ -88,15 +67,17 @@ pub fn main(process: std.process.Init) !void { var server = zix.Http1.Server.init(dispatch, .{ .io = process.io, - .ip = LISTEN_IP, + .ip = IP, .port = PORT, .dispatch_model = DISPATCH_MODEL, - .kernel_backlog = KERNEL_BACKLOG, - .max_recv_buf = MAX_RECV_BUF, - .ws_recv_buf = WS_RECV_BUF, - .max_headers = MAX_HEADERS, - .workers = WORKERS, + .workers = 0, + .kernel_backlog = 16 * 1024, + .max_recv_buf = 6 * 1024, + .ws_recv_buf = 32 * 1024, + .max_headers = 8, .send_date_header = false, + .uring_send_buf_size = 16 * 1024, + .process_queue_len = 2_000_000, }); defer server.deinit();