Skip to content
Open
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
30 changes: 30 additions & 0 deletions frameworks/dart-io/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
103 changes: 103 additions & 0 deletions frameworks/dart-io/benchmark_http_server.dart
Original file line number Diff line number Diff line change
@@ -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<int> bytes) {
if (bytes.isEmpty) return 0;
return int.tryParse(utf8.decode(bytes).trim()) ?? 0;
}

List<Map<String, dynamic>> _loadDataset(String path) {
try {
final decoded = jsonDecode(File(path).readAsStringSync()) as List;
return decoded
.map((e) => Map<String, dynamic>.from(e as Map))
.toList(growable: false);
} catch (_) {
return const <Map<String, dynamic>>[];
}
}

Uint8List _jsonPayload(List<Map<String, dynamic>> items, int count, int m) {
final clamped = count.clamp(0, items.length).toInt();
final out = List<Map<String, dynamic>>.generate(clamped, (i) {
final item = items[i];
final price = item['price'] as num;
final quantity = item['quantity'] as num;
return <String, dynamic>{
'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(<String, dynamic>{'items': out, 'count': out.length})),
);
}

Future<void> main(List<String> 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<List<int>>(<int>[], (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();
}
}
14 changes: 14 additions & 0 deletions frameworks/dart-io/build.sh
Original file line number Diff line number Diff line change
@@ -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"
22 changes: 22 additions & 0 deletions frameworks/dart-io/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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"
18 changes: 18 additions & 0 deletions frameworks/dart-io/meta.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
5 changes: 5 additions & 0 deletions frameworks/dart-io/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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"
20 changes: 20 additions & 0 deletions frameworks/dart-io/reuseport_shim.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stddef.h>
#include <sys/socket.h>

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);
}
36 changes: 36 additions & 0 deletions frameworks/dart-zig/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
45 changes: 45 additions & 0 deletions frameworks/dart-zig/README.md
Original file line number Diff line number Diff line change
@@ -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"`.
Loading