#!/usr/bin/env sh set -eu root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) port="${ZDS_HTTP_BENCH_PORT:-2586}" requests="${ZDS_HTTP_BENCH_REQUESTS:-5000}" concurrency="${ZDS_HTTP_BENCH_CONCURRENCY:-50}" heavy_requests="${ZDS_HTTP_BENCH_HEAVY_REQUESTS:-500}" heavy_concurrency="${ZDS_HTTP_BENCH_HEAVY_CONCURRENCY:-10}" db="${TMPDIR:-/tmp}/zds-http-bench.sqlite3" blob_root="${TMPDIR:-/tmp}/zds-http-bench-blobs" log="${TMPDIR:-/tmp}/zds-http-bench.log" session_body="${TMPDIR:-/tmp}/zds-http-bench-create-session.json" blob_payload="${TMPDIR:-/tmp}/zds-http-bench-blob.jpg" base="http://127.0.0.1:${port}" cleanup() { if [ -n "${server_pid:-}" ]; then kill "$server_pid" 2>/dev/null || true wait "$server_pid" 2>/dev/null || true fi } trap cleanup EXIT INT TERM need() { command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2 exit 1 } } bench() { name="$1" url="$2" shift 2 echo echo "== $name ==" ab -l -n "$requests" -c "$concurrency" "$@" "$url" | awk ' /Complete requests:/ || /Failed requests:/ || /Requests per second:/ || /Time per request:/ || /Transfer rate:/ || /^ 50%/ || /^ 95%/ || /^ 99%/ { print } ' } bench_sized() { name="$1" url="$2" route_requests="$3" route_concurrency="$4" shift 4 echo echo "== $name ==" ab -l -n "$route_requests" -c "$route_concurrency" "$@" "$url" | awk ' /Complete requests:/ || /Failed requests:/ || /Requests per second:/ || /Time per request:/ || /Transfer rate:/ || /^ 50%/ || /^ 95%/ || /^ 99%/ { print } ' } need ab need curl need sqlite3 need zig cd "$root" rm -f "$db" "$db-wal" "$db-shm" "$log" "$session_body" "$blob_payload" rm -rf "$blob_root" mkdir -p "$blob_root" zig build -Doptimize=ReleaseFast ./zig-out/bin/zds \ --host 127.0.0.1 \ --port "$port" \ --db "$db" \ --blobstore-path "$blob_root" \ --public-url "$base" \ --server-did did:web:localhost \ --handle-domains .test \ --admin-token http-bench-admin-token \ >"$log" 2>&1 & server_pid=$! i=0 while [ "$i" -lt 100 ]; do if curl -fsS "$base/xrpc/_health" >/dev/null 2>&1; then break fi i=$((i + 1)) sleep 0.1 done curl -fsS "$base/xrpc/_health" >/dev/null sqlite3 "$db" "insert into accounts (did, handle, email, password_hash, activated_at, email_confirmed_at) values ('did:plc:httpbench', 'http-bench.test', 'http-bench@test.com', 'password', unixepoch(), unixepoch())" sqlite3 "$db" "insert into oauth_requests (request_id, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, login_hint, expires_at) values ('http-bench-oauth', 'https://client.example/oauth-client.json', 'https://client.example/callback', 'repo:*?action=create blob:*/*', 'state', 'challenge', 'S256', 'http-bench.test', unixepoch() + 600)" printf '%s' '{"identifier":"http-bench.test","password":"password"}' > "$session_body" session=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createSession" \ -H 'content-type: application/json' \ --data @"$session_body") token=$(printf '%s' "$session" | sed -n 's/.*"accessJwt":"\([^"]*\)".*/\1/p') test -n "$token" curl -fsS -X POST "$base/xrpc/com.atproto.server.createInviteCode" \ -H "authorization: Bearer http-bench-admin-token" \ -H 'content-type: application/json' \ --data '{"useCount":1,"forAccount":"did:plc:httpbench"}' >/dev/null curl -fsS -X POST "$base/xrpc/app.bsky.actor.putPreferences" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \ --data '{"preferences":[{"$type":"app.bsky.actor.defs#contentLabelPref","label":"cats","visibility":"warn"}]}' >/dev/null create=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.createRecord" \ -H "authorization: Bearer $token" \ -H 'content-type: application/json' \ --data '{"repo":"did:plc:httpbench","collection":"app.bsky.feed.post","record":{"$type":"app.bsky.feed.post","text":"http bench","createdAt":"2026-05-31T00:00:00.000Z"}}') printf '%s' "$create" | grep -q '"validationStatus":"valid"' post_rkey=$(printf '%s' "$create" | sed -n 's#.*"uri":"at://did:plc:httpbench/app.bsky.feed.post/\([^"]*\)".*#\1#p') test -n "$post_rkey" printf '\377\330\377\340zds-http-bench' > "$blob_payload" blob=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.uploadBlob" \ -H "authorization: Bearer $token" \ -H 'content-type: image/jpeg' \ --data-binary @"$blob_payload") blob_cid=$(printf '%s' "$blob" | sed -n 's/.*"\$link":"\([^"]*\)".*/\1/p') test -n "$blob_cid" echo "zds HTTP benchmarks" echo "requests=$requests concurrency=$concurrency" echo "heavy_requests=$heavy_requests heavy_concurrency=$heavy_concurrency" echo "server_log=$log" bench "root" "$base/" bench "favicon" "$base/favicon.svg" bench "ogImage" "$base/og-image" bench "health" "$base/xrpc/_health" bench "didJson" "$base/.well-known/did.json" bench "atprotoDid" "$base/.well-known/atproto-did" bench "oauthProtectedResource" "$base/.well-known/oauth-protected-resource" bench "oauthAuthorizationServer" "$base/.well-known/oauth-authorization-server" bench "oauthJwks" "$base/oauth/jwks" bench "oauthAuthorizeJson" "$base/oauth/authorize?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Ahttp-bench-oauth" -H "accept: application/json" bench "describeServer" "$base/xrpc/com.atproto.server.describeServer" bench "createSession" "$base/xrpc/com.atproto.server.createSession" -p "$session_body" -T "application/json" bench "getSession" "$base/xrpc/com.atproto.server.getSession" -H "authorization: Bearer $token" bench "getServiceAuth" "$base/xrpc/com.atproto.server.getServiceAuth?aud=did%3Aplc%3Aappview&lxm=app.bsky.feed.getTimeline" -H "authorization: Bearer $token" bench "checkAccountStatus" "$base/xrpc/com.atproto.server.checkAccountStatus" -H "authorization: Bearer $token" bench "getAccountInviteCodes" "$base/xrpc/com.atproto.server.getAccountInviteCodes" -H "authorization: Bearer $token" bench "listAppPasswords" "$base/xrpc/com.atproto.server.listAppPasswords" -H "authorization: Bearer $token" bench "listPasskeys" "$base/xrpc/com.atproto.server.listPasskeys" -H "authorization: Bearer $token" bench "getPreferences" "$base/xrpc/app.bsky.actor.getPreferences" -H "authorization: Bearer $token" bench "resolveHandle" "$base/xrpc/com.atproto.identity.resolveHandle?handle=http-bench.test" bench "describeRepo" "$base/xrpc/com.atproto.repo.describeRepo?repo=did:plc:httpbench" bench "getRecord" "$base/xrpc/com.atproto.repo.getRecord?repo=did:plc:httpbench&collection=app.bsky.feed.post&rkey=$post_rkey" bench "listRecords" "$base/xrpc/com.atproto.repo.listRecords?repo=did:plc:httpbench&collection=app.bsky.feed.post&limit=50" -H "authorization: Bearer $token" bench "listMissingBlobs" "$base/xrpc/com.atproto.repo.listMissingBlobs?repo=did:plc:httpbench" -H "authorization: Bearer $token" bench "syncListRepos" "$base/xrpc/com.atproto.sync.listRepos?limit=50" bench "syncListBlobs" "$base/xrpc/com.atproto.sync.listBlobs?did=did:plc:httpbench&limit=50" bench "syncGetLatestCommit" "$base/xrpc/com.atproto.sync.getLatestCommit?did=did:plc:httpbench" bench "syncGetRepoStatus" "$base/xrpc/com.atproto.sync.getRepoStatus?did=did:plc:httpbench" bench_sized "syncGetBlob" "$base/xrpc/com.atproto.sync.getBlob?did=did:plc:httpbench&cid=$blob_cid" "$heavy_requests" "$heavy_concurrency" bench_sized "syncGetBlobHead" "$base/xrpc/com.atproto.sync.getBlob?did=did:plc:httpbench&cid=$blob_cid" "$heavy_requests" "$heavy_concurrency" -m HEAD bench_sized "syncGetRepo" "$base/xrpc/com.atproto.sync.getRepo?did=did:plc:httpbench" "$heavy_requests" "$heavy_concurrency" bench_sized "syncGetRepoHead" "$base/xrpc/com.atproto.sync.getRepo?did=did:plc:httpbench" "$heavy_requests" "$heavy_concurrency" -m HEAD