diff --git a/frameworks/dart-io/Dockerfile b/frameworks/dart-io/Dockerfile new file mode 100644 index 000000000..2b9323d00 --- /dev/null +++ b/frameworks/dart-io/Dockerfile @@ -0,0 +1,30 @@ +# Stage 1 — AOT compile the Dart server + build SO_REUSEPORT shim +FROM dart:stable AS build + +WORKDIR /app +COPY frameworks/dart-io/pubspec.yaml ./ +RUN dart pub get + +COPY frameworks/dart-io/benchmark_http_server.dart ./benchmark_http_server.dart +COPY frameworks/dart-io/reuseport_shim.c ./reuseport_shim.c +RUN dart build cli --target benchmark_http_server.dart --output /app/build + +# Compile the LD_PRELOAD shim that enables SO_REUSEPORT for cross-process +# port sharing (Dart's shared:true only works within one process by default). +RUN apt-get update \ + && apt-get install -y --no-install-recommends gcc libc-dev \ + && rm -rf /var/lib/apt/lists/* \ + && gcc -shared -fPIC -nostartfiles \ + -o /app/reuseport_shim.so reuseport_shim.c -ldl + +# Stage 2 — Minimal runtime image +FROM debian:bookworm-slim + +COPY --from=build /app/build/bundle /server +COPY --from=build /app/reuseport_shim.so /lib/reuseport_shim.so +COPY frameworks/dart-io/entrypoint.sh /entrypoint.sh +COPY data/dataset.json /data/dataset.json +RUN chmod +x /entrypoint.sh + +EXPOSE 8080 +CMD ["/entrypoint.sh"] diff --git a/frameworks/dart-io/benchmark_http_server.dart b/frameworks/dart-io/benchmark_http_server.dart new file mode 100644 index 000000000..1e48e9df7 --- /dev/null +++ b/frameworks/dart-io/benchmark_http_server.dart @@ -0,0 +1,103 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +int _sumQuery(Uri uri) { + var sum = 0; + for (final value in uri.queryParameters.values) { + final parsed = int.tryParse(value); + if (parsed != null) sum += parsed; + } + return sum; +} + +int _bodyValue(List bytes) { + if (bytes.isEmpty) return 0; + return int.tryParse(utf8.decode(bytes).trim()) ?? 0; +} + +List> _loadDataset(String path) { + try { + final decoded = jsonDecode(File(path).readAsStringSync()) as List; + return decoded + .map((e) => Map.from(e as Map)) + .toList(growable: false); + } catch (_) { + return const >[]; + } +} + +Uint8List _jsonPayload(List> items, int count, int m) { + final clamped = count.clamp(0, items.length).toInt(); + final out = List>.generate(clamped, (i) { + final item = items[i]; + final price = item['price'] as num; + final quantity = item['quantity'] as num; + return { + 'id': item['id'], + 'name': item['name'], + 'category': item['category'], + 'price': item['price'], + 'quantity': item['quantity'], + 'active': item['active'], + 'tags': item['tags'], + 'rating': item['rating'], + 'total': price * quantity * m, + }; + }, growable: false); + + return Uint8List.fromList( + utf8.encode(jsonEncode({'items': out, 'count': out.length})), + ); +} + +Future main(List args) async { + final port = args.isNotEmpty ? int.parse(args[0]) : 8080; + final jsonItems = _loadDataset('/data/dataset.json'); + final server = await HttpServer.bind(InternetAddress.anyIPv4, port, shared: true); + print('dart:io benchmark HTTP server on port $port'); + + await for (final req in server) { + final uri = req.uri; + if (uri.path == '/pipeline') { + req.response + ..statusCode = 200 + ..headers.contentType = ContentType('text', 'plain', charset: 'utf-8') + ..write('ok'); + await req.response.close(); + continue; + } + + if (uri.path == '/baseline11') { + final body = await req.fold>([], (acc, chunk) { + acc.addAll(chunk); + return acc; + }); + final total = _sumQuery(uri) + _bodyValue(body); + req.response + ..statusCode = 200 + ..headers.contentType = ContentType('text', 'plain', charset: 'utf-8') + ..write(total); + await req.response.close(); + continue; + } + + if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'json') { + final requestedCount = int.tryParse(uri.pathSegments[1]) ?? 0; + final multiplier = int.tryParse(uri.queryParameters['m'] ?? '') ?? 1; + final payload = _jsonPayload(jsonItems, requestedCount, multiplier); + req.response + ..statusCode = 200 + ..headers.contentType = ContentType('application', 'json', charset: 'utf-8') + ..add(payload); + await req.response.close(); + continue; + } + + req.response + ..statusCode = 404 + ..headers.contentType = ContentType('text', 'plain', charset: 'utf-8') + ..write('not found'); + await req.response.close(); + } +} diff --git a/frameworks/dart-io/build.sh b/frameworks/dart-io/build.sh new file mode 100755 index 000000000..3132fbdd9 --- /dev/null +++ b/frameworks/dart-io/build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HTT_ARENA_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)" +FRAMEWORK_IMAGE="httparena-dart-io" +BASE_IMAGE="${DART_IO_BASE_IMAGE:-dart:stable}" + +docker pull "$BASE_IMAGE" +docker build \ + -f "$SCRIPT_DIR/Dockerfile" \ + --build-arg BASE_IMAGE="$BASE_IMAGE" \ + -t "$FRAMEWORK_IMAGE" \ + "$HTT_ARENA_ROOT" diff --git a/frameworks/dart-io/entrypoint.sh b/frameworks/dart-io/entrypoint.sh new file mode 100755 index 000000000..4fd458019 --- /dev/null +++ b/frameworks/dart-io/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# Enable SO_REUSEPORT for all TCP sockets via LD_PRELOAD shim. +# Dart's HttpServer.bind(shared:true) only shares within one process; the shim +# makes cross-process port sharing work so each worker gets its own +# EventHandler thread and the kernel distributes connections evenly. +export LD_PRELOAD=/lib/reuseport_shim.so + +n="${WORKERS:-$(nproc)}" +port="${PORT:-8080}" +echo "[dart-io] spawning $n dart processes on port $port (nproc=$n)" + +# Spawn N independent OS processes, each running a single isolate. +# Each process gets its own Dart VM EventHandler (kqueue/epoll thread), +# so I/O scales linearly with CPU count — the same model as Node.js cluster. +# The LD_PRELOAD shim above ensures SO_REUSEPORT is set so the kernel +# distributes incoming connections evenly across all N processes. +for i in $(seq 1 $((n - 1))); do + /server/bin/benchmark_http_server "$port" & +done + +# Last worker runs in the foreground as PID 1 so Docker signals reach it. +exec /server/bin/benchmark_http_server "$port" diff --git a/frameworks/dart-io/meta.json b/frameworks/dart-io/meta.json new file mode 100644 index 000000000..e6945e999 --- /dev/null +++ b/frameworks/dart-io/meta.json @@ -0,0 +1,18 @@ +{ + "display_name": "dart-io", + "language": "Dart", + "engine": "dart:io", + "type": "engine", + "description": "Stock Dart HttpServer baseline in the same SDK lineage as dart-zig, with optional JIT/AOT runtime mode via DART_IO_MODE for direct PR comparisons.", + "repo": "https://github.com/kartikey321/dart-sdk", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json" + ], + "maintainers": [ + "kartikey321" + ] +} diff --git a/frameworks/dart-io/pubspec.yaml b/frameworks/dart-io/pubspec.yaml new file mode 100644 index 000000000..aaaadd012 --- /dev/null +++ b/frameworks/dart-io/pubspec.yaml @@ -0,0 +1,5 @@ +name: dart_io +description: HttpArena dart:io benchmark server. +version: 0.0.1 +environment: + sdk: ">=3.0.0 <4.0.0" diff --git a/frameworks/dart-io/reuseport_shim.c b/frameworks/dart-io/reuseport_shim.c new file mode 100644 index 000000000..b8ce889a7 --- /dev/null +++ b/frameworks/dart-io/reuseport_shim.c @@ -0,0 +1,20 @@ +#define _GNU_SOURCE +#include +#include +#include + +typedef int (*bind_fn)(int, const struct sockaddr *, socklen_t); + +int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) { + static bind_fn real_bind = (bind_fn)0; + if (!real_bind) real_bind = (bind_fn)dlsym(RTLD_NEXT, "bind"); + + int type = 0; + int opt = 1; + socklen_t tlen = sizeof(type); + getsockopt(sockfd, SOL_SOCKET, SO_TYPE, &type, &tlen); + if (type == SOCK_STREAM) { + setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); + } + return real_bind(sockfd, addr, addrlen); +} diff --git a/frameworks/dart-zig/Dockerfile b/frameworks/dart-zig/Dockerfile new file mode 100644 index 000000000..cf14ee73e --- /dev/null +++ b/frameworks/dart-zig/Dockerfile @@ -0,0 +1,36 @@ +ARG BUILDER_IMAGE=ghcr.io/kartikey321/dart-zig-builder:sha-0ed2b90cc29c41b6069204d07626e01b5bf074f8 +ARG BASE_IMAGE=ghcr.io/kartikey321/dart-zig-runtime:sha-0ed2b90cc29c41b6069204d07626e01b5bf074f8 + +FROM ${BUILDER_IMAGE} AS build + +COPY frameworks/dart-zig/benchmark_http_server.dart /opt/dart-zig-sdk/dart-zig/lib/_httparena_benchmark_http_server.dart +RUN /opt/dart-zig-sdk/out/ReleaseX64/dart \ + /opt/dart-zig-sdk/pkg/vm/bin/gen_kernel.dart \ + --platform /opt/dart-zig-sdk/out/ReleaseX64/vm_platform.dill \ + --link-platform \ + --packages /opt/dart-zig-sdk/.dart_tool/package_config.json \ + -o /tmp/benchmark_http_server.dill \ + /opt/dart-zig-sdk/dart-zig/lib/_httparena_benchmark_http_server.dart +RUN /opt/dart-zig-sdk/out/ReleaseX64/dart \ + /opt/dart-zig-sdk/pkg/vm/bin/gen_kernel.dart \ + --aot \ + --platform /opt/dart-zig-sdk/out/ReleaseX64/vm_platform_stripped.dill \ + --link-platform \ + --packages /opt/dart-zig-sdk/.dart_tool/package_config.json \ + -o /tmp/benchmark_http_server_aot.dill \ + /opt/dart-zig-sdk/dart-zig/lib/_httparena_benchmark_http_server.dart +RUN /opt/dart-zig-sdk/out/ReleaseX64/gen_snapshot \ + --snapshot_kind=app-aot-elf \ + --elf=/tmp/benchmark_http_server_aot.so \ + /tmp/benchmark_http_server_aot.dill + +FROM ${BASE_IMAGE} + +COPY frameworks/dart-zig/entrypoint.sh /entrypoint.sh +COPY data/dataset.json /data/dataset.json +COPY --from=build /tmp/benchmark_http_server.dill /app/benchmark_http_server.dill +COPY --from=build /tmp/benchmark_http_server_aot.so /app/benchmark_http_server_aot.so +RUN chmod +x /entrypoint.sh + +EXPOSE 8080 +CMD ["/entrypoint.sh"] diff --git a/frameworks/dart-zig/README.md b/frameworks/dart-zig/README.md new file mode 100644 index 000000000..41cc0909c --- /dev/null +++ b/frameworks/dart-zig/README.md @@ -0,0 +1,45 @@ +# dart-zig + +`dart-zig` in HttpArena is split into two images: + +- `ghcr.io/kartikey321/dart-zig-runtime`: generic production runtime +- `ghcr.io/kartikey321/dart-zig-builder`: SDK-backed builder image used only at image build time + +The benchmark application source lives in this framework directory. The +framework Dockerfile compiles `benchmark_http_server.dart` in a builder stage, +then copies the generated `.dill` into the generic runtime image. + +## Default build + +```sh +cd frameworks/dart-zig +./build.sh +``` + +This pulls the published runtime and builder images from GHCR, then builds the +framework image locally. + +## Local SDK mode + +```sh +cd frameworks/dart-zig +DART_ZIG_USE_LOCAL_BUNDLE=1 \ +DART_ZIG_SDK_ROOT=/path/to/sdk \ +./build.sh +``` + +Local mode does two things before building the framework image: + +- rebuilds the generic runtime bundle from the local SDK checkout +- builds a local builder image from the same SDK checkout + +That keeps the source-of-truth split clean: + +- runtime repo owns runtime packaging +- HttpArena owns benchmark app source + +## Runtime requirement + +`dart-zig` uses Linux `io_uring`. Containers must be started with Docker +seccomp relaxed enough to allow `io_uring`. HttpArena already handles this for +frameworks with `"engine": "io_uring"`. diff --git a/frameworks/dart-zig/benchmark_http_server.dart b/frameworks/dart-zig/benchmark_http_server.dart new file mode 100644 index 000000000..584ea327e --- /dev/null +++ b/frameworks/dart-zig/benchmark_http_server.dart @@ -0,0 +1,110 @@ +// HttpArena benchmark app for dart-zig. +// This source is owned by HttpArena. The framework Dockerfile compiles it in +// a builder stage, then copies the generated .dill into the runtime image. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'zig_http_server.dart'; + +int _sumQuery(Uri uri) { + var sum = 0; + for (final value in uri.queryParameters.values) { + final parsed = int.tryParse(value); + if (parsed != null) sum += parsed; + } + return sum; +} + +int _bodyValue(ZigHttpRequest req) { + if (req.bodyBytes.isEmpty) return 0; + return int.tryParse(req.bodyText.trim()) ?? 0; +} + +List> _loadDataset(String path) { + try { + final decoded = jsonDecode(File(path).readAsStringSync()) as List; + return decoded + .map((e) => Map.from(e as Map)) + .toList(growable: false); + } catch (_) { + return const >[]; + } +} + +Uint8List _jsonPayload(List> items, int count, int m) { + final clamped = count.clamp(0, items.length).toInt(); + final out = List>.generate(clamped, (i) { + final item = items[i]; + final price = item['price'] as num; + final quantity = item['quantity'] as num; + return { + 'id': item['id'], + 'name': item['name'], + 'category': item['category'], + 'price': item['price'], + 'quantity': item['quantity'], + 'active': item['active'], + 'tags': item['tags'], + 'rating': item['rating'], + 'total': price * quantity * m, + }; + }, growable: false); + + return Uint8List.fromList( + utf8.encode(jsonEncode({'items': out, 'count': out.length})), + ); +} + +Future main(List args) async { + final port = args.isNotEmpty ? int.parse(args[0]) : 8080; + final jsonItems = _loadDataset('/data/dataset.json'); + final server = await ZigHttpServer.bind('0.0.0.0', port); + print('dart-zig benchmark HTTP server on port $port'); + + final done = Completer(); + server.stream.listen( + (req) { + switch (req.path) { + case '/pipeline': + req.response + ..statusCode = 200 + ..headers.set('content-type', 'text/plain; charset=utf-8') + ..write('ok') + ..close(); + break; + default: + final uri = req.uri; + if (uri.path == '/baseline11') { + final total = _sumQuery(uri) + _bodyValue(req); + req.response + ..statusCode = 200 + ..headers.set('content-type', 'text/plain; charset=utf-8') + ..write(total) + ..close(); + } else if (uri.pathSegments.length == 2 && + uri.pathSegments[0] == 'json') { + final requestedCount = int.tryParse(uri.pathSegments[1]) ?? 0; + final multiplier = int.tryParse(uri.queryParameters['m'] ?? '') ?? 1; + final payload = _jsonPayload(jsonItems, requestedCount, multiplier); + req.response + ..statusCode = 200 + ..headers.set('content-type', 'application/json') + ..add(payload) + ..close(); + } else { + req.response + ..statusCode = 404 + ..headers.set('content-type', 'text/plain; charset=utf-8') + ..write('not found') + ..close(); + } + } + }, + onDone: done.complete, + ); + + await done.future; +} diff --git a/frameworks/dart-zig/build.sh b/frameworks/dart-zig/build.sh new file mode 100755 index 000000000..586cb0c77 --- /dev/null +++ b/frameworks/dart-zig/build.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HTT_ARENA_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)" +FRAMEWORK_IMAGE="httparena-dart-zig" +RUNTIME_IMAGE="${DART_ZIG_RUNTIME_IMAGE:-ghcr.io/kartikey321/dart-zig-runtime:latest}" +BUILDER_IMAGE="${DART_ZIG_BUILDER_IMAGE:-ghcr.io/kartikey321/dart-zig-builder:latest}" +USE_LOCAL_BUNDLE="${DART_ZIG_USE_LOCAL_BUNDLE:-0}" +LOCAL_RUNTIME_IMAGE="${DART_ZIG_LOCAL_RUNTIME_IMAGE:-dart-zig-runtime:local}" +LOCAL_BUILDER_IMAGE="${DART_ZIG_LOCAL_BUILDER_IMAGE:-dart-zig-builder:local}" +APP_SOURCE="$SCRIPT_DIR/benchmark_http_server.dart" +DATASET_SOURCE="$HTT_ARENA_ROOT/data/dataset.json" + +build_framework_image() { + docker build \ + -f "$SCRIPT_DIR/Dockerfile" \ + --build-arg BASE_IMAGE="$1" \ + --build-arg BUILDER_IMAGE="$2" \ + -t "$FRAMEWORK_IMAGE" \ + "$HTT_ARENA_ROOT" +} + +[[ -f "$APP_SOURCE" ]] || { + echo "missing benchmark app source: $APP_SOURCE" >&2 + exit 1 +} +[[ -f "$DATASET_SOURCE" ]] || { + echo "missing dataset: $DATASET_SOURCE" >&2 + exit 1 +} + +if [[ "$USE_LOCAL_BUNDLE" != "1" ]]; then + docker pull "$RUNTIME_IMAGE" + docker pull "$BUILDER_IMAGE" + build_framework_image "$RUNTIME_IMAGE" "$BUILDER_IMAGE" + exit 0 +fi + +: "${DART_ZIG_SDK_ROOT:?set DART_ZIG_SDK_ROOT when DART_ZIG_USE_LOCAL_BUNDLE=1}" +PKG_SCRIPT="$DART_ZIG_SDK_ROOT/dart-zig/scripts/package_runtime_bundle.sh" +RUNTIME_DOCKERFILE="$DART_ZIG_SDK_ROOT/dart-zig/docker/Dockerfile.runtime-base" +BUILDER_DOCKERFILE="$DART_ZIG_SDK_ROOT/dart-zig/docker/Dockerfile.builder-base" +BUNDLE_CONTEXT="$DART_ZIG_SDK_ROOT/dart-zig/dist" + +[[ -x "$PKG_SCRIPT" ]] || { echo "missing package script: $PKG_SCRIPT" >&2; exit 1; } +[[ -f "$RUNTIME_DOCKERFILE" ]] || { echo "missing runtime Dockerfile: $RUNTIME_DOCKERFILE" >&2; exit 1; } +[[ -f "$BUILDER_DOCKERFILE" ]] || { echo "missing builder Dockerfile: $BUILDER_DOCKERFILE" >&2; exit 1; } + +"$PKG_SCRIPT" +docker build -f "$RUNTIME_DOCKERFILE" -t "$LOCAL_RUNTIME_IMAGE" "$BUNDLE_CONTEXT" +docker build -f "$BUILDER_DOCKERFILE" -t "$LOCAL_BUILDER_IMAGE" "$DART_ZIG_SDK_ROOT" +build_framework_image "$LOCAL_RUNTIME_IMAGE" "$LOCAL_BUILDER_IMAGE" diff --git a/frameworks/dart-zig/entrypoint.sh b/frameworks/dart-zig/entrypoint.sh new file mode 100644 index 000000000..8bd623f6d --- /dev/null +++ b/frameworks/dart-zig/entrypoint.sh @@ -0,0 +1,33 @@ +#!/bin/sh +set -eu + +PORT="${PORT:-8080}" +WORKERS="${WORKERS:-$(nproc)}" +MODE="${DART_ZIG_MODE:-aot}" +CLUSTER="${DART_ZIG_CLUSTER:-1}" +export LD_LIBRARY_PATH=/opt/dart-zig/lib + +if [ "$MODE" = "aot" ]; then + BIN="/opt/dart-zig/bin/dart-zig-aot" + SNAP="/app/benchmark_http_server_aot.so" +else + BIN="/opt/dart-zig/bin/dart-zig" + SNAP="/app/benchmark_http_server.dill" +fi + +echo "[dart-zig] mode=${MODE} workers=${WORKERS} cluster=${CLUSTER} port=${PORT}" + +# Default mode: one process, runtime-managed worker threads. +if [ "$CLUSTER" != "1" ] || [ "${WORKERS}" -le 1 ]; then + exec "$BIN" --workers="$WORKERS" "$SNAP" "$PORT" +fi + +# Cluster mode: one OS process per worker, each with --workers=1. +# Runtime bind path sets SO_REUSEPORT so listeners can share the same port. +i=1 +while [ "$i" -lt "${WORKERS}" ]; do + "$BIN" --workers=1 "$SNAP" "$PORT" & + i=$((i + 1)) +done + +exec "$BIN" --workers=1 "$SNAP" "$PORT" diff --git a/frameworks/dart-zig/meta.json b/frameworks/dart-zig/meta.json new file mode 100644 index 000000000..e41225e82 --- /dev/null +++ b/frameworks/dart-zig/meta.json @@ -0,0 +1,18 @@ +{ + "display_name": "dart-zig", + "language": "Dart", + "engine": "io_uring", + "type": "engine", + "description": "dart-zig custom Dart runtime on Linux io_uring, using the reusable dart-zig runtime and builder images, with the benchmark app source owned by HttpArena. Current integration targets baseline, pipelined, limited-conn, and json.", + "repo": "https://github.com/kartikey321/dart-sdk/tree/dart-zig/dart-zig", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json" + ], + "maintainers": [ + "kartikey321" + ] +} diff --git a/frameworks/fletch/meta.json b/frameworks/fletch/meta.json index a640bb5b2..92f890bbf 100644 --- a/frameworks/fletch/meta.json +++ b/frameworks/fletch/meta.json @@ -2,7 +2,7 @@ "display_name": "Fletch", "language": "Dart", "engine": "dart:io", - "type": "tuned", + "type": "production", "description": "Express-inspired HTTP framework for Dart with radix-tree routing, middleware chains, signed sessions, and built-in DI.", "repo": "https://github.com/kartikey321/fletch", "enabled": true,