Live performance and regression harness
These scripts exercise actual RustProxy executables through loopback sockets.
They do not simulate Rust request handling. The Python server in peer.py is a
controllable test upstream, used to verify streaming, non-replayable writes,
headers, cache policy and WebSocket frames. Performance measurements use nginx
as the shared upstream instead of Python.
Requirements
- Linux with readable
/proc, Python 3.10+, executable RustProxy binaries. - nginx and wrk for performance comparisons. Both paths are explicit; no system-wide installation is required.
tasksetfrom util-linux for the benchmark's explicit CPU affinity and for optional affinity settings in the other scripts.- Optional Python
brotliandzstandarddecoders increase compression acceptance coverage. A missing optional decoder is reported as skipped. - Permission to bind unprivileged loopback sockets and start local processes.
In a sandbox without sockets or
/proc, run in the approved local execution environment. Do not replace real runtime results with a peer self-test.
Each run creates a new private temporary data directory and initializes its own
administrator with a random password supplied only over stdin. Configurations
are applied through RustProxy's existing authenticated CLI/API and listener
changes receive a restart. No historical SQLite database is copied or edited.
TLS keys, databases and passwords are never written to the evidence directory.
Temporary state is outside the evidence tree and is removed on normal exit.
fixture-cleanup.json records the cleanup check. Interrupted runs can leave an
unrelated /tmp/rustproxy-perf-* directory; do not include it in a release.
Before timed benchmark, soak or syscall-profile work, wait_quiescent() waits
for the existing authenticated ACME runtime API to report ready or
unavailable. A fresh RustProxy data directory can start a Python/Certbot
environment bootstrap even when no certificates have an issuer. The gate
requires no configured issuers, jobs or renewal observations, the same live Rust
PID and start time, and no child processes for five continuous seconds. It polls
every 0.5 seconds and fails after 180 seconds instead of measuring installation
work. Selected status, process count and timing evidence is saved in
<label>-quiescence.json. No PATH, pip settings or application features are
changed. An unavailable result permits the HTTP fixture to proceed; these runs
do not test ACME issuance or certificate-provider availability.
Regression tests
Run from the repository root, providing absolute binary and output paths:
python3 scripts/performance/regression.py \
--binary before=/absolute/path/rust-proxy-before \
--binary after=/absolute/path/rust-proxy-after \
--out /absolute/path/results/regression
--groups static,proxy,policy,cache,transform selects groups; all are enabled by
default. --workers 4 sets the same TOKIO_WORKER_THREADS for each binary.
The report contains individual pass/fail/skip observations and a comparison
matrix. An existing failure in the supplied binary remains a failure in its
report; it is not silently converted to a successful baseline expectation.
Coverage includes:
- Static GET/HEAD and byte checksums at 1,040 B, 64 KiB and 1 MiB; empty files;
ETag/Last-Modified conditional requests; prefix/suffix/open/multipart ranges,
If-Range and 416; directory redirect/index/listing; decoded path rejection;
deny/inside-root symlinks; precompressed gzip; MIME/download headers;
try_files, redirects and the compiled.htaccessheader subset. - Proxy POST/PUT, 1 MiB upload checksums, chunked upload delivery before the last chunk is sent, early response delivery, client disconnect, original status/redirect preservation, failed-POST no replay, request/response header scopes, hop-by-hop stripping, request-ID shape/uniqueness and WebSocket echo.
- Global host/vhost/profile/location ACL composition, upstream suppression on denial, trusted versus spoofed forwarding, untrusted country hints, method and rate policies, hot changes and rejection of invalid configuration.
- Private administration routes, login, session/CSRF and Origin enforcement, Basic Auth, ACL-before-auth, private-origin rejection, retained exchange logging fields and byte accounting.
- Cache global/vhost/route OFF and ON, MISS/HIT, Vary isolation, cookie, Authorization/private/no-store bypass, PURGE, concurrent fills, validator revalidation, static cached ranges/conditionals; transforms OFF/ON with cache OFF/ON; compression negotiation, exclusions and scope/global codec ceilings.
The generic suite does not establish TLS/H2/H3, Tinyauth, real PHP-FPM, real DNS rebinding, concurrent filesystem rename-race safety or long-soak acceptance. Use the dedicated transport tests and existing Rust integration suites for their covered cases. The sequential symlink-target test is explicitly not a concurrent rename-race proof.
TLS, HTTP/2, HTTP/3 and WebSocket interoperability
python3 scripts/performance/transport_regression.py \
--binary before=/absolute/path/rust-proxy-before \
--binary after=/absolute/path/rust-proxy-after \
--out /absolute/path/results/transport
This suite requires aioquic, httpx[http2] and websockets; use
--python-deps /absolute/path/pydeps for a separate dependency directory.
It verifies the temporary server certificate, negotiated protocol, response
status and bytes for TLS H2 and QUIC H3, including HEAD, validators, ranges,
cache ON/OFF, 1 MiB bodies and concurrent streams. Dedicated WebSocket checks
include bidirectional echo and a normal close handshake. Client protocol
self-checks are reported separately from RustProxy acceptance cases.
This establishes covered interoperability, not TLS/H2/H3 throughput or
production-certificate compatibility.
Two-origin load-balancer regression
python3 scripts/performance/lb_regression.py \
--binary before=/absolute/path/rust-proxy-before \
--binary after=/absolute/path/rust-proxy-after \
--out /absolute/path/results/lb
This separate, small suite sends real requests through RustProxy to two local TCP origins. It verifies equal round-robin allocation, GET failover after a 503 and after a stopped origin refuses connections, and the absence of POST replay. A controlled response pauses after its first body fragment while configuration changes drain or disable its member. New requests must use the other member, the admitted response must finish intact, and its active lease must remain counted until completion and then return to zero. The main HTTP case matrix is unchanged. This suite does not cover sticky sessions, active health probes, DNS rotation or TLS upstream failures.
Finite concurrency correctness stress
python3 scripts/performance/concurrency_regression.py \
--binary before=/absolute/path/rust-proxy-before \
--binary after=/absolute/path/rust-proxy-after \
--nginx /absolute/path/nginx --out /absolute/path/results/concurrency
This separate functional check uses 1, 16 and 128 concurrent client tasks for
proxy and static responses at 1,040 B, 64 KiB and 1 MiB. Each finite batch sends
max(32, 2 * clients) requests and verifies every status, length and body SHA-256.
After the clients finish, backend active-exchange counters must settle to zero.
RSS and file descriptors are recorded before the batch and after its drain.
These are not peak resource measurements or throughput results. Client tasks
start behind a barrier; configured concurrency does not establish that the same
number of server exchanges were active simultaneously.
Before/after/nginx benchmark
First verify starts, loopback routing and exact payload hashes without invoking the load generator:
python3 scripts/performance/benchmark.py \
--binary before=/absolute/path/rust-proxy-before \
--binary after=/absolute/path/rust-proxy-after \
--nginx /absolute/path/nginx --wrk /absolute/path/wrk \
--server-cpus 0-3 --origin-cpus 4-5 --client-cpus 6-8 \
--out /absolute/path/results/preflight --prepare-only
For a host with CPUs 0–8 available, a reproducible primary run is:
python3 scripts/performance/benchmark.py \
--binary before=/absolute/path/rust-proxy-before \
--binary after=/absolute/path/rust-proxy-after \
--nginx /absolute/path/nginx --wrk /absolute/path/wrk \
--out /absolute/path/results/primary \
--workers 4 --origin-workers 2 --client-threads 4 \
--server-cpus 0-3 --origin-cpus 4-5 --client-cpus 6-8 \
--concurrency 64 --sizes small,large,huge --modes proxy,static \
--duration 15 --warmup 3 --repeats 5
Choose explicit, nonempty and disjoint CPU sets allowed in the run environment; adapt all three sets on a smaller host. Frontends use the same CPU set and worker count, the shared origin uses another set, and the generator uses a third. The harness checks each process/thread's actual affinity and refuses unavailable resource counters. Four wrk threads share the three client CPUs in this example. The structured result records thread count and effective connection count; at 64 connections, four threads keep exactly 64 connections, avoiding wrk's integer division of connections between threads. Only one frontend is loaded at a time. Stop builds, other tests and profilers during throughput measurement.
The primary run takes approximately 27 minutes: six scenarios, three variants,
five samples, and 18 seconds per sample including excluded warm-up. To explore
concurrency, repeat in a fresh output directory with
--concurrency 1,16,64,128. More scenarios intentionally take more time.
Short sanity checks require the explicit --exploratory option and cannot
satisfy the >=5 samples / >=15-second measurement gate.
Cache is explicitly disabled for the primary benchmark. Transforms retain their
disabled defaults. Identity encoding is requested. Logging history, analytics,
request IDs, origin policy, body accounting, health/lease plumbing and normal
application functionality remain active. nginx uses sendfile on, HTTP/1.1,
keep-alive and streaming proxy delivery. The generated exact configurations are
saved, so this intentional difference in nginx/application features is visible.
No cache-disabled claim is inferred from a cache-on speedup.
Optional keep-alive tuning uses --max-idle label=64. Keep this as a separately
labelled experiment. The harness does not turn off bounded pool recycling.
Recorded evidence and interpretation
schedule.json: complete deterministic crossover order, rotating and reversing variants between repetitions. No best-run selection.preflight.json,fixture/payloads.json, binary/version/config metadata: exact status, body SHA-256, payload sizes and inputs.- Per-sample exact status/body checks before and after the measured interval, plus a wire-bytes-per-request sanity check, detect wrong content and gross transfer-size anomalies. These do not hash every response inside wrk.
raw/*.txtandraw/*.json: unmodified wrk output and structured requests, duration, bytes, RPS, p50/p95/p99, effective concurrency, and connect/read/write/timeout/status errors.raw/*-resources.json: timestamped/procsamples for each server process and its children, plus the separate origin process group.raw/*-affinity.json: observed process/thread CPU masks for the front end, origin and generator.runs.csv/json,summary.csv/json: all samples, medians, RPS range and coefficient of variation, latency medians, server CPU time per completed request, peak/end RSS and file descriptors.status/*.json: numeric drop and pool-generation/rotation counters read outside each measured interval. Warm-up output is kept separately.completion.jsonandSHA256SUMS: completion state and hashes of evidence. Any generator/network/status error stops the run withfailure.json; partial data remains available and is not presented as completed acceptance.
Use (after_median / before_median - 1) * 100 for throughput improvement and
after_median / nginx_median * 100 for relative nginx throughput. Rebuilding
both revisions with one compiler, target and resolved lockfile is required for
source-change attribution. Compare the supplied executable separately: build
flags and dependency versions can change its performance.
These are same-host, closed-loop HTTP/1.1 diagnostics. They do not establish production capacity, TLS/H2/H3 performance, an open-loop latency SLA, allocator rates, CPU cycles or 24-hour resource stability. wrk latency follows wrk's own histogram/correction behavior. RSS and descriptor peaks are sampled observations, so shorter spikes may be missed. Summed nginx RSS can double-count shared pages. Origin or generator CPU saturation can limit observed throughput; inspect their resource data and move the generator/origin to separate machines for release measurements. Run profilers separately so profiling overhead does not enter the headline A/B throughput numbers.
Mixed-traffic resource soak
Run this in its own phase, after correctness and throughput checks:
python3 scripts/performance/soak.py \
--binary /absolute/path/rust-proxy-after --label after \
--nginx /absolute/path/nginx --out /absolute/path/results/soak \
--duration 1800 --rate 100 --clients 8 \
--workers 4 --origin-workers 2 --server-cpus 0-3 --origin-cpus 4-5 \
--toggle-seconds 120 --sample-seconds 5 --abort-every 101
The bounded client executor alternates proxy/static and 1,040 B/64 KiB/1 MiB payloads, verifies full bytes and occasional HEAD/range semantics, closes some clients after an initial body prefix, and applies live cache OFF/ON changes. Every workload request opens a new frontend HTTP/1.1 connection; upstream keepalive retains its configured defaults. This exercises connection churn. The primary wrk benchmark separately uses persistent frontend connections. The nominal request rate is paced; scheduler delays and actual completion rate are reported instead of hiding an under-driven client. Pending jobs are bounded to twice the client count. The script restores base configuration, drains and rechecks both listeners before shutdown. After each cache apply, two separate GETs per source verify exact contents and require a HIT on the second GET when enabled; globally disabled cache must expose no X-Cache header. These probe requests are recorded separately from paced traffic. Backend active-exchange counters must settle to zero after the final drain and cooldown.
Five-second resource/status records, body/status failures, observed cache-status
counts, cache transitions/probes, RSS/FD trend and completed traffic totals are
retained. Controlled client closes mean that the client closes after reading a
prefix. The server may already have sent the complete response, so those counts
are not presented as proven server-side cancellations.
Memory trend is descriptive: switching caching and allocator retention affect
RSS. This is a 30-minute resource gate, not a 24-hour leak proof. A short
--exploratory --duration ... sanity run stays explicitly outside that gate.