Skip to content

Commit 437d454

Browse files
committed
chore: checkpoint local changes
1 parent 560825e commit 437d454

90 files changed

Lines changed: 13792 additions & 8 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
# Compiled binaries + BPF objects
1818
/benchmarks/tcpbench
19+
/benchmarks/httpbench/httpbench
1920
/benchmarks/sockmap_poc/relay.bpf.o
2021
/benchmarks/sockmap_poc/relay_test
2122
/src/Sockmap/relay.bpf.o
23+
24+
# Benchmark harness results (CSVs, logs, reports)
25+
/benchmarks/harness/results/

Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@
5050
# --network=host is recommended so SO_REUSEPORT, sockmap, and kernel
5151
# socket tuning work against the host's real TCP stack rather than
5252
# Docker's userspace bridge.
53+
#
54+
# Runtime tuning (measured in benchmarks/harness/results):
55+
# --ulimit nofile=1048576:1048576 # 1M file descriptors
56+
# --sysctl net.core.somaxconn=65535 # accept queue
57+
# --sysctl net.ipv4.tcp_tw_reuse=1 # recycle TIME_WAIT faster
58+
# --sysctl net.ipv4.ip_local_port_range="1024 65535"
59+
# --sysctl net.ipv4.tcp_fin_timeout=15
60+
#
61+
# Host-side kernel tuning (apply on the Docker host, not in-container):
62+
# sysctl -w fs.file-max=2097152
63+
# sysctl -w vm.max_map_count=1048576
64+
# sysctl -w net.core.netdev_max_backlog=32768
65+
# sysctl -w net.ipv4.tcp_max_syn_backlog=32768
66+
# See benchmarks/setup.sh for the full tuning set.
5367

5468
# ------------- stage 1: build the BPF object -------------
5569
FROM alpine:3.20 AS bpf-builder

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ High-performance, protocol-agnostic proxy built on Swoole for HTTP, TCP, and SMT
1717
| Connection rate (sustained) | 18,067/sec |
1818
| CPU at peak | ~60% |
1919

20+
## Benchmarks
21+
22+
The `benchmarks/harness/` directory contains a language-agnostic benchmark harness
23+
that compares the PHP/Swoole implementation against the Rust port on three axes
24+
-- connections per MB of RAM, HTTP RPS, and TCP connections per second. Results
25+
land in a single CSV plus a markdown report with Rust-vs-PHP deltas. See
26+
[`benchmarks/harness/README.md`](benchmarks/harness/README.md) for usage and the
27+
C load generators at `benchmarks/tcpbench.c` and `benchmarks/httpbench/`.
28+
2029
## Requirements
2130

2231
- PHP >= 8.4

benchmarks/harness/README.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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

Comments
 (0)