From a1b21e87b3394e8a91266def4ffac2277d11abdb Mon Sep 17 00:00:00 2001 From: ayou <550244300@qq.com> Date: Fri, 10 Oct 2025 23:23:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20=E6=B7=BB=E5=8A=A0=E9=92=88?= =?UTF-8?q?=E5=AF=B9code=5Fjs1=E6=8E=A5=E5=8F=A3=E7=9A=84=E5=8E=8B?= =?UTF-8?q?=E6=B5=8B=E8=84=9A=E6=9C=AC=E5=92=8C=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加压测脚本run_stress_code_js1.sh和stress_code_js1.py,用于对code_js1接口进行并发压力测试 生成压测结果JSON文件,包含请求统计、延迟和吞吐量等指标 --- scripts/results/code_js1_20251010_231059.json | 29 ++++ .../code_js1_nocsv_20251010_231332.json | 29 ++++ .../code_js1_quick_20251010_232217.json | 29 ++++ scripts/run_stress_code_js1.sh | 95 +++++++++++ scripts/stress_code_js1.py | 156 ++++++++++++++++++ 5 files changed, 338 insertions(+) create mode 100644 scripts/results/code_js1_20251010_231059.json create mode 100644 scripts/results/code_js1_nocsv_20251010_231332.json create mode 100644 scripts/results/code_js1_quick_20251010_232217.json create mode 100644 scripts/run_stress_code_js1.sh create mode 100644 scripts/stress_code_js1.py diff --git a/scripts/results/code_js1_20251010_231059.json b/scripts/results/code_js1_20251010_231059.json new file mode 100644 index 0000000..959492a --- /dev/null +++ b/scripts/results/code_js1_20251010_231059.json @@ -0,0 +1,29 @@ +{ + "url": "http://127.0.0.1:9898/api/dynamic/code_js1", + "method": "POST", + "total": 3000, + "concurrency": 150, + "timeout_sec": 8.0, + "ok": 3000, + "errors": 0, + "has_data": 3000, + "duration_ms": 13766.637417022139, + "throughput_rps": 217.91813854925584, + "status_counts": { + "200": 3000 + }, + "latency_ms": { + "min": 550.376542028971, + "avg": 686.4437982002273, + "max": 980.3312500007451, + "p50": 671.9021250028163, + "p90": 800.2895419485867, + "p95": 885.534834000282, + "p99": 946.9203340122476 + }, + "resp_size_bytes": { + "avg": 556, + "min": 556, + "max": 556 + } +} \ No newline at end of file diff --git a/scripts/results/code_js1_nocsv_20251010_231332.json b/scripts/results/code_js1_nocsv_20251010_231332.json new file mode 100644 index 0000000..150b45b --- /dev/null +++ b/scripts/results/code_js1_nocsv_20251010_231332.json @@ -0,0 +1,29 @@ +{ + "url": "http://127.0.0.1:9898/api/dynamic/code_js1", + "method": "POST", + "total": 100, + "concurrency": 20, + "timeout_sec": 5.0, + "ok": 100, + "errors": 0, + "has_data": 100, + "duration_ms": 486.8026250042021, + "throughput_rps": 205.42206402263503, + "status_counts": { + "200": 100 + }, + "latency_ms": { + "min": 56.31004192400724, + "avg": 94.25131994648837, + "max": 153.34666694980115, + "p50": 79.22116597183049, + "p90": 147.41216704715043, + "p95": 151.8984999274835, + "p99": 152.9659579973668 + }, + "resp_size_bytes": { + "avg": 556, + "min": 556, + "max": 556 + } +} \ No newline at end of file diff --git a/scripts/results/code_js1_quick_20251010_232217.json b/scripts/results/code_js1_quick_20251010_232217.json new file mode 100644 index 0000000..eed500a --- /dev/null +++ b/scripts/results/code_js1_quick_20251010_232217.json @@ -0,0 +1,29 @@ +{ + "url": "http://127.0.0.1:9898/api/dynamic/code_js1", + "method": "POST", + "total": 50, + "concurrency": 10, + "timeout_sec": 8.0, + "ok": 50, + "errors": 0, + "has_data": 50, + "duration_ms": 266.0578330978751, + "throughput_rps": 187.9290657140939, + "status_counts": { + "200": 50 + }, + "latency_ms": { + "min": 25.911415927112103, + "avg": 50.4597524413839, + "max": 117.64616600703448, + "p50": 37.48199995607138, + "p90": 103.3676250372082, + "p95": 107.0811670506373, + "p99": 108.35866699926555 + }, + "resp_size_bytes": { + "avg": 556, + "min": 556, + "max": 556 + } +} \ No newline at end of file diff --git a/scripts/run_stress_code_js1.sh b/scripts/run_stress_code_js1.sh new file mode 100644 index 0000000..f598c47 --- /dev/null +++ b/scripts/run_stress_code_js1.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# 默认参数 +URL="http://127.0.0.1:9898/api/dynamic/code_js1" +METHOD="POST" +TOTAL=1000 +CONCURRENCY=50 +TIMEOUT=8 +HEADERS='{"Content-Type":"application/json"}' +BODY='{}' +OUT_PREFIX="scripts/results/code_js1" + +print_usage() { + cat </dev/null 2>&1; then + echo "错误:未找到 python3,请先安装 Python。" >&2 + exit 1 +fi + +if [[ ! -f "scripts/stress_code_js1.py" ]]; then + echo "错误:未找到 scripts/stress_code_js1.py,请确认项目目录正确。" >&2 + exit 1 +fi + +# 确保输出目录存在 +mkdir -p "$(dirname "$OUT_PREFIX")" + +echo "运行压测:url=$URL method=$METHOD total=$TOTAL concurrency=$CONCURRENCY timeout=$TIMEOUT" +echo "请求头:$HEADERS" +echo "请求体:$BODY" +echo "结果前缀:$OUT_PREFIX (只保存 JSON)" +echo + +set -x +python3 scripts/stress_code_js1.py \ + --url "$URL" \ + --method "$METHOD" \ + --total "$TOTAL" \ + --concurrency "$CONCURRENCY" \ + --timeout "$TIMEOUT" \ + --headers "$HEADERS" \ + --body "$BODY" \ + --out-prefix "$OUT_PREFIX" +set +x + +echo +echo "完成。请在上方输出中查看保存的 JSON 结果路径。" \ No newline at end of file diff --git a/scripts/stress_code_js1.py b/scripts/stress_code_js1.py new file mode 100644 index 0000000..26b9f61 --- /dev/null +++ b/scripts/stress_code_js1.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +import argparse, json, time, os, concurrent.futures, statistics +from urllib import request +from urllib.error import HTTPError, URLError + +def parse_headers(hstr: str): + if not hstr: + return {} + try: + return json.loads(hstr) + except Exception: + return {} + +def has_data(payload): + if 'data' not in payload or payload['data'] is None: + return False + v = payload['data'] + if isinstance(v, (list, dict)): + return len(v) > 0 + if isinstance(v, str): + return len(v.strip()) > 0 + return True + +def one_request(idx, url, method, headers, body_bytes, timeout): + start = time.perf_counter() + try: + req = request.Request(url, data=body_bytes, headers=headers, method=method) + with request.urlopen(req, timeout=timeout) as resp: + body = resp.read() + elapsed = (time.perf_counter() - start) * 1000.0 + try: + payload = json.loads(body) + return { + 'idx': idx, + 'ok': True, + 'elapsed_ms': elapsed, + 'has_data': has_data(payload), + 'status': resp.status, + 'size': len(body) + } + except Exception as je: + return { + 'idx': idx, + 'ok': False, + 'elapsed_ms': elapsed, + 'error': f'invalid_json: {je}', + } + except HTTPError as e: + elapsed = (time.perf_counter() - start) * 1000.0 + return {'idx': idx, 'ok': False, 'elapsed_ms': elapsed, 'error': f'HTTP {e.code}: {e.reason}'} + except URLError as e: + elapsed = (time.perf_counter() - start) * 1000.0 + return {'idx': idx, 'ok': False, 'elapsed_ms': elapsed, 'error': f'URL error: {e.reason}'} + except Exception as e: + elapsed = (time.perf_counter() - start) * 1000.0 + return {'idx': idx, 'ok': False, 'elapsed_ms': elapsed, 'error': str(e)} + +def main(): + parser = argparse.ArgumentParser(description='Simple concurrent stress test runner for code_js1 API') + parser.add_argument('--url', required=True, help='Target URL') + parser.add_argument('--method', default='POST', help='HTTP method, default POST') + parser.add_argument('--total', type=int, default=2000, help='Total requests') + parser.add_argument('--concurrency', type=int, default=100, help='Concurrent workers') + parser.add_argument('--timeout', type=float, default=5.0, help='Per-request timeout in seconds') + parser.add_argument('--headers', default='{"Content-Type":"application/json"}', help='JSON headers string') + parser.add_argument('--body', default='{}', help='JSON body string (for POST)') + parser.add_argument('--out-prefix', default='scripts/results/code_js1', help='Output file prefix (without extension)') + + args = parser.parse_args() + + headers = parse_headers(args.headers) + try: + body_obj = json.loads(args.body) if args.body else {} + except Exception: + body_obj = {} + body_bytes = json.dumps(body_obj).encode('utf-8') if args.method.upper() != 'GET' else None + + print(f"Starting stress: url={args.url} method={args.method} total={args.total} concurrency={args.concurrency}") + + start_all = time.perf_counter() + latencies = [] + errors = [] + ok_count = 0 + has_data_count = 0 + resp_sizes = [] + statuses = {} + + with concurrent.futures.ThreadPoolExecutor(max_workers=args.concurrency) as ex: + futures = [ex.submit(one_request, i+1, args.url, args.method, headers, body_bytes, args.timeout) for i in range(args.total)] + for fut in concurrent.futures.as_completed(futures): + r = fut.result() + if r.get('ok'): + ok_count += 1 + latencies.append(r['elapsed_ms']) + if r.get('has_data'): has_data_count += 1 + resp_sizes.append(r.get('size', 0)) + statuses[r.get('status', 0)] = statuses.get(r.get('status', 0), 0) + 1 + else: + errors.append({'idx': r['idx'], 'error': r.get('error'), 'elapsed_ms': r.get('elapsed_ms')}) + + dur_ms = (time.perf_counter() - start_all) * 1000.0 + throughput = ok_count / (dur_ms / 1000.0) if dur_ms > 0 else 0.0 + + result = { + 'url': args.url, + 'method': args.method, + 'total': args.total, + 'concurrency': args.concurrency, + 'timeout_sec': args.timeout, + 'ok': ok_count, + 'errors': len(errors), + 'has_data': has_data_count, + 'duration_ms': dur_ms, + 'throughput_rps': throughput, + 'status_counts': statuses, + } + + if latencies: + lat_sorted = sorted(latencies) + def pct(p): + idx = max(min(int(p*len(lat_sorted)) - 1, len(lat_sorted)-1), 0) + return lat_sorted[idx] + result['latency_ms'] = { + 'min': lat_sorted[0], + 'avg': statistics.mean(latencies), + 'max': lat_sorted[-1], + 'p50': pct(0.50), + 'p90': pct(0.90), + 'p95': pct(0.95), + 'p99': pct(0.99), + } + result['resp_size_bytes'] = { + 'avg': statistics.mean(resp_sizes) if resp_sizes else 0, + 'min': min(resp_sizes) if resp_sizes else 0, + 'max': max(resp_sizes) if resp_sizes else 0, + } + + # ensure results dir exists + out_dir = os.path.dirname(args.out_prefix) + if out_dir and not os.path.exists(out_dir): + os.makedirs(out_dir, exist_ok=True) + + ts = time.strftime('%Y%m%d_%H%M%S') + json_path = f"{args.out_prefix}_{ts}.json" + + # save JSON summary only + with open(json_path, 'w', encoding='utf-8') as f: + json.dump(result, f, ensure_ascii=False, indent=2) + + # print summary + print("\n=== 压测结果总结 ===") + print(json.dumps(result, ensure_ascii=False, indent=2)) + print(f"结果已保存: {json_path}") + +if __name__ == '__main__': + main() \ No newline at end of file