From 23b9619da12e1cbf223e088f4257b24bd70aa665 Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Sun, 25 Oct 2026 18:24:50 +0300 Subject: [PATCH] bench: 75th percentile latency, loops for -L, BENCH_WORKERS The CSV has p50, p75, p90 and p99 now, for wrk (its --latency distribution) and for the mix load generator alike; report.py puts in the table those the results have. 3proxy with -L among its options is given as many event loops as the proxy has CPUs, as envoy is given workers; BENCH_WORKERS sets that count for both whatever the CPUs. Co-Authored-By: Claude Opus 5.5 --- bench/bench.py | 13 +++++++++---- bench/mix/main.go | 5 +++-- bench/report.py | 5 +++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/bench/bench.py b/bench/bench.py index 192c493..e11ac66 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -563,6 +563,8 @@ def proxy_conf(kind, work, ncpu, label, opts="", out=PRX_OUT): on TCP_PORT, both towards the origin. Returns the argv. opts are added to both 3proxy services, e.g. -s0 to keep a version from using splice(). out is the address the proxy connects from.""" + # BENCH_WORKERS: as many workers (envoys, 3proxy's loops) whatever the CPUs + ncpu = int(os.environ.get("BENCH_WORKERS", ncpu)) if kind == "3proxy": # ktls among the options is not a service option: kernel TLS for the # TLS services (ssl_ktls) @@ -571,6 +573,9 @@ def proxy_conf(kind, work, ncpu, label, opts="", out=PRX_OUT): opts = " ".join(w for w in words if w != "ktls") conf = os.path.join(work, f"{label}.cfg") with open(conf, "w") as f: + # on event loops: as many as the proxy has CPUs, as envoy is given + if "-L" in opts.split(): + f.write(f"loops {ncpu}\n") f.write(f"""maxconn 100000 auth none proxy {opts} -i{PRX_IN} -e{out} -p{HTTP_PORT} @@ -998,7 +1003,7 @@ def run_http(proxy, params, args, work, mark): res["rate"] = float(m.group(1)) if m else 0.0 m = re.search(r"Transfer/sec:\s+([\d.]+)(\w+)", out) res["gbit"] = float(m.group(1)) * UNITS[m.group(2)] * 8 / 1e9 if m else 0.0 - for pct in ("50", "90", "99"): + for pct in ("50", "75", "90", "99"): m = re.search(rf"^\s+{pct}%\s+(\S+)", out, re.M) res[f"p{pct}_ms"] = to_ms(m.group(1)) if m else float("nan") errors = 0 @@ -1052,7 +1057,7 @@ def run_stream(proxy, params, args, work, mark): finally: if wrapper: stop(wrapper) - res = {"raw": out, "rate": float("nan"), "p50_ms": float("nan"), "p90_ms": float("nan"), + res = {"raw": out, "rate": float("nan"), "p50_ms": float("nan"), "p75_ms": float("nan"), "p90_ms": float("nan"), "p99_ms": float("nan"), "requests": 0, "errors": 0, "retrans": 0, "timeouts": 0} try: j = json.loads(out) @@ -1126,7 +1131,7 @@ def run_mix(proxy, params, args, work, mark): res["rate"] = j.get("rate", 0.0) res["gbit"] = j.get("gbit", 0.0) res["bytes"] = j.get("bytes", 0) - for pct in ("50", "90", "99"): + for pct in ("50", "75", "90", "99"): res[f"p{pct}_ms"] = j.get(f"p{pct}_ms", float("nan")) res["timeouts"] = j.get("timeouts", 0) # connections of the pool which could not be opened count too @@ -1190,7 +1195,7 @@ def measure(proxy, scenario, args, work): # --- main -------------------------------------------------------------------- -FIELDS = ["proxy", "scenario", "rep", "gbit", "rate", "p50_ms", "p90_ms", "p99_ms", "errors", "timeouts", "retrans", +FIELDS = ["proxy", "scenario", "rep", "gbit", "rate", "p50_ms", "p75_ms", "p90_ms", "p99_ms", "errors", "timeouts", "retrans", "prx_cpu_s", "prx_task_s", "cpu_s_per_gb", "cpu_us_per_req", "peak_mb", "cli_util", "prx_util", "srv_util", "cli_thread", "srv_thread", "bound"] diff --git a/bench/mix/main.go b/bench/mix/main.go index e09982d..b8b9e6c 100644 --- a/bench/mix/main.go +++ b/bench/mix/main.go @@ -722,6 +722,7 @@ type summary struct { Gbit float64 `json:"gbit"` Bytes uint64 `json:"bytes"` P50ms float64 `json:"p50_ms"` + P75ms float64 `json:"p75_ms"` P90ms float64 `json:"p90_ms"` P99ms float64 `json:"p99_ms"` FirstP50ms float64 `json:"first_p50_ms"` @@ -848,8 +849,8 @@ func runClient(cl *client, active int, rate float64, warmup, duration time.Durat Bytes: cl.bytes.Load(), Errors: cl.errs.Load(), Timeouts: cl.timeouts.Load(), Reconnects: cl.reconnects.Load(), Late: late.Load(), Aborts: cl.aborts.Load(), Resets: cl.resets.Load(), HalfCloses: cl.halfcloses.Load(), - P50ms: cl.complete.quantile(0.5), P90ms: cl.complete.quantile(0.9), - P99ms: cl.complete.quantile(0.99), + P50ms: cl.complete.quantile(0.5), P75ms: cl.complete.quantile(0.75), + P90ms: cl.complete.quantile(0.9), P99ms: cl.complete.quantile(0.99), FirstP50ms: cl.first.quantile(0.5), FirstP99ms: cl.first.quantile(0.99)} s.DialError, _ = cl.dialErr.Load().(string) s.ExchError, _ = cl.exchErr.Load().(string) diff --git a/bench/report.py b/bench/report.py index 4b04ad3..bfe201c 100644 --- a/bench/report.py +++ b/bench/report.py @@ -270,8 +270,9 @@ def latency(path, labels, splice, prefix=None): rule.append(":-:") head += [labels.get(p, p) for p in g] rule += ["---:"] * len(g) - pcts = ["p50_ms", "p90_ms", "p99_ms"] if any(values(rs, "p90_ms") for rs in by.values()) \ - else ["p50_ms", "p99_ms"] + # the percentiles the results have: older ones only p50 and p99 + pcts = [c for c in ("p50_ms", "p75_ms", "p90_ms", "p99_ms") + if c in ("p50_ms", "p99_ms") or any(values(rs, c) for rs in by.values())] out = [f"| scenario, {' / '.join(c[:3] for c in pcts)} ms | " + " | ".join(head) + " |", "|---|" + "|".join(rule) + "|"] for s in scenarios: