Skip to content

Commit d1ef728

Browse files
Merge pull request #87 from QueryaHub/chore-perf-test-harness
chore(perf-test): add reproducible OxyRoute vs FastAPI bench harness
2 parents 8e5e763 + 044175a commit d1ef728

4 files changed

Lines changed: 147 additions & 0 deletions

File tree

perf-test/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# perf-test
2+
3+
Reproducible micro-bench harness for OxyRoute vs FastAPI.
4+
5+
## Apps
6+
7+
- `app.py` -> OxyRoute hello endpoint (`GET /`)
8+
- `fastapi_app.py` -> FastAPI hello endpoint (`GET /`)
9+
10+
Both return plain text `hello world` to keep payloads equivalent.
11+
12+
## Prerequisites
13+
14+
- `wrk` installed
15+
- `granian` installed
16+
- For FastAPI runs: `uv` (uses temporary dependency install via `--with fastapi`)
17+
18+
## Default benchmark profile
19+
20+
- Server tuning: `--workers 2 --runtime-mode mt --runtime-threads 1`
21+
- Load profile: `wrk -t4 -c128 -d15s`
22+
- Repetitions: `3`
23+
24+
## Run
25+
26+
From repository root:
27+
28+
```bash
29+
bash perf-test/bench.sh
30+
```
31+
32+
The script prints per-run metrics plus average/median RPS and relative delta.

perf-test/app.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from oxyroute import App
2+
3+
app = App(title="Perf Test")
4+
5+
6+
@app.get("/")
7+
def hello() -> str:
8+
return "hello world"

perf-test/bench.sh

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
cd "${ROOT_DIR}"
6+
7+
HOST="127.0.0.1"
8+
PORT="8000"
9+
WRK_THREADS="4"
10+
WRK_CONN="128"
11+
WRK_DUR="15s"
12+
RUNS="3"
13+
SERVER_FLAGS=(--workers 2 --runtime-mode mt --runtime-threads 1)
14+
15+
cleanup() {
16+
if [[ -n "${SERVER_PID:-}" ]]; then
17+
kill "${SERVER_PID}" 2>/dev/null || true
18+
wait "${SERVER_PID}" 2>/dev/null || true
19+
SERVER_PID=""
20+
fi
21+
}
22+
trap cleanup EXIT
23+
24+
extract_rps() {
25+
awk '/Requests\/sec:/ {print $2}' "$1"
26+
}
27+
28+
extract_latency() {
29+
awk '/Latency/ && $2 ~ /ms|us|s/ {print $2}' "$1" | head -n1
30+
}
31+
32+
run_suite() {
33+
local name="$1"
34+
local start_cmd="$2"
35+
local out_prefix="$3"
36+
37+
echo "=== ${name} ==="
38+
eval "${start_cmd}" >/tmp/"${out_prefix}"_server.log 2>&1 &
39+
SERVER_PID=$!
40+
sleep 2
41+
curl -fsS "http://${HOST}:${PORT}/" >/tmp/"${out_prefix}"_smoke.txt
42+
43+
local rps_values=()
44+
for i in $(seq 1 "${RUNS}"); do
45+
local out_file="/tmp/${out_prefix}_wrk_${i}.txt"
46+
wrk -t"${WRK_THREADS}" -c"${WRK_CONN}" -d"${WRK_DUR}" "http://${HOST}:${PORT}/" | tee "${out_file}" >/dev/null
47+
local rps
48+
rps="$(extract_rps "${out_file}")"
49+
local lat
50+
lat="$(extract_latency "${out_file}")"
51+
echo "run${i}: rps=${rps} latency_avg=${lat}"
52+
rps_values+=("${rps}")
53+
done
54+
55+
cleanup
56+
57+
python3 - "$name" "${rps_values[@]}" <<'PY'
58+
import statistics
59+
import sys
60+
name = sys.argv[1]
61+
vals = [float(x) for x in sys.argv[2:]]
62+
print(f"{name} avg_rps={sum(vals)/len(vals):.2f} median_rps={statistics.median(vals):.2f}")
63+
PY
64+
}
65+
66+
run_suite \
67+
"OxyRoute RSGI (tuned)" \
68+
"granian perf-test.app:app --interface rsgi --host ${HOST} --port ${PORT} ${SERVER_FLAGS[*]}" \
69+
"oxyroute"
70+
71+
run_suite \
72+
"FastAPI ASGI (tuned)" \
73+
"uv run --with fastapi granian perf-test.fastapi_app:app --interface asgi --host ${HOST} --port ${PORT} ${SERVER_FLAGS[*]}" \
74+
"fastapi"
75+
76+
python3 - <<'PY'
77+
import glob
78+
import statistics
79+
80+
def read_rps(prefix):
81+
vals = []
82+
for p in sorted(glob.glob(f"/tmp/{prefix}_wrk_*.txt")):
83+
with open(p, "r", encoding="utf-8") as f:
84+
for line in f:
85+
if line.startswith("Requests/sec:"):
86+
vals.append(float(line.split()[-1]))
87+
break
88+
return vals
89+
90+
oxy = read_rps("oxyroute")
91+
fa = read_rps("fastapi")
92+
oxy_avg = sum(oxy)/len(oxy)
93+
fa_avg = sum(fa)/len(fa)
94+
delta = (oxy_avg / fa_avg - 1.0) * 100.0
95+
print("=== Summary ===")
96+
print(f"OxyRoute avg={oxy_avg:.2f} median={statistics.median(oxy):.2f}")
97+
print(f"FastAPI avg={fa_avg:.2f} median={statistics.median(fa):.2f}")
98+
print(f"Delta (OxyRoute vs FastAPI): {delta:+.2f}%")
99+
PY

perf-test/fastapi_app.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from fastapi import FastAPI
2+
3+
app = FastAPI(title="Perf Test FastAPI")
4+
5+
6+
@app.get("/")
7+
def hello() -> str:
8+
return "hello world"

0 commit comments

Comments
 (0)