Coverage for agentos/benchmarks/runner.py: 47%
89 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 01:44 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 01:44 +0800
1"""v0.80 — 性能基准测试运行器:延迟/吞吐/并发。"""
3from __future__ import annotations
5import asyncio
6import json
7import time
8from collections.abc import Awaitable, Callable
9from dataclasses import dataclass, field
10from typing import Any
13@dataclass
14class BenchmarkScenario:
15 """单个基准测试场景。"""
17 name: str
18 description: str = ""
19 setup: Callable[[], Any] | None = None
20 teardown: Callable[[Any], None] | None = None
23@dataclass
24class BenchmarkConfig:
25 """基准测试配置。"""
27 warmup_iterations: int = 3
28 measure_iterations: int = 10
29 concurrency_levels: list[int] = field(default_factory=lambda: [1, 4, 8])
30 timeout_per_run: float = 30.0
33@dataclass
34class _LatencyStats:
35 min_ms: float = 0
36 max_ms: float = 0
37 avg_ms: float = 0
38 p50_ms: float = 0
39 p95_ms: float = 0
40 p99_ms: float = 0
42 @staticmethod
43 def compute(latencies_ms: list[float]) -> _LatencyStats:
44 if not latencies_ms:
45 return _LatencyStats()
46 s = sorted(latencies_ms)
47 n = len(s)
48 return _LatencyStats(
49 min_ms=s[0],
50 max_ms=s[-1],
51 avg_ms=sum(s) / n,
52 p50_ms=s[int(n * 0.5)],
53 p95_ms=s[int(n * 0.95)] if int(n * 0.95) < n else s[-1],
54 p99_ms=s[int(n * 0.99)] if int(n * 0.99) < n else s[-1],
55 )
58@dataclass
59class BenchmarkReport:
60 """基准测试报告。"""
62 scenario: str = ""
63 description: str = ""
64 config: BenchmarkConfig = field(default_factory=BenchmarkConfig)
65 results: list[dict[str, Any]] = field(default_factory=list)
66 summary: str = ""
68 def to_json(self) -> str:
69 return json.dumps(
70 {
71 "scenario": self.scenario,
72 "description": self.description,
73 "results": self.results,
74 "summary": self.summary,
75 },
76 indent=2,
77 ensure_ascii=False,
78 )
80 def to_markdown(self) -> str:
81 lines = [
82 f"# Benchmark: {self.scenario}",
83 "",
84 f"_{self.description}_",
85 "",
86 "| 并发 | 总调用 | 总耗时(s) | 吞吐(QPS) | 平均延迟(ms) | P50(ms) | P95(ms) | P99(ms) | 成功率 |",
87 "|------|--------|-----------|-----------|-------------|---------|---------|---------|--------|",
88 ]
89 for r in self.results:
90 lines.append(
91 f"| {r['concurrency']} | {r['total_calls']} | {r['total_time_s']:.2f} | "
92 f"{r['throughput_qps']:.1f} | {r['latency_stats']['avg_ms']:.1f} | "
93 f"{r['latency_stats']['p50_ms']:.1f} | {r['latency_stats']['p95_ms']:.1f} | "
94 f"{r['latency_stats']['p99_ms']:.1f} | {r['success_rate']*100:.0f}% |"
95 )
96 if self.summary:
97 lines.extend(["", f"> {self.summary}"])
98 return "\n".join(lines)
101class BenchmarkRunner:
102 """基准测试运行器。"""
104 def __init__(self, config: BenchmarkConfig | None = None):
105 self.config = config or BenchmarkConfig()
107 async def run(
108 self,
109 scenario: BenchmarkScenario,
110 callable_fn: Callable[[], Any],
111 async_callable_fn: Callable[[], Awaitable[Any]] | None = None,
112 ) -> BenchmarkReport:
113 """运行基准测试。
115 Args:
116 scenario: 测试场景。
117 callable_fn: 同步测试函数。
118 async_callable_fn: 异步测试函数(用于并发测试)。
119 """
120 setup_state = scenario.setup() if scenario.setup else None
122 results: list[dict[str, Any]] = []
124 for concurrency in self.config.concurrency_levels:
125 total_calls = concurrency * self.config.measure_iterations
126 total_start = time.perf_counter()
127 success = 0
128 latencies_ms: list[float] = []
130 async def _one_call():
131 nonlocal success
132 t0 = time.perf_counter()
133 try:
134 if async_callable_fn:
135 await async_callable_fn()
136 else:
137 callable_fn()
138 success += 1
139 except Exception:
140 pass
141 latencies_ms.append((time.perf_counter() - t0) * 1000)
143 # warmup
144 for _ in range(self.config.warmup_iterations):
145 try:
146 callable_fn()
147 except Exception:
148 pass
150 # measure
151 tasks = [_one_call() for _ in range(total_calls)]
152 await asyncio.gather(*tasks)
154 total_time = time.perf_counter() - total_start
155 stats = _LatencyStats.compute(latencies_ms)
157 results.append(
158 {
159 "concurrency": concurrency,
160 "total_calls": total_calls,
161 "total_time_s": round(total_time, 3),
162 "throughput_qps": round(total_calls / total_time, 1) if total_time > 0 else 0,
163 "latency_stats": {
164 "min_ms": round(stats.min_ms, 2),
165 "max_ms": round(stats.max_ms, 2),
166 "avg_ms": round(stats.avg_ms, 2),
167 "p50_ms": round(stats.p50_ms, 2),
168 "p95_ms": round(stats.p95_ms, 2),
169 "p99_ms": round(stats.p99_ms, 2),
170 },
171 "success_rate": round(success / total_calls, 4) if total_calls else 0,
172 }
173 )
175 if scenario.teardown and setup_state is not None:
176 scenario.teardown(setup_state)
178 avg_throughput = sum(r["throughput_qps"] for r in results) / len(results) if results else 0
179 return BenchmarkReport(
180 scenario=scenario.name,
181 description=scenario.description,
182 config=self.config,
183 results=results,
184 summary=f"平均吞吐: {avg_throughput:.1f} QPS | 并发级别: {self.config.concurrency_levels}",
185 )
188async def run_benchmark(
189 scenario_name: str,
190 callable_fn: Callable[[], Any],
191 config: BenchmarkConfig | None = None,
192) -> BenchmarkReport:
193 """便捷函数:运行一次基准测试并返回 Markdown 报告。"""
194 runner = BenchmarkRunner(config)
195 scenario = BenchmarkScenario(name=scenario_name)
196 return await runner.run(scenario, callable_fn)