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 <noreply@anthropic.com>
This commit is contained in:
Vladimir Dubrovin 2026-10-25 18:24:50 +03:00
parent 1fb4a313c5
commit 23b9619da1
3 changed files with 15 additions and 8 deletions

View File

@ -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"]

View File

@ -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)

View File

@ -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: