Files

105 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""
throughput.py - a minimal, dependency-free concurrent HTTP load generator.
This repo intentionally does not shell out to wrk/hey/ab (neither was available in the
sandbox this repo was built in, and a stdlib script means the load generator itself needs
no separate install or version callout). Each worker thread keeps one persistent
HTTP/1.1 connection (http.client, keep-alive) and fires GET requests back-to-back for a
fixed wall-clock duration; the main thread joins all workers and aggregates.
Usage:
throughput.py <label> <host> <port> <path> <duration_s> <concurrency>
Prints one JSON line: requests, errors, requests_per_sec, latency_ms mean/p50/p95/p99.
"""
import http.client
import json
import statistics
import sys
import threading
import time
def worker(host, port, path, stop_at, latencies, counters, idx):
conn = http.client.HTTPConnection(host, port, timeout=5)
local_ok = 0
local_err = 0
local_latencies = []
while time.monotonic() < stop_at:
t0 = time.monotonic()
try:
conn.request("GET", path)
resp = conn.getresponse()
resp.read()
if resp.status == 200:
local_ok += 1
else:
local_err += 1
except Exception:
local_err += 1
try:
conn.close()
except Exception:
pass
conn = http.client.HTTPConnection(host, port, timeout=5)
local_latencies.append((time.monotonic() - t0) * 1000.0)
counters[idx] = (local_ok, local_err)
latencies[idx] = local_latencies
try:
conn.close()
except Exception:
pass
def main():
label, host, port, path, duration_s, concurrency = sys.argv[1:7]
port = int(port)
duration_s = float(duration_s)
concurrency = int(concurrency)
counters = [None] * concurrency
latencies = [None] * concurrency
stop_at = time.monotonic() + duration_s
threads = [
threading.Thread(target=worker, args=(host, port, path, stop_at, latencies, counters, i))
for i in range(concurrency)
]
wall_start = time.monotonic()
for t in threads:
t.start()
for t in threads:
t.join()
wall_elapsed = time.monotonic() - wall_start
total_ok = sum(c[0] for c in counters)
total_err = sum(c[1] for c in counters)
all_latencies = [x for sub in latencies for x in sub]
all_latencies.sort()
def pct(p):
if not all_latencies:
return None
k = min(len(all_latencies) - 1, int(len(all_latencies) * p))
return round(all_latencies[k], 2)
result = {
"label": label,
"path": path,
"concurrency": concurrency,
"duration_s": round(wall_elapsed, 2),
"requests_ok": total_ok,
"requests_err": total_err,
"requests_per_sec": round(total_ok / wall_elapsed, 1) if wall_elapsed > 0 else None,
"latency_ms_mean": round(statistics.mean(all_latencies), 2) if all_latencies else None,
"latency_ms_p50": pct(0.50),
"latency_ms_p95": pct(0.95),
"latency_ms_p99": pct(0.99),
}
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()