|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +cd "$(dirname "$0")/.." |
| 5 | + |
| 6 | +DB_URL="${DB_URL:-postgresql://postgres:postgres@localhost:5432/mimeme}" |
| 7 | +PG_CONTAINER="${PG_CONTAINER:-mimeme-postgres}" |
| 8 | +API_PORT="${API_PORT:-8000}" |
| 9 | +DURATION="${DURATION:-30s}" |
| 10 | +CONCURRENCY="${CONCURRENCY:-200}" |
| 11 | +OVERLOAD_CONCURRENCY="${OVERLOAD_CONCURRENCY:-400}" |
| 12 | +SEED_COUNT="${SEED_COUNT:-5000}" |
| 13 | +OHA_BIN="${OHA_BIN:-oha}" |
| 14 | +OUT_DIR="${OUT_DIR:-data/loadprobe}" |
| 15 | +OVERLOAD_CPUS="${OVERLOAD_CPUS:-0.1}" |
| 16 | + |
| 17 | +TARGET="http://127.0.0.1:${API_PORT}/images?limit=20" |
| 18 | +API_LOG="${OUT_DIR}/api.log" |
| 19 | + |
| 20 | +mkdir -p "$OUT_DIR" |
| 21 | + |
| 22 | +if ! docker ps --format '{{.Names}}' | grep -qx "$PG_CONTAINER"; then |
| 23 | + docker compose up -d postgres |
| 24 | + sleep 3 |
| 25 | +fi |
| 26 | + |
| 27 | +export DB_URL |
| 28 | +SEED_COUNT="$SEED_COUNT" uv run python - <<'PY' |
| 29 | +import os |
| 30 | +
|
| 31 | +from sqlalchemy import create_engine, func, select |
| 32 | +from sqlalchemy.orm import Session |
| 33 | +
|
| 34 | +from shared.config import settings |
| 35 | +from shared.models.orm import Base, Image |
| 36 | +
|
| 37 | +seed_count = int(os.environ["SEED_COUNT"]) |
| 38 | +engine = create_engine(settings.db_url_str) |
| 39 | +Base.metadata.create_all(engine) |
| 40 | +with Session(engine) as session: |
| 41 | + existing = session.scalar(select(func.count()).select_from(Image)) or 0 |
| 42 | + for i in range(existing, seed_count): |
| 43 | + session.add( |
| 44 | + Image( |
| 45 | + sha256=f"loadprobe-{i:056d}", |
| 46 | + dataset="loadprobe", |
| 47 | + s3_key=f"images/loadprobe/{i}.jpg", |
| 48 | + width=640, |
| 49 | + height=480, |
| 50 | + format="jpeg", |
| 51 | + file_size=12345, |
| 52 | + ) |
| 53 | + ) |
| 54 | + session.commit() |
| 55 | + total = session.scalar(select(func.count()).select_from(Image)) or 0 |
| 56 | +print(f"images in db: {total}") |
| 57 | +PY |
| 58 | + |
| 59 | +APP_ENV=development DEBUG=false LOG_LEVEL=INFO \ |
| 60 | +RATE_LIMIT_ENABLED=false PRELOAD_TEXT_ENCODER_ON_STARTUP=false GPU_BACKEND=local \ |
| 61 | +AXIOM_API_TOKEN= AXIOM_DATASET= S3_ENDPOINT_URL=http://127.0.0.1:9 \ |
| 62 | +uv run uvicorn api.main:app --host 127.0.0.1 --port "$API_PORT" --log-level warning \ |
| 63 | + >"$API_LOG" 2>&1 & |
| 64 | +API_PID=$! |
| 65 | +trap 'kill "$API_PID" 2>/dev/null || true; docker update --cpus 0 "$PG_CONTAINER" >/dev/null 2>&1 || true' EXIT |
| 66 | + |
| 67 | +for _ in $(seq 1 60); do |
| 68 | + curl -fsS -o /dev/null "$TARGET" 2>/dev/null && break |
| 69 | + sleep 1 |
| 70 | +done |
| 71 | +curl -fsS -o /dev/null "$TARGET" |
| 72 | + |
| 73 | +echo "== warmup, c=50 for 5s" |
| 74 | +"$OHA_BIN" -z 5s -c 50 --no-tui --output-format quiet "$TARGET" >/dev/null |
| 75 | +LAG_AFTER_WARMUP=$(grep -c event_loop_lag "$API_LOG" || true) |
| 76 | + |
| 77 | +echo "== phase (a): normal load, c=${CONCURRENCY} for ${DURATION}" |
| 78 | +"$OHA_BIN" -z "$DURATION" -c "$CONCURRENCY" --no-tui --output-format json "$TARGET" >"$OUT_DIR/normal.json" |
| 79 | +LAG_AFTER_NORMAL=$(grep -c event_loop_lag "$API_LOG" || true) |
| 80 | + |
| 81 | +echo "== phase (b): overload, c=${OVERLOAD_CONCURRENCY}, postgres constrained to ${OVERLOAD_CPUS} cpus" |
| 82 | +docker update --cpus "$OVERLOAD_CPUS" "$PG_CONTAINER" >/dev/null |
| 83 | +"$OHA_BIN" -z "$DURATION" -c "$OVERLOAD_CONCURRENCY" --no-tui --output-format json "$TARGET" >"$OUT_DIR/overload.json" |
| 84 | +docker update --cpus 0 "$PG_CONTAINER" >/dev/null |
| 85 | + |
| 86 | +kill "$API_PID" |
| 87 | +wait "$API_PID" 2>/dev/null || true |
| 88 | + |
| 89 | +LAG_TOTAL=$(grep -c event_loop_lag "$API_LOG" || true) |
| 90 | +TRACEBACKS=$(grep -c Traceback "$API_LOG" || true) |
| 91 | + |
| 92 | +LAG_AFTER_WARMUP="$LAG_AFTER_WARMUP" \ |
| 93 | +LAG_AFTER_NORMAL="$LAG_AFTER_NORMAL" LAG_TOTAL="$LAG_TOTAL" TRACEBACKS="$TRACEBACKS" \ |
| 94 | +OUT_DIR="$OUT_DIR" uv run python - <<'PY' |
| 95 | +import json |
| 96 | +import os |
| 97 | +
|
| 98 | +out_dir = os.environ["OUT_DIR"] |
| 99 | +for phase in ("normal", "overload"): |
| 100 | + with open(f"{out_dir}/{phase}.json") as f: |
| 101 | + report = json.load(f) |
| 102 | + summary = report["summary"] |
| 103 | + percentiles = report["latencyPercentiles"] |
| 104 | + print(f"-- {phase}") |
| 105 | + print(f" rps={summary['requestsPerSec']:.1f} total={report['statusCodeDistribution']}") |
| 106 | + for key in ("p50", "p90", "p99"): |
| 107 | + print(f" {key}={percentiles[key] * 1000:.1f}ms") |
| 108 | + print(f" errors={report['errorDistribution']}") |
| 109 | +warmup = int(os.environ["LAG_AFTER_WARMUP"]) |
| 110 | +after_normal = int(os.environ["LAG_AFTER_NORMAL"]) |
| 111 | +total = int(os.environ["LAG_TOTAL"]) |
| 112 | +print(f"-- loop lag events: warmup={warmup} normal={after_normal - warmup} overload={total - after_normal}") |
| 113 | +print(f"-- tracebacks in api log: {os.environ['TRACEBACKS']}") |
| 114 | +PY |
| 115 | + |
| 116 | +echo "raw reports in ${OUT_DIR}/, api log in ${API_LOG}" |
0 commit comments