/ justapi / benchmarks / run_crud_bench.sh
swadhinbiswas docs: update PLAN.md, DECISIONS.md ADR-075, and BENCHMARKS.md ledger · 7/24/2026
5de743b
173 lines · 6.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/bin/env bash
# benchmarks/run_crud_bench.sh — Real DB-backed CRUD benchmark.
#
# Compares JustAPI (Python-handler + Rust-native), FastAPI+SQLAlchemy(async),
# and Robyn on single-row INSERT/SELECT/UPDATE/DELETE against a SQLite file
# (WAL, 10-connection pool) under `oha -c 50`.
#
# Prereqs: `cargo install oha` and the benchmark venv with justapi + fastapi
# + robyn + sqlalchemy + aiosqlite + uvicorn installed.
#
# Usage: bash benchmarks/run_crud_bench.sh
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(dirname "$HERE")"
VENV_PY="$ROOT/crates/justapi-py/.venv/bin/python"
HOST="127.0.0.1"
DUR="5s"
WARM="2s"
CONC=20
DATE="$(date +%Y-%m-%d)"

log()  { echo -e "\033[0;32m[crud-bench]\033[0m $*" >&2; }
warn() { echo -e "\033[1;33m[warn]\033[0m $*" >&2; }
err()  { echo -e "\033[0;31m[error]\033[0m $*" >&2; }

command -v oha >/dev/null || { err "oha not found (cargo install oha)"; exit 1; }

# Pick a free base port per server to avoid collisions.
JA_PORT=8131
FA_PORT=8132
RO_PORT=8133

# measure <name> <method> <url> [body]
measure() {
  local name="$1" method="$2" url="$3" body="${4:-}"
  log "  $name ($method $url)"
  # warmup
  if [[ "$method" == "POST" || "$method" == "PUT" || "$method" == "DELETE" ]]; then
    oha -z "$WARM" -c "$CONC" -m "$method" -H "Content-Type: application/json" \
        ${body:+-d "$body"} "$url" >/dev/null 2>&1 || true
  else
    oha -z "$WARM" -c "$CONC" "$url" >/dev/null 2>&1 || true
  fi
  local jsonf
  jsonf=$(mktemp /tmp/oha.XXXXXX.json)
  if [[ "$method" == "POST" || "$method" == "PUT" || "$method" == "DELETE" ]]; then
    oha -z "$DUR" -c "$CONC" -m "$method" -H "Content-Type: application/json" \
        --output-format json ${body:+-d "$body"} "$url" > "$jsonf" 2>/dev/null || true
  else
    oha -z "$DUR" -c "$CONC" --output-format json "$url" > "$jsonf" 2>/dev/null || true
  fi
  local rps p50 p99
  rps=$("$VENV_PY" -c "import json,sys; 
try:
    d=json.load(open('$jsonf')); print(round(d['summary']['requestsPerSec'],1))
except Exception as e: print('NA')" 2>/dev/null)
  p50=$("$VENV_PY" -c "import json,sys
try:
    d=json.load(open('$jsonf')); print(round(d['latencyPercentiles']['p50']/1e6,2),'ms')
except Exception: print('NA')" 2>/dev/null)
  p99=$("$VENV_PY" -c "import json,sys
try:
    d=json.load(open('$jsonf')); print(round(d['latencyPercentiles']['p99']/1e6,2),'ms')
except Exception: print('NA')" 2>/dev/null)
  rm -f "$jsonf"
  echo "| $name | $rps | $p50 | $p99 |"
}

start_server() {
  local name="$1" port="$2"; shift 2
  log "Starting $name on :$port"
  cd "$ROOT"
  local logf="/tmp/crud_bench_${name//[^a-zA-Z0-9]/_}.log"
  "$@" >"$logf" 2>&1 &
  local pid=$!
  sleep 3
  local tries=20
  while ! curl -s "http://$HOST:$port/items/1" >/dev/null 2>&1; do
    tries=$((tries-1))
    if [[ $tries -le 0 || ! -d "/proc/$pid" ]]; then
      err "$name failed to start (see $logf)"; tail -5 "$logf" >&2
      kill "$pid" 2>/dev/null || true; return 1
    fi
    sleep 1
  done
  echo "$pid"
}

stop_server() {
  local pid="$1" name="$2"
  [[ -d "/proc/$pid" ]] && { kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null; sleep 1; }
  log "Stopped $name"
}

BODY='{"name":"widget","qty":7}'

echo ""
echo "## Real DB-backed CRUD benchmark (recorded $DATE)"
echo ""
echo "- **Workload:** single-row INSERT / SELECT / UPDATE / DELETE, SQLite file (WAL, busy_timeout=5000, 10-conn pool)"
echo "- **Note:** single-file SQLite write-serializes (one writer at a time), so write"
echo "  RPS is SQLite-bound (~hundreds) and similar across frameworks; SELECT is the"
echo "  framework-differentiated metric."
echo "- **Tool:** \`oha -c $CONC -z $DUR\`"
echo "- **CPU:** $(grep 'model name' /proc/cpuinfo | head -1 | cut -d: -f2 | xargs) ($(nproc) threads)"
echo "- **Kernel:** $(uname -sr)"
echo ""

# ---- JustAPI (Python-handler) ----
PID=$(start_server "JustAPI(py)" "$JA_PORT" "$VENV_PY" "$HERE/crud_justapi.py" python "$JA_PORT") || true
if [[ -n "${PID:-}" ]]; then
  echo "### JustAPI — Python-handler CRUD"
  echo ""
  echo "| Operation | RPS | p50 | p99 |"
  echo "|-----------|-----|-----|-----|"
  measure "INSERT" POST "http://$HOST:$JA_PORT/items" "$BODY"
  measure "SELECT" GET  "http://$HOST:$JA_PORT/items/1"
  measure "UPDATE" PUT  "http://$HOST:$JA_PORT/items/1" "$BODY"
  measure "DELETE" DELETE "http://$HOST:$JA_PORT/items/1"
  echo ""
  stop_server "$PID" "JustAPI(py)"
fi

# ---- JustAPI (Rust-native) ----
PID=$(start_server "JustAPI(native)" "$JA_PORT" "$VENV_PY" "$HERE/crud_justapi.py" native "$JA_PORT") || true
if [[ -n "${PID:-}" ]]; then
  echo "### JustAPI — Rust-native CRUD (fast path)"
  echo ""
  echo "| Operation | RPS | p50 | p99 |"
  echo "|-----------|-----|-----|-----|"
  measure "INSERT" POST "http://$HOST:$JA_PORT/items" "$BODY"
  measure "SELECT" GET  "http://$HOST:$JA_PORT/items/1"
  measure "UPDATE" PUT  "http://$HOST:$JA_PORT/items/1" "$BODY"
  measure "DELETE" DELETE "http://$HOST:$JA_PORT/items/1"
  echo ""
  stop_server "$PID" "JustAPI(native)"
fi

# ---- FastAPI + SQLAlchemy ----
PID=$(start_server "FastAPI" "$FA_PORT" "$VENV_PY" "$HERE/crud_fastapi.py" "$FA_PORT") || true
if [[ -n "${PID:-}" ]]; then
  echo "### FastAPI + SQLAlchemy (async)"
  echo ""
  echo "| Operation | RPS | p50 | p99 |"
  echo "|-----------|-----|-----|-----|"
  measure "INSERT" POST "http://$HOST:$FA_PORT/items" "$BODY"
  measure "SELECT" GET  "http://$HOST:$FA_PORT/items/1"
  measure "UPDATE" PUT  "http://$HOST:$FA_PORT/items/1" "$BODY"
  measure "DELETE" DELETE "http://$HOST:$FA_PORT/items/1"
  echo ""
  stop_server "$PID" "FastAPI"
fi

# ---- Robyn ----
PID=$(start_server "Robyn" "$RO_PORT" "$VENV_PY" "$HERE/crud_robyn.py" "$RO_PORT") || true
if [[ -n "${PID:-}" ]]; then
  echo "### Robyn (sync handler, sqlite3)"
  echo ""
  echo "| Operation | RPS | p50 | p99 |"
  echo "|-----------|-----|-----|-----|"
  measure "INSERT" POST "http://$HOST:$RO_PORT/items" "$BODY"
  measure "SELECT" GET  "http://$HOST:$RO_PORT/items/1"
  measure "UPDATE" PUT  "http://$HOST:$RO_PORT/items/1" "$BODY"
  measure "DELETE" DELETE "http://$HOST:$RO_PORT/items/1"
  echo ""
  stop_server "$PID" "Robyn"
fi

# Cleanup bench db
for ext in "" "-wal" "-shm"; do rm -f "$HERE/crud_bench.sqlite$ext"; done
log "Done. Paste the tables above into BENCHMARKS.md."