Files
Oxicloud/tests/load/run.sh
T
Edouard Vanbelle 0ef4c624c5 chore(load): start implementation of load tests
initial test from Ed's nuc:

    metric                                          pctl  baseline    current     delta    status
    -----------------------------------------------------------------------------------------------
    folder_cascade.list_depth1                      p50   0.3ms       0.3ms       -6.3%    ok
    folder_cascade.list_depth1                      p95   2.3ms       0.5ms       -75.7%   ok
    folder_cascade.list_depth1                      p99   4.7ms       2.5ms       -48.1%   ok
    folder_cascade.list_depth4                      p50   0.4ms       0.3ms       -10.0%   ok
    folder_cascade.list_depth4                      p95   0.9ms       0.6ms       -31.2%   ok
    folder_cascade.list_depth4                      p99   2.4ms       1.0ms       -56.7%   ok
    folder_cascade.list_depth8                      p50   0.3ms       0.3ms       -8.8%    ok
    folder_cascade.list_depth8                      p95   0.6ms       0.5ms       -22.2%   ok
    folder_cascade.list_depth8                      p99   1.9ms       0.5ms       -71.5%   ok
    folder_cascade.list_depth_deep                  p50   0.3ms       0.3ms       -5.0%    ok
    folder_cascade.list_depth_deep                  p95   0.6ms       0.5ms       -18.6%   ok
    folder_cascade.list_depth_deep                  p99   2.0ms       0.7ms       -67.2%   ok
    share_cascade_rebac.list_grants                 p50   0.4ms       0.3ms       -27.6%   ok
    share_cascade_rebac.list_grants                 p95   1.2ms       0.5ms       -57.0%   ok
    share_cascade_rebac.list_grants                 p99   1.7ms       1.1ms       -36.7%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p50   0.5ms       0.5ms       -11.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p95   1.1ms       0.7ms       -41.2%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p99   3.0ms       1.3ms       -58.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p50   0.5ms       0.5ms       -13.7%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p95   1.4ms       0.7ms       -50.5%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p99   2.2ms       1.1ms       -49.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p50   0.5ms       0.4ms       -16.0%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p95   0.9ms       0.7ms       -26.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p99   1.5ms       0.9ms       -40.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p50   0.5ms       0.4ms       -17.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p95   1.0ms       0.7ms       -31.2%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p99   1.6ms       0.8ms       -49.4%   ok
    subject_group_nested.fetch_as_member_depth1     p50   0.5ms       0.4ms       -8.4%    ok
    subject_group_nested.fetch_as_member_depth1     p95   0.6ms       0.6ms       -10.2%   ok
    subject_group_nested.fetch_as_member_depth1     p99   1.4ms       0.6ms       -55.2%   ok
    subject_group_nested.fetch_as_member_depth4     p50   0.5ms       0.5ms       -7.9%    ok
    subject_group_nested.fetch_as_member_depth4     p95   0.6ms       0.6ms       -4.0%    ok
    subject_group_nested.fetch_as_member_depth4     p99   0.7ms       0.6ms       -2.2%    ok
    subject_group_nested.fetch_as_member_depth8     p50   0.5ms       0.4ms       -8.9%    ok
    subject_group_nested.fetch_as_member_depth8     p95   0.5ms       0.6ms       +7.1%    ok
    subject_group_nested.fetch_as_member_depth8     p99   0.6ms       0.7ms       +10.3%   ok
    subject_group_nested.fetch_as_member_depth_deep p50   0.5ms       0.4ms       -7.6%    ok
    subject_group_nested.fetch_as_member_depth_deep p95   0.6ms       0.5ms       -11.9%   ok
    subject_group_nested.fetch_as_member_depth_deep p99   0.6ms       0.7ms       +8.9%    ok
2026-06-18 09:38:25 +02:00

167 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Full k6 load-test runner.
# Starts postgres + OxiCloud server, seeds fixtures, runs k6 scenarios,
# compares results against baseline/load.json, tears everything down.
#
# Usage (from repo root):
# bash tests/load/run.sh
#
# Env overrides:
# BUILD_TARGET=release # prefer release build for accurate timings
# LOAD_DEPTH=8 # override seeder shape (otherwise read from test.env)
# LOAD_FANOUT=3
# LOAD_FILES_PER_LEAF=3
# LOAD_EXTRA_USERS=20
# LOAD_GROUP_DEPTH=3
# LOAD_GROUP_FANOUT=5
# K6_SUMMARY_OUT=path # explicit output path (default: tests/load/results/<ts>.json)
#
# Prerequisites: docker, cargo, k6 >= 0.46, node >= 18
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
COMMON="$REPO_ROOT/tests/common"
LOAD_DIR="$REPO_ROOT/tests/load"
# shellcheck source=test.env
source "$LOAD_DIR/test.env"
SERVER_PORT="${base_url##*:}"
log() { echo "[load] $*"; }
die() { echo "[load] ERROR: $*" >&2; exit 1; }
wait_for_http() {
local url="$1" timeout="${2:-120}"
local deadline=$(( $(date +%s) + timeout ))
until curl -sf "$url" >/dev/null 2>&1; do
[[ $(date +%s) -ge $deadline ]] && die "Timeout waiting for $url"
sleep 1
done
}
command -v k6 >/dev/null 2>&1 || die "k6 is required (https://k6.io/docs/get-started/installation/)"
command -v node >/dev/null 2>&1 || die "node >= 18 is required for compare.mjs"
SERVER_PID=""
cleanup() {
if [[ -n "$SERVER_PID" ]]; then
log "Stopping OxiCloud server (pid $SERVER_PID)..."
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
bash "$COMMON/stop-db.sh" || true
}
trap cleanup EXIT
bash "$COMMON/spawn-db.sh"
# NOTE: do NOT run init-test-schema.sh here. The OxiCloud server applies
# sqlx migrations on startup; applying them via raw psql first leaves the
# server's _sqlx_migrations tracking table empty, which makes the second
# pass try to re-ALTER tables that already have the column (e.g. migration
# 20260507000000_session_family.sql), and the server panics on boot.
set -a
# shellcheck source=../common/server.env
source "$COMMON/server.env"
OXICLOUD_SERVER_PORT=$SERVER_PORT
OXICLOUD_STORAGE_PATH="$LOAD_DIR/storage"
set +a
rm -rf "$OXICLOUD_STORAGE_PATH"
mkdir -p "$OXICLOUD_STORAGE_PATH"
BUILD_TARGET="${BUILD_TARGET:-release}"
# Respect CARGO_TARGET_DIR for self-hosted runners that bind-mount target/
# outside the workspace (avoids actions/checkout EBUSY on the mount point).
TARGET_DIR="${CARGO_TARGET_DIR:-$REPO_ROOT/target}"
OXICLOUD_BIN="$TARGET_DIR/$BUILD_TARGET/oxicloud"
SEED_BIN="$TARGET_DIR/$BUILD_TARGET/load-seed"
# Build both bins in one invocation. `load_seed_bin` is an empty marker
# feature that gates the load-seed bin without affecting oxicloud's dep
# graph, so cargo plans a single workspace build and oxicloud compiles
# exactly once. (See Cargo.toml comments on `load_seed_bin`.)
if [[ ! -x "$OXICLOUD_BIN" || ! -x "$SEED_BIN" ]]; then
log "Building OxiCloud + load-seed ($BUILD_TARGET)..."
if [[ "$BUILD_TARGET" == "release" ]]; then
cargo build --release --features load_seed_bin --bin oxicloud --bin load-seed
else
cargo build --features load_seed_bin --bin oxicloud --bin load-seed
fi
fi
# Start the server FIRST so its sqlx::migrate! populates _sqlx_migrations
# against the clean DB. The seeder then runs against the migrated schema
# while the server is still alive (it issues plain INSERTs, no DDL).
log "Starting OxiCloud server ($BUILD_TARGET) on port $SERVER_PORT..."
"$OXICLOUD_BIN" &
SERVER_PID=$!
wait_for_http "$base_url/ready" 120
log "Server ready."
log "Seeding fixtures..."
DEPTH="${LOAD_DEPTH:-${load_depth:-5}}"
FANOUT="${LOAD_FANOUT:-${load_fanout:-4}}"
FILES_PER_LEAF="${LOAD_FILES_PER_LEAF:-${load_files_per_leaf:-3}}"
EXTRA_USERS="${LOAD_EXTRA_USERS:-${load_extra_users:-20}}"
GROUP_DEPTH="${LOAD_GROUP_DEPTH:-${load_group_depth:-3}}"
GROUP_FANOUT="${LOAD_GROUP_FANOUT:-${load_group_fanout:-5}}"
mkdir -p "$LOAD_DIR/results"
MANIFEST_PATH="$LOAD_DIR/results/seed-manifest.json"
"$SEED_BIN" \
--depth "$DEPTH" \
--fanout "$FANOUT" \
--files-per-leaf "$FILES_PER_LEAF" \
--extra-users "$EXTRA_USERS" \
--group-depth "$GROUP_DEPTH" \
--group-fanout "$GROUP_FANOUT" \
--password "${password:-TestPassword1!}" \
--manifest "$MANIFEST_PATH"
TS="$(date +%s)"
SUMMARY_OUT="${K6_SUMMARY_OUT:-$LOAD_DIR/results/run-$TS.json}"
export K6_BASE_URL="$base_url"
export K6_USERNAME="${username:-admin}"
export K6_PASSWORD="${password:-TestPassword1!}"
# k6 only accepts one script per invocation, so each scenario runs separately
# and we merge the summaries afterwards. Per-scenario summaries also make it
# easier to attribute regressions when looking at raw artifacts in CI.
SCENARIOS=(folder_cascade share_cascade_rebac subject_group_nested)
PARTIAL_SUMMARIES=()
K6_FAILED=0
for name in "${SCENARIOS[@]}"; do
partial="$LOAD_DIR/results/run-$TS-$name.json"
PARTIAL_SUMMARIES+=("$partial")
log "Running k6 scenario: $name"
# --summary-trend-stats forces p(99) into the summary export; k6's default
# only includes avg/min/med/max/p(90)/p(95), so without it baseline.p99 can
# never be baked or diffed.
k6 run \
--summary-export="$partial" \
--summary-trend-stats="avg,min,med,max,p(90),p(95),p(99)" \
--quiet \
"$LOAD_DIR/scenarios/$name.js" \
|| K6_FAILED=$?
done
log "Merging summaries -> $SUMMARY_OUT"
node "$LOAD_DIR/merge-summaries.mjs" "${PARTIAL_SUMMARIES[@]}" "$SUMMARY_OUT"
log "Comparing against baseline..."
COMPARE_RC=0
node "$LOAD_DIR/compare.mjs" "$SUMMARY_OUT" "$LOAD_DIR/baseline/load.json" || COMPARE_RC=$?
if [[ "$K6_FAILED" -ne 0 ]]; then
log "k6 reported threshold failures (exit $K6_FAILED)."
exit "$K6_FAILED"
fi
exit "$COMPARE_RC"