name: Runners
on:
workflow_dispatch:
inputs:
benchmark_size:
description: Benchmark workload to run
required: false
type: choice
options:
- full
- preview
- single
- saturation
default: full
publish_to_r2:
description: Publish main or release-tag runs to Cloudflare R2
required: false
type: boolean
default: false
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUST_VERSION: "1.95"
NODE_VERSION: "24"
BENCHMARK_SIZE: ${{ inputs.benchmark_size || 'full' }}
BENCH_COHORT_ID: gh-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.sha }}-throughput-http
PUBLISH_TO_R2: ${{ inputs.publish_to_r2 && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }}
R2_BUCKET: drizzle-bench
jobs:
plan:
name: Plan
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
size: ${{ steps.resolve.outputs.size }}
class: ${{ steps.resolve.outputs.class }}
workload: ${{ steps.resolve.outputs.workload }}
publish: ${{ steps.resolve.outputs.publish }}
paced_trials: ${{ steps.resolve.outputs.paced_trials }}
cross_family: ${{ steps.resolve.outputs.cross_family }}
cross_workload: ${{ steps.resolve.outputs.cross_workload }}
cross_trials: ${{ steps.resolve.outputs.cross_trials }}
steps:
- uses: actions/checkout@v6
- name: Resolve run class and workload
id: resolve
shell: bash
run: |
size="${BENCHMARK_SIZE}"
publish="${PUBLISH_TO_R2}"
class="full"
workload="bench/spec/workload.throughput.v1.json"
if [[ "$size" == "preview" && "$publish" != "true" ]]; then
class="small"
workload="bench/spec/workload.preview.v1.json"
elif [[ "$size" == "single" ]]; then
workload="bench/spec/workload.single-throughput.v1.json"
if [[ "$publish" == "true" ]]; then
class="publish"
fi
elif [[ "$size" == "saturation" ]]; then
# Capacity, not latency-at-fixed-load: an unpaced ramp until p99
# breaches the workload's SLO. This is the only suite that can
# produce a throughput number at all — the paced suites cap every
# target at `VUs / think_time`, so their rps describes the load
# generator rather than the target.
workload="bench/spec/workload.saturation.v1.json"
if [[ "$publish" == "true" ]]; then
class="publish"
fi
elif [[ "$publish" == "true" ]]; then
class="publish"
fi
# The workflow now runs only by explicit dispatch. Release creation
# dispatches a full publish run, while a person can select `preview`
# from a branch to exercise the topology cheaply. Every run includes
# the cross-family jobs so the manual and release paths stay identical.
cross_family="true"
# The `*-all` jobs always run the saturation ramp, whatever `$workload`
# resolved to for the per-family jobs. A cross-family ranking needs a
# number that describes the target, and the paced suites cap every
# target at `VUs / think_time` — their rps describes the load
# generator. The paced reading is not lost: the per-family jobs still
# produce it, and it is what the latency views are built from.
cross_workload="bench/spec/workload.saturation.v1.json"
cross_trials="2"
if [[ "$size" == "preview" ]]; then
# Smoke-test the topology itself without a five-hour run.
cross_workload="bench/spec/workload.saturation-preview.v1.json"
cross_trials="1"
fi
# Paced trials: 3 for full/publish, class default (3) for small.
# 5 trials does not fit any grouped-job structure inside GitHub's
# 360-minute cancellation (27 targets x 5 x ~7 min is 15.6 h on
# Linux alone). Measured cost of 3 vs 5 on the recorded five-trial
# cohorts: rps.avg medians move <=2.6% (median 0.1%), whole-ramp p95
# <=12.5% (median 1.1%); the sustained-latency reference median is
# the loosest — its per-trial jitter measured 5-39% on slower
# targets — and spread.trials discloses the count either way.
paced_trials=""
if [[ "$class" != "small" ]]; then
paced_trials="3"
fi
{
echo "size=$size"
echo "class=$class"
echo "workload=$workload"
echo "publish=$publish"
echo "paced_trials=$paced_trials"
echo "cross_family=$cross_family"
echo "cross_workload=$cross_workload"
echo "cross_trials=$cross_trials"
} >> "$GITHUB_OUTPUT"
echo "size=$size class=$class workload=$workload publish=$publish cross_family=$cross_family"
echo "cross_workload=$cross_workload cross_trials=$cross_trials"
- name: Check per-job time budgets
shell: bash
env:
PACED_WORKLOAD: ${{ steps.resolve.outputs.workload }}
PACED_TRIALS: ${{ steps.resolve.outputs.paced_trials }}
CROSS_FAMILY: ${{ steps.resolve.outputs.cross_family }}
CROSS_WORKLOAD: ${{ steps.resolve.outputs.cross_workload }}
CROSS_TRIALS: ${{ steps.resolve.outputs.cross_trials }}
CLASS: ${{ steps.resolve.outputs.class }}
run: |
python3 - <<'PY'
import json, os, sys
SPEC = {
"sqlite": "bench/spec/targets.sqlite.v1.json",
"sqlite-ts": "bench/spec/targets.sqlite-ts.v1.json",
"libsql": "bench/spec/targets.libsql.v1.json",
"turso": "bench/spec/targets.turso.v1.json",
"postgres": "bench/spec/targets.postgres.v1.json",
"postgres-rust-orms": "bench/spec/targets.postgres-rust-orms.v1.json",
"postgres-ts": "bench/spec/targets.postgres-ts.v1.json",
"spacetimedb": "bench/spec/targets.spacetimedb.v1.json",
}
small = os.environ["CLASS"] == "small"
# Empty means "class default", which is 3 for small.
paced_trials = int(os.environ["PACED_TRIALS"] or "3")
cross_trials = int(os.environ["CROSS_TRIALS"])
cross = os.environ["CROSS_FAMILY"] == "true"
# Keep in sync with the job definitions below: (job, families,
# workload env var, trials, timeout minutes, runs on this event).
JOBS = [
("paced-linux-embedded", ["sqlite", "sqlite-ts", "turso"],
"PACED_WORKLOAD", paced_trials, 150 if small else 300, True),
("paced-linux-isolated", ["libsql", "spacetimedb"],
"PACED_WORKLOAD", paced_trials, 150 if small else 240, True),
("paced-linux-postgres", ["postgres", "postgres-ts", "postgres-rust-orms"],
"PACED_WORKLOAD", paced_trials, 150 if small else 350, True),
("paced-desktop-embedded", ["sqlite", "sqlite-ts", "turso", "spacetimedb"],
"PACED_WORKLOAD", paced_trials, 150 if small else 350, True),
("paced-desktop-postgres", ["postgres", "postgres-ts", "postgres-rust-orms"],
"PACED_WORKLOAD", paced_trials, 150 if small else 350, True),
("linux-all", list(SPEC),
"CROSS_WORKLOAD", cross_trials, 350, cross),
("desktop-all", [f for f in SPEC if f != "libsql"],
"CROSS_WORKLOAD", cross_trials, 350, cross),
]
# Per-target-per-trial cost that is not the ramp itself: process
# spawn, seed, LISTENING wait, teardown. Run 31773786939 measured
# ~10-15 s with a hot cargo cache; padded to double.
OVERHEAD_S = 30
# Toolchain install, cargo release build of the runner plus the
# external target crates, bun install, database and SpacetimeDB
# setup — the cold-cache day, not the usual one.
BUILD_MIN = 55
failed = False
for job, families, workload_env, trials, timeout, active in JOBS:
if not active:
continue
workload = json.load(open(os.environ[workload_env]))
ramp_s = sum(s["sec"] for s in workload.get("stages", []))
targets = sum(len(json.load(open(SPEC[f]))) for f in families)
total = round(targets * trials * (ramp_s + OVERHEAD_S) / 60 + BUILD_MIN)
status = "ok" if total <= timeout - 10 else "OVER BUDGET"
print(
f"{job}: {targets} targets x {trials} trials x "
f"({ramp_s}s ramp + {OVERHEAD_S}s overhead) + {BUILD_MIN} min build "
f"= ~{total} min (timeout {timeout}) {status}"
)
if total > timeout - 10:
print(
f"::error::{job} needs ~{total} min against a {timeout} min timeout. "
f"Lower trials, regroup its families, or split the job — GitHub "
f"hard-cancels at 360 and the ranking would come back partial."
)
failed = True
if failed:
sys.exit(1)
PY
paced-linux-embedded:
name: Paced embedded (linux)
needs: plan
runs-on: ubuntu-latest
timeout-minutes: ${{ needs.plan.outputs.class == 'small' && 150 || 300 }}
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: true
package-manager-cache: false
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache Rust dependencies
if: ${{ !env.ACT }}
uses: Swatinem/rust-cache@v2
with:
shared-key: runner-paced-linux-embedded
save-if: ${{ env.PUBLISH_TO_R2 == 'true' }}
- name: Install TS target dependencies
uses: ./.github/actions/install-ts-targets
- name: Benchmark SQLite family
uses: ./.github/actions/bench-runner-run
with:
family: sqlite
targets: bench/spec/targets.sqlite.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
- name: Benchmark SQLite TS family
uses: ./.github/actions/bench-runner-run
with:
family: sqlite-ts
targets: bench/spec/targets.sqlite-ts.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
- name: Benchmark Turso family
uses: ./.github/actions/bench-runner-run
with:
family: turso
targets: bench/spec/targets.turso.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
prebuild-manifests: bench/targets/toasty/Cargo.toml
paced-linux-isolated:
name: Paced libSQL + SpacetimeDB (linux)
needs: plan
runs-on: ubuntu-latest
timeout-minutes: ${{ needs.plan.outputs.class == 'small' && 150 || 240 }}
env:
SPACETIME_URI: "ws://127.0.0.1:3000"
SPACETIME_MODULE: "bench-module"
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Cache Rust dependencies
if: ${{ !env.ACT }}
uses: Swatinem/rust-cache@v2
with:
shared-key: runner-paced-linux-isolated
save-if: ${{ env.PUBLISH_TO_R2 == 'true' }}
- name: Benchmark libSQL family
uses: ./.github/actions/bench-runner-run
with:
family: libsql
targets: bench/spec/targets.libsql.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
runner-features: libsql
- name: Install SpacetimeDB CLI
uses: ./.github/actions/install-spacetime
- name: Start SpacetimeDB
shell: bash
run: |
rm -rf "$HOME/.local/share/spacetime/data" "$HOME/.config/spacetime"
pkill -f spacetimedb-standalone 2>/dev/null || true
sleep 1
spacetime start --listen-addr 127.0.0.1:3000 --pg-port 5433 &
for i in $(seq 1 30); do
if spacetime server ping http://127.0.0.1:3000 2>/dev/null; then
echo "SpacetimeDB WebSocket ready"
break
fi
sleep 1
done
for i in $(seq 1 15); do
if (echo > /dev/tcp/127.0.0.1/5433) 2>/dev/null; then
echo "SpacetimeDB PGWire ready on :5433"
break
fi
sleep 1
done
- name: Build and publish SpacetimeDB module
shell: bash
run: |
spacetime build -p bench/targets/spacetime-module
spacetime publish bench-module -p bench/targets/spacetime-module --server http://127.0.0.1:3000
- name: Extract SpacetimeDB identity token
id: spacetime-token
shell: bash
run: |
config="$HOME/.config/spacetime/cli.toml"
if [[ ! -f "$config" ]]; then
echo "WARNING: $config not found"
exit 0
fi
token=$(sed -n 's/^[[:space:]]*spacetimedb_token[[:space:]]*=[[:space:]]*"\(.*\)"/\1/p' "$config" | head -n1)
if [[ -z "$token" ]]; then
echo "WARNING: No token found in $config"
cat "$config"
exit 0
fi
echo "::add-mask::$token"
echo "SPACETIME_TOKEN=$token" >> "$GITHUB_ENV"
- name: Benchmark SpacetimeDB family
uses: ./.github/actions/bench-runner-run
with:
family: spacetimedb
targets: bench/spec/targets.spacetimedb.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
prebuild-manifests: bench/targets/spacetime-native-rs/Cargo.toml
paced-linux-postgres:
name: Paced PostgreSQL (linux)
needs: plan
runs-on: ubuntu-latest
timeout-minutes: ${{ needs.plan.outputs.class == 'small' && 150 || 350 }}
services:
postgres:
image: postgres:18-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: drizzle_test
ports:
- 5432/tcp
options: >-
--cpuset-cpus 3
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: true
package-manager-cache: false
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache Rust dependencies
if: ${{ !env.ACT }}
uses: Swatinem/rust-cache@v2
with:
shared-key: runner-paced-linux-postgres
save-if: ${{ env.PUBLISH_TO_R2 == 'true' }}
- name: Install TS target dependencies
uses: ./.github/actions/install-ts-targets
- name: Resolve DATABASE_URL
id: pgurl
uses: ./.github/actions/resolve-postgres-url
with:
port: ${{ job.services.postgres.ports[5432] }}
service-id: ${{ job.services.postgres.id }}
- name: Benchmark PostgreSQL family
uses: ./.github/actions/bench-runner-run
with:
family: postgres
targets: bench/spec/targets.postgres.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
database-url: ${{ steps.pgurl.outputs.url }}
db-cpuset: '3'
- name: Benchmark PostgreSQL TS family
uses: ./.github/actions/bench-runner-run
with:
family: postgres-ts
targets: bench/spec/targets.postgres-ts.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
database-url: ${{ steps.pgurl.outputs.url }}
db-cpuset: '3'
- name: Benchmark PostgreSQL Rust ORM family
uses: ./.github/actions/bench-runner-run
with:
family: postgres-rust-orms
targets: bench/spec/targets.postgres-rust-orms.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
database-url: ${{ steps.pgurl.outputs.url }}
db-cpuset: '3'
prebuild-manifests: |
bench/targets/rust-pg-orms/Cargo.toml
bench/targets/toasty/Cargo.toml
paced-desktop-embedded:
name: Paced embedded (${{ matrix.platform }})
needs: plan
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
platform: windows
- os: macos-latest
platform: macos
runs-on: ${{ matrix.os }}
timeout-minutes: ${{ needs.plan.outputs.class == 'small' && 150 || 350 }}
env:
SPACETIME_URI: "ws://127.0.0.1:3000"
SPACETIME_MODULE: "bench-module"
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: true
package-manager-cache: false
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache Rust dependencies
if: ${{ !env.ACT }}
uses: Swatinem/rust-cache@v2
with:
shared-key: runner-paced-desktop-${{ matrix.platform }}
save-if: ${{ env.PUBLISH_TO_R2 == 'true' }}
- name: Install TS target dependencies
uses: ./.github/actions/install-ts-targets
- name: Benchmark SQLite family
uses: ./.github/actions/bench-runner-run
with:
family: sqlite
platform: ${{ matrix.platform }}
targets: bench/spec/targets.sqlite.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
- name: Benchmark SQLite TS family
uses: ./.github/actions/bench-runner-run
with:
family: sqlite-ts
platform: ${{ matrix.platform }}
targets: bench/spec/targets.sqlite-ts.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
- name: Benchmark Turso family
uses: ./.github/actions/bench-runner-run
with:
family: turso
platform: ${{ matrix.platform }}
targets: bench/spec/targets.turso.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
prebuild-manifests: bench/targets/toasty/Cargo.toml
- name: Install SpacetimeDB CLI
uses: ./.github/actions/install-spacetime
- name: Start SpacetimeDB
shell: bash
run: |
if [[ "$RUNNER_OS" == "Windows" ]]; then
# A process backgrounded from a bash step dies with the step's
# console on Windows; Start-Process detaches it properly.
powershell -NoProfile -Command \
"Start-Process spacetime -WindowStyle Hidden -ArgumentList 'start','--listen-addr','127.0.0.1:3000','--pg-port','5433'"
else
rm -rf "$HOME/.local/share/spacetime/data" "$HOME/.config/spacetime"
nohup spacetime start --listen-addr 127.0.0.1:3000 --pg-port 5433 >/dev/null 2>&1 &
fi
for i in $(seq 1 60); do
if spacetime server ping http://127.0.0.1:3000 2>/dev/null; then
echo "SpacetimeDB WebSocket ready"
break
fi
sleep 1
done
spacetime server ping http://127.0.0.1:3000
- name: Build and publish SpacetimeDB module
shell: bash
run: |
spacetime build -p bench/targets/spacetime-module
spacetime publish bench-module -p bench/targets/spacetime-module --server http://127.0.0.1:3000
- name: Extract SpacetimeDB identity token
shell: bash
run: |
config="$HOME/.config/spacetime/cli.toml"
if [[ "$RUNNER_OS" == "Windows" ]]; then
config="$LOCALAPPDATA/SpacetimeDB/config/cli.toml"
[[ -f "$config" ]] || config="$HOME/.config/spacetime/cli.toml"
fi
if [[ ! -f "$config" ]]; then
echo "WARNING: spacetime cli.toml not found"
exit 0
fi
token=$(sed -n 's/^[[:space:]]*spacetimedb_token[[:space:]]*=[[:space:]]*"\(.*\)"/\1/p' "$config" | head -n1)
if [[ -z "$token" ]]; then
echo "WARNING: No token found in $config"
exit 0
fi
echo "::add-mask::$token"
echo "SPACETIME_TOKEN=$token" >> "$GITHUB_ENV"
- name: Benchmark SpacetimeDB family
uses: ./.github/actions/bench-runner-run
with:
family: spacetimedb
platform: ${{ matrix.platform }}
targets: bench/spec/targets.spacetimedb.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
prebuild-manifests: bench/targets/spacetime-native-rs/Cargo.toml
paced-desktop-postgres:
name: Paced PostgreSQL (${{ matrix.platform }})
needs: plan
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
platform: windows
- os: macos-latest
platform: macos
runs-on: ${{ matrix.os }}
timeout-minutes: ${{ needs.plan.outputs.class == 'small' && 150 || 350 }}
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: true
package-manager-cache: false
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache Rust dependencies
if: ${{ !env.ACT }}
uses: Swatinem/rust-cache@v2
with:
shared-key: runner-paced-desktop-pg-${{ matrix.platform }}
save-if: ${{ env.PUBLISH_TO_R2 == 'true' }}
- name: Install TS target dependencies
uses: ./.github/actions/install-ts-targets
- name: Set up PostgreSQL 18
id: pgnative
uses: ./.github/actions/setup-postgres-native
- name: Benchmark PostgreSQL family
uses: ./.github/actions/bench-runner-run
with:
family: postgres
platform: ${{ matrix.platform }}
targets: bench/spec/targets.postgres.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
database-url: ${{ steps.pgnative.outputs.url }}
- name: Benchmark PostgreSQL TS family
uses: ./.github/actions/bench-runner-run
with:
family: postgres-ts
platform: ${{ matrix.platform }}
targets: bench/spec/targets.postgres-ts.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
database-url: ${{ steps.pgnative.outputs.url }}
- name: Benchmark PostgreSQL Rust ORM family
uses: ./.github/actions/bench-runner-run
with:
family: postgres-rust-orms
platform: ${{ matrix.platform }}
targets: bench/spec/targets.postgres-rust-orms.v1.json
workload: ${{ needs.plan.outputs.workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.paced_trials }}
database-url: ${{ steps.pgnative.outputs.url }}
prebuild-manifests: |
bench/targets/rust-pg-orms/Cargo.toml
bench/targets/toasty/Cargo.toml
linux-all:
name: All families (linux)
needs: plan
if: ${{ needs.plan.outputs.cross_family == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 350
env:
SPACETIME_URI: "ws://127.0.0.1:3000"
SPACETIME_MODULE: "bench-module"
services:
postgres:
image: postgres:18-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: drizzle_test
ports:
- 5432/tcp
options: >-
--cpuset-cpus 3
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: true
package-manager-cache: false
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache Rust dependencies
if: ${{ !env.ACT }}
uses: Swatinem/rust-cache@v2
with:
shared-key: runner-linux-all
save-if: ${{ env.PUBLISH_TO_R2 == 'true' }}
- name: Install TS target dependencies
uses: ./.github/actions/install-ts-targets
- name: Resolve DATABASE_URL
id: pgurl
uses: ./.github/actions/resolve-postgres-url
with:
port: ${{ job.services.postgres.ports[5432] }}
service-id: ${{ job.services.postgres.id }}
- name: Benchmark SQLite family
uses: ./.github/actions/bench-runner-run
with:
family: sqlite
slug-suffix: cross
targets: bench/spec/targets.sqlite.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
runner-features: libsql
- name: Benchmark SQLite TS family
uses: ./.github/actions/bench-runner-run
with:
family: sqlite-ts
slug-suffix: cross
targets: bench/spec/targets.sqlite-ts.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
runner-features: libsql
- name: Benchmark libSQL family
uses: ./.github/actions/bench-runner-run
with:
family: libsql
slug-suffix: cross
targets: bench/spec/targets.libsql.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
runner-features: libsql
- name: Benchmark Turso family
uses: ./.github/actions/bench-runner-run
with:
family: turso
slug-suffix: cross
targets: bench/spec/targets.turso.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
runner-features: libsql
prebuild-manifests: bench/targets/toasty/Cargo.toml
- name: Benchmark PostgreSQL family
uses: ./.github/actions/bench-runner-run
with:
family: postgres
slug-suffix: cross
targets: bench/spec/targets.postgres.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
runner-features: libsql
database-url: ${{ steps.pgurl.outputs.url }}
db-cpuset: '3'
- name: Benchmark PostgreSQL Rust ORM family
uses: ./.github/actions/bench-runner-run
with:
family: postgres-rust-orms
slug-suffix: cross
targets: bench/spec/targets.postgres-rust-orms.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
runner-features: libsql
database-url: ${{ steps.pgurl.outputs.url }}
db-cpuset: '3'
prebuild-manifests: |
bench/targets/rust-pg-orms/Cargo.toml
bench/targets/toasty/Cargo.toml
- name: Benchmark PostgreSQL TS family
uses: ./.github/actions/bench-runner-run
with:
family: postgres-ts
slug-suffix: cross
targets: bench/spec/targets.postgres-ts.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
runner-features: libsql
database-url: ${{ steps.pgurl.outputs.url }}
db-cpuset: '3'
- name: Install SpacetimeDB CLI
uses: ./.github/actions/install-spacetime
- name: Start SpacetimeDB
shell: bash
run: |
rm -rf "$HOME/.local/share/spacetime/data" "$HOME/.config/spacetime"
pkill -f spacetimedb-standalone 2>/dev/null || true
sleep 1
taskset -c 3 spacetime start --listen-addr 127.0.0.1:3000 --pg-port 5433 &
for i in $(seq 1 30); do
if spacetime server ping http://127.0.0.1:3000 2>/dev/null; then
echo "SpacetimeDB WebSocket ready"
break
fi
sleep 1
done
for i in $(seq 1 15); do
if (echo > /dev/tcp/127.0.0.1/5433) 2>/dev/null; then
echo "SpacetimeDB PGWire ready on :5433"
break
fi
sleep 1
done
- name: Build and publish SpacetimeDB module
shell: bash
run: |
spacetime build -p bench/targets/spacetime-module
spacetime publish bench-module -p bench/targets/spacetime-module --server http://127.0.0.1:3000
- name: Extract SpacetimeDB identity token
id: spacetime-token
shell: bash
run: |
config="$HOME/.config/spacetime/cli.toml"
if [[ ! -f "$config" ]]; then
echo "WARNING: $config not found"
exit 0
fi
token=$(sed -n 's/^[[:space:]]*spacetimedb_token[[:space:]]*=[[:space:]]*"\(.*\)"/\1/p' "$config" | head -n1)
if [[ -z "$token" ]]; then
echo "WARNING: No token found in $config"
cat "$config"
exit 0
fi
echo "::add-mask::$token"
echo "SPACETIME_TOKEN=$token" >> "$GITHUB_ENV"
- name: Benchmark SpacetimeDB family
uses: ./.github/actions/bench-runner-run
with:
family: spacetimedb
slug-suffix: cross
targets: bench/spec/targets.spacetimedb.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
runner-features: libsql
db-cpuset: '3'
prebuild-manifests: bench/targets/spacetime-native-rs/Cargo.toml
desktop-all:
name: All families (${{ matrix.platform }})
needs: plan
if: ${{ needs.plan.outputs.cross_family == 'true' }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
platform: macos
- os: windows-latest
platform: windows
runs-on: ${{ matrix.os }}
timeout-minutes: 350
env:
SPACETIME_URI: "ws://127.0.0.1:3000"
SPACETIME_MODULE: "bench-module"
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: true
package-manager-cache: false
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache Rust dependencies
if: ${{ !env.ACT }}
uses: Swatinem/rust-cache@v2
with:
shared-key: runner-desktop-all-${{ matrix.platform }}
save-if: ${{ env.PUBLISH_TO_R2 == 'true' }}
- name: Install TS target dependencies
uses: ./.github/actions/install-ts-targets
- name: Benchmark SQLite family
uses: ./.github/actions/bench-runner-run
with:
family: sqlite
platform: ${{ matrix.platform }}
slug-suffix: cross
targets: bench/spec/targets.sqlite.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
- name: Benchmark SQLite TS family
uses: ./.github/actions/bench-runner-run
with:
family: sqlite-ts
platform: ${{ matrix.platform }}
slug-suffix: cross
targets: bench/spec/targets.sqlite-ts.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
- name: Benchmark Turso family
uses: ./.github/actions/bench-runner-run
with:
family: turso
platform: ${{ matrix.platform }}
slug-suffix: cross
targets: bench/spec/targets.turso.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
prebuild-manifests: bench/targets/toasty/Cargo.toml
- name: Set up PostgreSQL 18
id: pgnative
uses: ./.github/actions/setup-postgres-native
- name: Benchmark PostgreSQL family
uses: ./.github/actions/bench-runner-run
with:
family: postgres
platform: ${{ matrix.platform }}
slug-suffix: cross
targets: bench/spec/targets.postgres.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
database-url: ${{ steps.pgnative.outputs.url }}
- name: Benchmark PostgreSQL TS family
uses: ./.github/actions/bench-runner-run
with:
family: postgres-ts
platform: ${{ matrix.platform }}
slug-suffix: cross
targets: bench/spec/targets.postgres-ts.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
database-url: ${{ steps.pgnative.outputs.url }}
- name: Benchmark PostgreSQL Rust ORM family
uses: ./.github/actions/bench-runner-run
with:
family: postgres-rust-orms
platform: ${{ matrix.platform }}
slug-suffix: cross
targets: bench/spec/targets.postgres-rust-orms.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
database-url: ${{ steps.pgnative.outputs.url }}
prebuild-manifests: |
bench/targets/rust-pg-orms/Cargo.toml
bench/targets/toasty/Cargo.toml
- name: Install SpacetimeDB CLI
uses: ./.github/actions/install-spacetime
- name: Start SpacetimeDB
shell: bash
run: |
if [[ "$RUNNER_OS" == "Windows" ]]; then
# A process backgrounded from a bash step dies with the step's
# console on Windows; Start-Process detaches it properly.
powershell -NoProfile -Command \
"Start-Process spacetime -WindowStyle Hidden -ArgumentList 'start','--listen-addr','127.0.0.1:3000','--pg-port','5433'"
else
rm -rf "$HOME/.local/share/spacetime/data" "$HOME/.config/spacetime"
nohup spacetime start --listen-addr 127.0.0.1:3000 --pg-port 5433 >/dev/null 2>&1 &
fi
for i in $(seq 1 60); do
if spacetime server ping http://127.0.0.1:3000 2>/dev/null; then
echo "SpacetimeDB WebSocket ready"
break
fi
sleep 1
done
spacetime server ping http://127.0.0.1:3000
- name: Build and publish SpacetimeDB module
shell: bash
run: |
spacetime build -p bench/targets/spacetime-module
spacetime publish bench-module -p bench/targets/spacetime-module --server http://127.0.0.1:3000
- name: Extract SpacetimeDB identity token
shell: bash
run: |
config="$HOME/.config/spacetime/cli.toml"
if [[ "$RUNNER_OS" == "Windows" ]]; then
config="$LOCALAPPDATA/SpacetimeDB/config/cli.toml"
[[ -f "$config" ]] || config="$HOME/.config/spacetime/cli.toml"
fi
if [[ ! -f "$config" ]]; then
echo "WARNING: spacetime cli.toml not found"
exit 0
fi
token=$(sed -n 's/^[[:space:]]*spacetimedb_token[[:space:]]*=[[:space:]]*"\(.*\)"/\1/p' "$config" | head -n1)
if [[ -z "$token" ]]; then
echo "WARNING: No token found in $config"
exit 0
fi
echo "::add-mask::$token"
echo "SPACETIME_TOKEN=$token" >> "$GITHUB_ENV"
- name: Benchmark SpacetimeDB family
uses: ./.github/actions/bench-runner-run
with:
family: spacetimedb
platform: ${{ matrix.platform }}
slug-suffix: cross
targets: bench/spec/targets.spacetimedb.v1.json
workload: ${{ needs.plan.outputs.cross_workload }}
class: ${{ needs.plan.outputs.class }}
cohort-id: ${{ env.BENCH_COHORT_ID }}-cross
publish: ${{ needs.plan.outputs.publish }}
trials: ${{ needs.plan.outputs.cross_trials }}
prebuild-manifests: bench/targets/spacetime-native-rs/Cargo.toml
dashboard-preview:
name: Dashboard preview data
runs-on: ubuntu-latest
timeout-minutes: 30
needs:
- plan
- paced-linux-embedded
- paced-linux-isolated
- paced-linux-postgres
- paced-desktop-embedded
- paced-desktop-postgres
- linux-all
- desktop-all
if: ${{ always() && !cancelled() }}
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
check-latest: true
package-manager-cache: false
- name: Download runner artifacts
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: runner-*
path: bench-preview/artifacts
- name: Build publish tool
shell: bash
run: |
cargo build --release -p bench-runner
bin="$GITHUB_WORKSPACE/target/release/bench-runner"
if [[ "$RUNNER_OS" == "Windows" ]]; then
bin="${bin}.exe"
fi
echo "BENCH_RUNNER_BIN=$bin" >> "$GITHUB_ENV"
- name: Assemble dashboard object store
shell: bash
run: |
out="bench-out/dashboard-data"
index="$out/index.json"
rm -rf "$out"
mkdir -p "$out/runs"
echo '{"version":"v1","runs":[]}' > "$index"
manifests=()
if [[ -d bench-preview/artifacts ]]; then
mapfile -t manifests < <(find bench-preview/artifacts -name manifest.json | sort)
fi
assembled=0
skipped=()
for manifest in "${manifests[@]}"; do
run_dir="$(dirname "$manifest")"
run_id="$(sed -n 's/^[[:space:]]*"run_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -n1)"
if [[ -z "$run_id" ]]; then
echo "::warning::manifest is missing run_id, skipping: $manifest"
skipped+=("$manifest")
continue
fi
dest="$out/runs/$run_id"
mkdir -p "$dest"
cp -R "$run_dir"/. "$dest"/
if ! "$BENCH_RUNNER_BIN" publish --run "$dest" --index "$index"; then
echo "::warning::publish failed for $run_id, dropping it from the index"
rm -rf "$dest"
skipped+=("$manifest")
continue
fi
assembled=$(( assembled + 1 ))
done
if (( assembled == 0 )); then
echo "::error::no runner manifests could be assembled (${#manifests[@]} found, ${#skipped[@]} skipped)"
exit 1
fi
if (( ${#skipped[@]} > 0 )); then
echo "::warning::assembled $assembled run(s); skipped ${#skipped[@]}"
fi
echo "Dashboard data contains $assembled run(s):"
find "$out" -maxdepth 3 -type f | sort
- name: Upload dashboard preview data
uses: actions/upload-artifact@v7
with:
name: dashboard-bench-data
path: bench-out/dashboard-data
retention-days: 30
- name: Publish dashboard data to R2
if: ${{ env.PUBLISH_TO_R2 == 'true' && !env.ACT }}
shell: bash
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
if [[ -z "$CLOUDFLARE_API_TOKEN" ]]; then
echo "No R2 credentials, skipping publish"
exit 0
fi
# Install once, outside the workspace. `npx wrangler` per object
# re-resolved the package for every file in the store.
npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/wrangler" 'wrangler@^4.116.0'
wrangler="$RUNNER_TEMP/wrangler/node_modules/.bin/wrangler"
# --remote is load-bearing: wrangler v4 defaults `r2 object put` to
# LOCAL simulated storage, which "succeeds" while publishing nothing.
#
# A store is hundreds of objects, so a single transient upstream blip
# (R2 has returned 10001 "internal error" here) would otherwise abort
# the whole publish under `set -e` and leave the bucket half-written.
# Each object retries with backoff; index.json is uploaded last so a
# partial store never becomes visible to the dashboard.
put_object() {
local key="$1" file="$2" attempt
for attempt in 1 2 3 4 5; do
if "$wrangler" r2 object put "$R2_BUCKET/$key" --file "$file" --remote; then
return 0
fi
echo "::warning::r2 put failed for $key (attempt $attempt), retrying"
sleep $(( attempt * 5 ))
done
echo "::error::r2 put failed for $key after 5 attempts"
return 1
}
failed=0
while read -r file; do
key="${file#bench-out/dashboard-data/}"
[[ "$key" == "index.json" ]] && continue
put_object "$key" "$file" || failed=1
done < <(find bench-out/dashboard-data -type f | sort)
if (( failed )); then
echo "::error::one or more objects failed to publish; index.json withheld"
exit 1
fi
put_object "index.json" "bench-out/dashboard-data/index.json"