|
| 1 | +# Proxy benchmark harness |
| 2 | + |
| 3 | +Language-agnostic benchmark harness that compares the PHP/Swoole and Rust/Tokio |
| 4 | +proxy implementations across three scenarios: |
| 5 | + |
| 6 | +1. **Connections per MB of RAM** -- how many concurrent TCP connections each |
| 7 | + proxy holds per MB of resident memory (idle-ish connections) |
| 8 | +2. **HTTP RPS** -- sustained HTTP/1.1 requests per second |
| 9 | +3. **TCP connections per second** -- accept-path throughput |
| 10 | + |
| 11 | +One shared load generator, one CSV, one markdown report. |
| 12 | + |
| 13 | +## Layout |
| 14 | + |
| 15 | +``` |
| 16 | +benchmarks/ |
| 17 | +├── httpbench/ # minimal pthreads HTTP/1.1 load generator (C) |
| 18 | +├── tcpbench.c # existing TCP load generator (rr, rate, hold modes) |
| 19 | +└── harness/ |
| 20 | + ├── run.sh # orchestrator |
| 21 | + ├── lib.sh # shared bash helpers |
| 22 | + ├── scenarios/ |
| 23 | + │ ├── ram_per_connection.sh |
| 24 | + │ ├── http_rps.sh |
| 25 | + │ └── tcp_conn_rate.sh |
| 26 | + ├── targets/ |
| 27 | + │ ├── php_tcp.env php_http.env |
| 28 | + │ └── rust_tcp.env rust_http.env |
| 29 | + ├── report.py # CSV -> markdown |
| 30 | + └── results/ # CSV + report + per-run logs (gitignored) |
| 31 | +``` |
| 32 | + |
| 33 | +## Prerequisites |
| 34 | + |
| 35 | +### Kernel tuning (Linux) |
| 36 | + |
| 37 | +Run once before benchmarking: |
| 38 | + |
| 39 | +```bash |
| 40 | +sudo benchmarks/setup.sh # aggressive (benchmark) |
| 41 | +# or |
| 42 | +sudo benchmarks/setup.sh --production # conservative |
| 43 | +``` |
| 44 | + |
| 45 | +`lib.sh::check_kernel` warns about `net.core.somaxconn < 32768` and `ulimit -n < |
| 46 | +1000000`. |
| 47 | + |
| 48 | +### Load generators |
| 49 | + |
| 50 | +Both generators are plain C + pthreads: |
| 51 | + |
| 52 | +```bash |
| 53 | +# existing |
| 54 | +( cd benchmarks && cc -O2 -Wall -Wextra -pthread -o tcpbench tcpbench.c ) |
| 55 | + |
| 56 | +# new |
| 57 | +( cd benchmarks/httpbench && make ) |
| 58 | +``` |
| 59 | + |
| 60 | +### PHP proxy |
| 61 | + |
| 62 | +Install deps in the repo root: |
| 63 | + |
| 64 | +```bash |
| 65 | +composer install |
| 66 | +``` |
| 67 | + |
| 68 | +Requires PHP 8.4+ and ext-swoole 6.0+. |
| 69 | + |
| 70 | +### Rust proxy |
| 71 | + |
| 72 | +Release binary is expected at `rust/target/release/proxy`: |
| 73 | + |
| 74 | +```bash |
| 75 | +( cd rust && cargo build --release ) |
| 76 | +``` |
| 77 | + |
| 78 | +If the Rust crate is not yet present, pass `--impl php` to `run.sh` to benchmark |
| 79 | +only the PHP side. |
| 80 | + |
| 81 | +## Usage |
| 82 | + |
| 83 | +```bash |
| 84 | +# Run everything (both impls, all scenarios, 30s duration). |
| 85 | +bash benchmarks/harness/run.sh |
| 86 | + |
| 87 | +# PHP only, HTTP RPS only, 60s. |
| 88 | +bash benchmarks/harness/run.sh --impl php --scenario rps --duration 60 |
| 89 | + |
| 90 | +# Rust only, RAM scenario only. |
| 91 | +bash benchmarks/harness/run.sh --impl rust --scenario ram |
| 92 | + |
| 93 | +# Custom output directory. |
| 94 | +bash benchmarks/harness/run.sh --output-dir /tmp/proxy-bench |
| 95 | +``` |
| 96 | + |
| 97 | +Flags: |
| 98 | + |
| 99 | +- `--impl {php|rust|both}` -- default `both` |
| 100 | +- `--scenario {ram|rps|conn-rate|all}` -- default `all` |
| 101 | +- `--duration SEC` -- default `30` |
| 102 | +- `--output-dir DIR` -- default `benchmarks/harness/results` |
| 103 | + |
| 104 | +Implementations are run strictly sequentially (PHP then Rust), so their listen |
| 105 | +ports intentionally collide -- only one proxy is alive at a time. |
| 106 | + |
| 107 | +## Remote / split-host mode |
| 108 | + |
| 109 | +For realistic numbers the load generator and proxy should run on **separate |
| 110 | +machines** so neither starves the other for CPU, and every request crosses a |
| 111 | +real network. The harness can orchestrate this over SSH. |
| 112 | + |
| 113 | +Required: passwordless key-based SSH to both hosts (`root@...` or a sudo-able |
| 114 | +user), rsync on the controller, and `bash` + `rsync` on the targets. |
| 115 | + |
| 116 | +```bash |
| 117 | +# One-time deploy: rsync project, install deps, build binaries on each host. |
| 118 | +PROXY_HOST=root@1.2.3.4 LOAD_HOST=root@5.6.7.8 \ |
| 119 | + bash benchmarks/harness/deploy.sh --install-deps |
| 120 | + |
| 121 | +# Subsequent runs (after a code change, re-deploy without re-installing deps): |
| 122 | +PROXY_HOST=root@1.2.3.4 LOAD_HOST=root@5.6.7.8 \ |
| 123 | + bash benchmarks/harness/deploy.sh |
| 124 | + |
| 125 | +# Run the harness against the remote hosts: |
| 126 | +bash benchmarks/harness/run.sh \ |
| 127 | + --proxy-host root@1.2.3.4 --load-host root@5.6.7.8 |
| 128 | +``` |
| 129 | + |
| 130 | +Equivalent env form: |
| 131 | + |
| 132 | +```bash |
| 133 | +PROXY_HOST=root@1.2.3.4 LOAD_HOST=root@5.6.7.8 \ |
| 134 | + bash benchmarks/harness/run.sh |
| 135 | +``` |
| 136 | + |
| 137 | +When `PROXY_HOST` is set the harness starts the proxy (+ echo backend) on that |
| 138 | +host, samples RSS over SSH, and tears down via SSH. When `LOAD_HOST` is set the |
| 139 | +load generator (`tcpbench` / `httpbench`) runs there, pointed at |
| 140 | +`$PROXY_HOST:$PORT`. Result CSVs and logs are written on the controller as in |
| 141 | +localhost mode. |
| 142 | + |
| 143 | +Paths on targets default to `/opt/utopia-proxy`; override via |
| 144 | +`PROXY_REMOTE_DIR` / `LOAD_REMOTE_DIR`. Extra SSH options go into `SSH_OPTS`. |
| 145 | + |
| 146 | +## Output |
| 147 | + |
| 148 | +- `results/<unix-ts>.csv` -- one row per (impl, scenario, concurrency) tuple |
| 149 | + with columns |
| 150 | + `timestamp,impl,scenario,concurrency,duration,ops_per_sec,conns_per_sec,avg_latency_us,rss_peak_kb,rss_per_conn_kb` |
| 151 | +- `results/report.md` -- markdown tables with a Delta (Rust vs PHP %) column |
| 152 | +- `results/logs/*.log` -- per-process stdout/stderr |
| 153 | +- `results/logs/*.out` -- raw key=value output from the load generators |
| 154 | + |
| 155 | +Re-run the harness multiple times; `report.py` aggregates every CSV in the |
| 156 | +results directory. |
| 157 | + |
| 158 | +## Interpretation guide |
| 159 | + |
| 160 | +### RAM scenario |
| 161 | + |
| 162 | +- `rss_per_conn_kb` = `(proxy_rss_kb - idle_rss_kb) / concurrent_conns`. Lower |
| 163 | + is better. |
| 164 | +- `conns_per_mb` = inverse of the above, scaled to MB. The summary table picks |
| 165 | + the best ratio achieved at any batch size. |
| 166 | +- The ramp stops when the tcpbench hold worker reports > 5% errors (usually fd |
| 167 | + exhaustion or accept queue overflow). If you want to push further, bump |
| 168 | + `ulimit -n` and rerun. |
| 169 | + |
| 170 | +### HTTP RPS scenario |
| 171 | + |
| 172 | +- httpbench uses HTTP/1.1 keep-alive (`-k`) so each worker thread issues a |
| 173 | + tight pipeline of GETs over one socket. |
| 174 | +- `ops_per_sec` peaks when concurrency * latency roughly matches the backend |
| 175 | + and proxy service times. A higher ceiling at comparable latency = a faster |
| 176 | + proxy. |
| 177 | + |
| 178 | +### TCP conn-rate scenario |
| 179 | + |
| 180 | +- Each worker does `connect -> send pg startup -> recv first response -> |
| 181 | + close`, looped. It measures the accept + first-response path, not bulk |
| 182 | + forwarding. |
| 183 | + |
| 184 | +## Extending |
| 185 | + |
| 186 | +Add a new scenario: |
| 187 | + |
| 188 | +1. Drop a script in `scenarios/` that accepts `<impl> <csv_path>` and appends |
| 189 | + rows via `record_csv`. |
| 190 | +2. Wire a case into `run.sh::run_scenario` and the `SCENARIO` parser. |
| 191 | +3. Add a `SCENARIOS` entry in `report.py` with a renderer. |
| 192 | + |
| 193 | +Add a new target: |
| 194 | + |
| 195 | +1. Drop an env file in `targets/<impl>_<protocol>.env` exporting the required |
| 196 | + env vars and setting `BIN` to the launch command. |
| 197 | +2. Use the new `<impl>` value with `--impl`. |
0 commit comments