-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstress.py
More file actions
140 lines (118 loc) · 5.21 KB
/
Copy pathstress.py
File metadata and controls
140 lines (118 loc) · 5.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
"""Concurrency stress test — doubles the parallel request count each round.
Fires a batch of simultaneous requests at the running server. If every request
in the batch succeeds, the batch size is doubled and the next round runs. The
test stops at the first round that produces any error (HTTP error, timeout,
connection failure, or an error payload in the response), and reports the last
batch size that fully succeeded.
Note: the server serializes upstream Copilot calls behind a single lock
(see server/api.py), so concurrent requests queue and run one at a time. This
test therefore probes how the server copes with a growing *queue* of waiting
connections (socket/timeout limits), not true upstream parallelism.
# 1. Start the server in another terminal
python app.py
# 2. Run the stress test from the project root
python tests/stress.py
python tests/stress.py --max 64 --timeout 120 --url http://localhost:8000
Be considerate: this hammers your Copilot account. Keep --max modest.
"""
import argparse
import json
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
PROMPT = "Reply with a single word: ok"
def one_request(url, timeout, index):
"""Send a single chat completion. Returns (ok, elapsed, detail)."""
body = json.dumps({
"model": "copilot",
"messages": [{"role": "user", "content": PROMPT}],
}).encode("utf-8")
req = urllib.request.Request(
url,
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
start = time.perf_counter()
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
payload = json.loads(resp.read().decode("utf-8"))
elapsed = time.perf_counter() - start
# The server returns HTTP 200 with an "error" object on upstream
# failures, so inspect the body too — not just the status code.
if "error" in payload:
return False, elapsed, f"error payload: {payload['error']}"
content = payload["choices"][0]["message"]["content"]
return True, elapsed, content.strip()[:40]
except urllib.error.HTTPError as exc:
return False, time.perf_counter() - start, f"HTTP {exc.code}: {exc.reason}"
except Exception as exc: # timeout, connection reset, malformed body, ...
return False, time.perf_counter() - start, f"{type(exc).__name__}: {exc}"
def run_round(url, timeout, concurrency):
"""Fire `concurrency` requests at once. Returns (results, wall_seconds)."""
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=concurrency) as pool:
futures = [
pool.submit(one_request, url, timeout, i) for i in range(concurrency)
]
results = [f.result() for f in as_completed(futures)]
return results, time.perf_counter() - start
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--url", default="http://localhost:8000",
help="Server base URL (default: http://localhost:8000)",
)
parser.add_argument(
"--start", type=int, default=1,
help="Starting concurrency (default: 1)",
)
parser.add_argument(
"--max", type=int, default=128,
help="Stop after this batch size even if it succeeds (default: 128)",
)
parser.add_argument(
"--timeout", type=float, default=180,
help="Per-request timeout in seconds (default: 180)",
)
parser.add_argument(
"--pause", type=float, default=1.0,
help="Seconds to wait between rounds (default: 1.0)",
)
args = parser.parse_args()
endpoint = args.url.rstrip("/") + "/v1/chat/completions"
print(f"Stress testing {endpoint}")
print(f"Doubling concurrency from {args.start} up to {args.max}, "
f"timeout {args.timeout}s\n")
concurrency = args.start
last_good = 0
try:
while concurrency <= args.max:
print(f"── Round: {concurrency} concurrent "
f"request{'s' if concurrency > 1 else ''} ──")
results, wall = run_round(endpoint, args.timeout, concurrency)
oks = [r for r in results if r[0]]
fails = [r for r in results if not r[0]]
latencies = sorted(r[1] for r in results)
lo, hi = latencies[0], latencies[-1]
mid = latencies[len(latencies) // 2]
print(f" ok={len(oks)} failed={len(fails)} wall={wall:.1f}s")
print(f" latency min={lo:.1f}s median={mid:.1f}s max={hi:.1f}s")
if fails:
print(f"\n✗ {len(fails)} request(s) failed at concurrency "
f"{concurrency}. Sample errors:")
for _, elapsed, detail in fails[:5]:
print(f" [{elapsed:.1f}s] {detail}")
break
last_good = concurrency
print(f" ✓ all {concurrency} succeeded\n")
concurrency *= 2
time.sleep(args.pause)
else:
print(f"\nReached --max={args.max} with no errors.")
except KeyboardInterrupt:
print("\nInterrupted.")
print(f"\nHighest fully-successful concurrency: {last_good}")
if __name__ == "__main__":
main()