from __future__ import annotations import argparse import json import statistics import threading import time import urllib.error import urllib.request from concurrent.futures import ThreadPoolExecutor, as_completed def main() -> None: parser = argparse.ArgumentParser(description="Run a lightweight chat SSE load test.") parser.add_argument("--base-url", default="http://127.0.0.1:8100/api") parser.add_argument("--phone", required=True) parser.add_argument("--code", default="123456") parser.add_argument("--message", default="请用一句话回复:压测验证") parser.add_argument("--concurrency", type=int, default=5) parser.add_argument("--requests", type=int, default=20) parser.add_argument("--timeout", type=int, default=90) args = parser.parse_args() token = login(args.base_url, args.phone, args.code, args.timeout) lock = threading.Lock() results: list[dict] = [] started_at = time.perf_counter() with ThreadPoolExecutor(max_workers=args.concurrency) as executor: futures = [ executor.submit(run_one, args.base_url, token, args.message, args.timeout) for _ in range(args.requests) ] for future in as_completed(futures): result = future.result() with lock: results.append(result) print(json.dumps(result, ensure_ascii=False)) elapsed = time.perf_counter() - started_at print_summary(results, elapsed) def login(base_url: str, phone: str, code: str, timeout: int) -> str: body = post_json(f"{base_url}/auth/login", {"phone": phone, "code": code}, timeout=timeout) return body["data"]["token"] def run_one(base_url: str, token: str, message: str, timeout: int) -> dict: started_at = time.perf_counter() try: session_body = post_json( f"{base_url}/chat/session", {"title": "压测会话"}, token=token, timeout=timeout, ) session_id = session_body["data"]["sessionId"] first_event_ms = None content_events = 0 error_message = "" request = urllib.request.Request( f"{base_url}/chat/completions", data=json.dumps({"sessionId": session_id, "message": message}).encode("utf-8"), headers={ "Content-Type": "application/json", "Authorization": f"Bearer {token}", }, ) with urllib.request.urlopen(request, timeout=timeout) as response: for raw_line in response: line = raw_line.decode("utf-8").strip() if not line.startswith("data:"): continue if first_event_ms is None: first_event_ms = elapsed_ms(started_at) if line == "data: [DONE]": break payload = json.loads(line[5:].strip()) if payload.get("type") == "content": content_events += 1 if payload.get("type") == "error": error_message = payload.get("message", "unknown error") return { "ok": not error_message and content_events > 0, "sessionId": session_id, "firstEventMs": first_event_ms, "totalMs": elapsed_ms(started_at), "contentEvents": content_events, "error": error_message, } except (urllib.error.URLError, TimeoutError, KeyError, ValueError) as exc: return { "ok": False, "sessionId": None, "firstEventMs": None, "totalMs": elapsed_ms(started_at), "contentEvents": 0, "error": str(exc), } def post_json(url: str, payload: dict, *, token: str | None = None, timeout: int) -> dict: headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" request = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"), headers=headers) with urllib.request.urlopen(request, timeout=timeout) as response: return json.loads(response.read().decode("utf-8")) def print_summary(results: list[dict], elapsed: float) -> None: ok_results = [result for result in results if result["ok"]] failed_results = [result for result in results if not result["ok"]] first_event_values = [result["firstEventMs"] for result in ok_results if result["firstEventMs"] is not None] total_values = [result["totalMs"] for result in ok_results] summary = { "totalRequests": len(results), "success": len(ok_results), "failed": len(failed_results), "elapsedSeconds": round(elapsed, 2), "firstEventMsAvg": round(statistics.mean(first_event_values), 2) if first_event_values else None, "firstEventMsP95": percentile(first_event_values, 95), "totalMsAvg": round(statistics.mean(total_values), 2) if total_values else None, "totalMsP95": percentile(total_values, 95), } print("SUMMARY " + json.dumps(summary, ensure_ascii=False)) def percentile(values: list[int], percentile_value: int) -> int | None: if not values: return None sorted_values = sorted(values) index = min(len(sorted_values) - 1, round((percentile_value / 100) * (len(sorted_values) - 1))) return sorted_values[index] def elapsed_ms(started_at: float) -> int: return int((time.perf_counter() - started_at) * 1000) if __name__ == "__main__": main()