pytest-async-benchmark 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_async_benchmark/__init__.py +31 -0
- pytest_async_benchmark/analytics.py +49 -0
- pytest_async_benchmark/comparison.py +241 -0
- pytest_async_benchmark/display.py +169 -0
- pytest_async_benchmark/plugin.py +91 -0
- pytest_async_benchmark/runner.py +56 -0
- pytest_async_benchmark/stats.py +36 -0
- pytest_async_benchmark/utils.py +30 -0
- pytest_async_benchmark-0.1.1.dist-info/METADATA +518 -0
- pytest_async_benchmark-0.1.1.dist-info/RECORD +13 -0
- pytest_async_benchmark-0.1.1.dist-info/WHEEL +4 -0
- pytest_async_benchmark-0.1.1.dist-info/entry_points.txt +2 -0
- pytest_async_benchmark-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""pytest-async-benchmark: Modern pytest benchmarking for async code. ๐"""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.1"
|
|
4
|
+
__author__ = "Mihai Farcas"
|
|
5
|
+
__email__ = "contact@mihai.ltd"
|
|
6
|
+
|
|
7
|
+
from .analytics import benchmark_summary, compare_benchmarks, performance_grade
|
|
8
|
+
from .comparison import (
|
|
9
|
+
BenchmarkComparator,
|
|
10
|
+
BenchmarkScenario,
|
|
11
|
+
a_vs_b_comparison,
|
|
12
|
+
quick_compare,
|
|
13
|
+
)
|
|
14
|
+
from .display import display_comparison_table, format_speedup, format_time
|
|
15
|
+
from .plugin import async_benchmark
|
|
16
|
+
from .runner import AsyncBenchmarkRunner
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"async_benchmark",
|
|
20
|
+
"AsyncBenchmarkRunner",
|
|
21
|
+
"BenchmarkComparator",
|
|
22
|
+
"BenchmarkScenario",
|
|
23
|
+
"quick_compare",
|
|
24
|
+
"a_vs_b_comparison",
|
|
25
|
+
"display_comparison_table",
|
|
26
|
+
"format_time",
|
|
27
|
+
"format_speedup",
|
|
28
|
+
"compare_benchmarks",
|
|
29
|
+
"performance_grade",
|
|
30
|
+
"benchmark_summary",
|
|
31
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Additional utilities for pytest-async-benchmark."""
|
|
2
|
+
|
|
3
|
+
import statistics
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def compare_benchmarks(
|
|
8
|
+
result1: dict[str, Any], result2: dict[str, Any]
|
|
9
|
+
) -> dict[str, float]:
|
|
10
|
+
"""Compare two benchmark results and return relative performance metrics."""
|
|
11
|
+
return {
|
|
12
|
+
"mean_ratio": result2["mean"] / result1["mean"],
|
|
13
|
+
"min_ratio": result2["min"] / result1["min"],
|
|
14
|
+
"max_ratio": result2["max"] / result1["max"],
|
|
15
|
+
"speedup": result1["mean"] / result2["mean"],
|
|
16
|
+
"relative_stddev": (result2["stddev"] - result1["stddev"]) / result1["stddev"],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def benchmark_summary(results: list[dict[str, Any]]) -> dict[str, Any]:
|
|
21
|
+
"""Create a summary of multiple benchmark results."""
|
|
22
|
+
all_means = [r["mean"] for r in results]
|
|
23
|
+
all_stddevs = [r["stddev"] for r in results]
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"total_tests": len(results),
|
|
27
|
+
"fastest_mean": min(all_means),
|
|
28
|
+
"slowest_mean": max(all_means),
|
|
29
|
+
"average_mean": statistics.mean(all_means),
|
|
30
|
+
"mean_variation": statistics.stdev(all_means) if len(all_means) > 1 else 0,
|
|
31
|
+
"stability_score": 1
|
|
32
|
+
- (statistics.mean(all_stddevs) / statistics.mean(all_means)),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def performance_grade(result: dict[str, Any], thresholds: dict[str, float]) -> str:
|
|
37
|
+
"""Grade performance based on configurable thresholds."""
|
|
38
|
+
mean_time = result["mean"]
|
|
39
|
+
|
|
40
|
+
if mean_time <= thresholds.get("excellent", 0.001):
|
|
41
|
+
return "A+"
|
|
42
|
+
elif mean_time <= thresholds.get("good", 0.005):
|
|
43
|
+
return "A"
|
|
44
|
+
elif mean_time <= thresholds.get("acceptable", 0.01):
|
|
45
|
+
return "B"
|
|
46
|
+
elif mean_time <= thresholds.get("slow", 0.05):
|
|
47
|
+
return "C"
|
|
48
|
+
else:
|
|
49
|
+
return "D"
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comparison utilities for pytest-async-benchmark.
|
|
3
|
+
|
|
4
|
+
This module provides high-level utilities for comparing multiple benchmark results
|
|
5
|
+
and displaying them in various formats.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Awaitable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from .analytics import compare_benchmarks, performance_grade
|
|
17
|
+
from .display import display_comparison_table, format_speedup, format_time
|
|
18
|
+
from .runner import AsyncBenchmarkRunner
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class BenchmarkScenario:
|
|
23
|
+
"""Represents a single benchmark scenario."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
func: Callable[[], Awaitable[Any]]
|
|
27
|
+
rounds: int = 5
|
|
28
|
+
iterations: int = 10
|
|
29
|
+
description: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BenchmarkComparator:
|
|
33
|
+
"""High-level utility for comparing multiple benchmarks."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, console: Optional[Console] = None):
|
|
36
|
+
self.console = console or Console()
|
|
37
|
+
self.runner = AsyncBenchmarkRunner()
|
|
38
|
+
self.results: list[dict[str, Any]] = []
|
|
39
|
+
|
|
40
|
+
async def add_scenario(
|
|
41
|
+
self,
|
|
42
|
+
name: str,
|
|
43
|
+
func: Callable[[], Awaitable[Any]],
|
|
44
|
+
rounds: int = 5,
|
|
45
|
+
iterations: int = 10,
|
|
46
|
+
description: Optional[str] = None,
|
|
47
|
+
) -> dict[str, Any]:
|
|
48
|
+
"""Add and run a benchmark scenario."""
|
|
49
|
+
scenario = BenchmarkScenario(name, func, rounds, iterations, description)
|
|
50
|
+
|
|
51
|
+
self.console.print(f"๐ Running scenario: [cyan]{name}[/cyan]")
|
|
52
|
+
if description:
|
|
53
|
+
self.console.print(f" {description}")
|
|
54
|
+
|
|
55
|
+
runner = AsyncBenchmarkRunner(rounds=rounds, iterations=iterations)
|
|
56
|
+
result = await runner.run(func)
|
|
57
|
+
|
|
58
|
+
scenario_result = {
|
|
59
|
+
"name": name,
|
|
60
|
+
"result": result,
|
|
61
|
+
"description": description,
|
|
62
|
+
"scenario": scenario,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
self.results.append(scenario_result)
|
|
66
|
+
return scenario_result
|
|
67
|
+
|
|
68
|
+
def display_all_results(self, title: str = "๐ Benchmark Results Summary"):
|
|
69
|
+
"""Display all collected results in a summary table."""
|
|
70
|
+
if not self.results:
|
|
71
|
+
self.console.print("โ No benchmark results to display")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
display_comparison_table(self.results, title=title, console=self.console)
|
|
75
|
+
|
|
76
|
+
def display_pairwise_comparison(
|
|
77
|
+
self, name1: str, name2: str, title: Optional[str] = None
|
|
78
|
+
):
|
|
79
|
+
"""Display a detailed comparison between two specific scenarios."""
|
|
80
|
+
result1 = next((r for r in self.results if r["name"] == name1), None)
|
|
81
|
+
result2 = next((r for r in self.results if r["name"] == name2), None)
|
|
82
|
+
|
|
83
|
+
if not result1 or not result2:
|
|
84
|
+
self.console.print(f"โ Could not find results for '{name1}' or '{name2}'")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
comparison_title = title or f"โก {name1} vs {name2}"
|
|
88
|
+
|
|
89
|
+
display_comparison_table(
|
|
90
|
+
[result1, result2], title=comparison_title, console=self.console
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
comparison = compare_benchmarks(result1["result"], result2["result"])
|
|
94
|
+
|
|
95
|
+
self.console.print("\n๐ Detailed Analysis:")
|
|
96
|
+
analysis_text = Text()
|
|
97
|
+
|
|
98
|
+
speedup = comparison["speedup"]
|
|
99
|
+
if speedup > 1.2:
|
|
100
|
+
analysis_text.append(f"๐ {name2} is significantly faster ", style="green")
|
|
101
|
+
analysis_text.append(f"({format_speedup(speedup)})", style="bold green")
|
|
102
|
+
elif speedup < 0.8:
|
|
103
|
+
analysis_text.append(f"๐ {name1} is significantly faster ", style="red")
|
|
104
|
+
analysis_text.append(f"({format_speedup(1 / speedup)})", style="bold red")
|
|
105
|
+
else:
|
|
106
|
+
analysis_text.append(
|
|
107
|
+
"โ๏ธ Both scenarios have similar performance", style="yellow"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self.console.print(Panel(analysis_text, title="Performance Summary"))
|
|
111
|
+
|
|
112
|
+
def display_performance_grades(self, thresholds: Optional[dict[str, float]] = None):
|
|
113
|
+
"""Display performance grades for all scenarios."""
|
|
114
|
+
if not self.results:
|
|
115
|
+
self.console.print("โ No results to grade")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
default_thresholds = {
|
|
119
|
+
"excellent": 0.001,
|
|
120
|
+
"good": 0.005,
|
|
121
|
+
"acceptable": 0.01,
|
|
122
|
+
"slow": 0.05,
|
|
123
|
+
}
|
|
124
|
+
thresholds = thresholds or default_thresholds
|
|
125
|
+
|
|
126
|
+
from rich.table import Table
|
|
127
|
+
|
|
128
|
+
table = Table(title="๐ Performance Grades")
|
|
129
|
+
table.add_column("Scenario", style="cyan")
|
|
130
|
+
table.add_column("Mean Time", style="yellow", justify="right")
|
|
131
|
+
table.add_column("Grade", style="green", justify="center")
|
|
132
|
+
table.add_column("Status", justify="center")
|
|
133
|
+
|
|
134
|
+
for result in self.results:
|
|
135
|
+
grade = performance_grade(result["result"], thresholds)
|
|
136
|
+
mean_time = format_time(result["result"]["mean"])
|
|
137
|
+
|
|
138
|
+
status = {"A+": "๐", "A": "๐ฅ", "B": "๐ฅ", "C": "๐ฅ", "D": "๐"}.get(
|
|
139
|
+
grade, "โ"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
grade_style = {
|
|
143
|
+
"A+": "bold green",
|
|
144
|
+
"A": "green",
|
|
145
|
+
"B": "yellow",
|
|
146
|
+
"C": "orange3",
|
|
147
|
+
"D": "red",
|
|
148
|
+
}.get(grade, "white")
|
|
149
|
+
|
|
150
|
+
table.add_row(
|
|
151
|
+
result["name"],
|
|
152
|
+
mean_time,
|
|
153
|
+
f"[{grade_style}]{grade}[/{grade_style}]",
|
|
154
|
+
status,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
self.console.print(table)
|
|
158
|
+
|
|
159
|
+
def get_fastest_scenario(self) -> Optional[dict[str, Any]]:
|
|
160
|
+
"""Get the scenario with the fastest mean time."""
|
|
161
|
+
if not self.results:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
return min(self.results, key=lambda r: r["result"]["mean"])
|
|
165
|
+
|
|
166
|
+
def get_most_stable_scenario(self) -> Optional[dict[str, Any]]:
|
|
167
|
+
"""Get the scenario with the lowest standard deviation."""
|
|
168
|
+
if not self.results:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
return min(self.results, key=lambda r: r["result"]["stddev"])
|
|
172
|
+
|
|
173
|
+
def export_results(self) -> list[dict[str, Any]]:
|
|
174
|
+
"""Export all results for external analysis."""
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
"name": r["name"],
|
|
178
|
+
"description": r["description"],
|
|
179
|
+
"mean": r["result"]["mean"],
|
|
180
|
+
"min": r["result"]["min"],
|
|
181
|
+
"max": r["result"]["max"],
|
|
182
|
+
"median": r["result"]["median"],
|
|
183
|
+
"stddev": r["result"]["stddev"],
|
|
184
|
+
"rounds": r["result"]["rounds"],
|
|
185
|
+
"iterations": r["result"]["iterations"],
|
|
186
|
+
}
|
|
187
|
+
for r in self.results
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
def clear_results(self):
|
|
191
|
+
"""Clear all stored results."""
|
|
192
|
+
self.results.clear()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def quick_compare(
|
|
196
|
+
scenarios: list[BenchmarkScenario],
|
|
197
|
+
title: str = "๐ Quick Benchmark Comparison",
|
|
198
|
+
console: Optional[Console] = None,
|
|
199
|
+
) -> BenchmarkComparator:
|
|
200
|
+
"""Run a quick comparison of multiple scenarios."""
|
|
201
|
+
comparator = BenchmarkComparator(console)
|
|
202
|
+
|
|
203
|
+
comparator.console.print(f"\n[bold blue]{title}[/bold blue]")
|
|
204
|
+
comparator.console.print("=" * 50)
|
|
205
|
+
|
|
206
|
+
for scenario in scenarios:
|
|
207
|
+
await comparator.add_scenario(
|
|
208
|
+
scenario.name,
|
|
209
|
+
scenario.func,
|
|
210
|
+
scenario.rounds,
|
|
211
|
+
scenario.iterations,
|
|
212
|
+
scenario.description,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
comparator.display_all_results(title)
|
|
216
|
+
comparator.display_performance_grades()
|
|
217
|
+
|
|
218
|
+
return comparator
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def a_vs_b_comparison(
|
|
222
|
+
name_a: str,
|
|
223
|
+
func_a: Callable[[], Awaitable[Any]],
|
|
224
|
+
name_b: str,
|
|
225
|
+
func_b: Callable[[], Awaitable[Any]],
|
|
226
|
+
rounds: int = 10,
|
|
227
|
+
iterations: int = 50,
|
|
228
|
+
console: Optional[Console] = None,
|
|
229
|
+
) -> BenchmarkComparator:
|
|
230
|
+
"""Quick A vs B comparison utility."""
|
|
231
|
+
comparator = BenchmarkComparator(console)
|
|
232
|
+
|
|
233
|
+
comparator.console.print(f"\n[bold magenta]โ๏ธ {name_a} vs {name_b}[/bold magenta]")
|
|
234
|
+
comparator.console.print("=" * 50)
|
|
235
|
+
|
|
236
|
+
await comparator.add_scenario(name_a, func_a, rounds, iterations)
|
|
237
|
+
await comparator.add_scenario(name_b, func_b, rounds, iterations)
|
|
238
|
+
|
|
239
|
+
comparator.display_pairwise_comparison(name_a, name_b)
|
|
240
|
+
|
|
241
|
+
return comparator
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Display utilities for async benchmarking."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_time(seconds: float) -> str:
|
|
10
|
+
"""Format time duration in human-readable format."""
|
|
11
|
+
if seconds >= 1.0:
|
|
12
|
+
return f"{seconds:.3f}s"
|
|
13
|
+
elif seconds >= 0.001:
|
|
14
|
+
return f"{seconds * 1000:.3f}ms"
|
|
15
|
+
elif seconds >= 0.000001:
|
|
16
|
+
return f"{seconds * 1000000:.3f}ฮผs"
|
|
17
|
+
else:
|
|
18
|
+
return f"{seconds * 1000000000:.3f}ns"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_percentage(value: float) -> str:
|
|
22
|
+
"""Format percentage with appropriate styling."""
|
|
23
|
+
if value > 0:
|
|
24
|
+
return f"+{value:.1f}%"
|
|
25
|
+
else:
|
|
26
|
+
return f"{value:.1f}%"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def format_speedup(ratio: float) -> str:
|
|
30
|
+
"""Format speedup ratio with descriptive text."""
|
|
31
|
+
if ratio > 1:
|
|
32
|
+
return f"{ratio:.2f}x faster"
|
|
33
|
+
elif ratio < 1:
|
|
34
|
+
return f"{1 / ratio:.2f}x slower"
|
|
35
|
+
else:
|
|
36
|
+
return "same speed"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def display_comparison_table(
|
|
40
|
+
comparisons: list[dict[str, Any]],
|
|
41
|
+
title: str = "๐ Async Benchmark Comparison",
|
|
42
|
+
console: Optional[Console] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Display a comparison table for multiple benchmark results."""
|
|
45
|
+
if console is None:
|
|
46
|
+
console = Console()
|
|
47
|
+
|
|
48
|
+
table = Table(title=title)
|
|
49
|
+
|
|
50
|
+
table.add_column("Metric", style="cyan", no_wrap=True)
|
|
51
|
+
|
|
52
|
+
for comp in comparisons:
|
|
53
|
+
name = comp.get("name", "Benchmark")
|
|
54
|
+
table.add_column(name, style="green", justify="right")
|
|
55
|
+
|
|
56
|
+
if len(comparisons) == 2:
|
|
57
|
+
table.add_column("Difference", style="yellow", justify="right")
|
|
58
|
+
|
|
59
|
+
def get_metric_values(metric: str):
|
|
60
|
+
values = []
|
|
61
|
+
for comp in comparisons:
|
|
62
|
+
result = comp["result"]
|
|
63
|
+
if metric in result:
|
|
64
|
+
values.append(format_time(result[metric]))
|
|
65
|
+
else:
|
|
66
|
+
values.append("N/A")
|
|
67
|
+
return values
|
|
68
|
+
|
|
69
|
+
metrics = ["min", "max", "mean", "median", "stddev"]
|
|
70
|
+
|
|
71
|
+
for metric in metrics:
|
|
72
|
+
row = [metric.title()]
|
|
73
|
+
row.extend(get_metric_values(metric))
|
|
74
|
+
|
|
75
|
+
if len(comparisons) == 2:
|
|
76
|
+
result1 = comparisons[0]["result"]
|
|
77
|
+
result2 = comparisons[1]["result"]
|
|
78
|
+
|
|
79
|
+
if metric in result1 and metric in result2:
|
|
80
|
+
val1, val2 = result1[metric], result2[metric]
|
|
81
|
+
if val1 > 0:
|
|
82
|
+
diff_percent = ((val2 - val1) / val1) * 100
|
|
83
|
+
speedup = val1 / val2
|
|
84
|
+
|
|
85
|
+
if metric == "mean":
|
|
86
|
+
row.append(format_speedup(speedup))
|
|
87
|
+
else:
|
|
88
|
+
row.append(format_percentage(diff_percent))
|
|
89
|
+
else:
|
|
90
|
+
row.append("N/A")
|
|
91
|
+
else:
|
|
92
|
+
row.append("N/A")
|
|
93
|
+
|
|
94
|
+
table.add_row(*row)
|
|
95
|
+
|
|
96
|
+
config_row = ["Rounds"]
|
|
97
|
+
config_row.extend(
|
|
98
|
+
[str(comp["result"].get("rounds", "N/A")) for comp in comparisons]
|
|
99
|
+
)
|
|
100
|
+
if len(comparisons) == 2:
|
|
101
|
+
config_row.append("")
|
|
102
|
+
table.add_row(*config_row)
|
|
103
|
+
|
|
104
|
+
iter_row = ["Iterations"]
|
|
105
|
+
iter_row.extend(
|
|
106
|
+
[str(comp["result"].get("iterations", "N/A")) for comp in comparisons]
|
|
107
|
+
)
|
|
108
|
+
if len(comparisons) == 2:
|
|
109
|
+
iter_row.append("")
|
|
110
|
+
table.add_row(*iter_row)
|
|
111
|
+
|
|
112
|
+
console.print("\n")
|
|
113
|
+
console.print(table)
|
|
114
|
+
|
|
115
|
+
if len(comparisons) == 2:
|
|
116
|
+
result1 = comparisons[0]["result"]
|
|
117
|
+
result2 = comparisons[1]["result"]
|
|
118
|
+
name1 = comparisons[0].get("name", "Version 1")
|
|
119
|
+
name2 = comparisons[1].get("name", "Version 2")
|
|
120
|
+
|
|
121
|
+
if "mean" in result1 and "mean" in result2:
|
|
122
|
+
speedup = result1["mean"] / result2["mean"]
|
|
123
|
+
if speedup > 1.1:
|
|
124
|
+
console.print(f"๐ {name2} is {format_speedup(speedup)} than {name1}")
|
|
125
|
+
elif speedup < 0.9:
|
|
126
|
+
console.print(
|
|
127
|
+
f"๐ {name1} is {format_speedup(1 / speedup)} than {name2}"
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
console.print(f"โ๏ธ {name1} and {name2} have similar performance")
|
|
131
|
+
|
|
132
|
+
console.print("โ
Comparison completed successfully!\n")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def validate_async_function(func) -> bool:
|
|
136
|
+
"""Validate that a function is async."""
|
|
137
|
+
import asyncio
|
|
138
|
+
|
|
139
|
+
return asyncio.iscoroutinefunction(func)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def display_results_rich(benchmark_results, terminalreporter):
|
|
143
|
+
"""Display benchmark results using rich."""
|
|
144
|
+
console = Console(file=terminalreporter._tw)
|
|
145
|
+
|
|
146
|
+
table = Table(title="Async Benchmark Results ๐")
|
|
147
|
+
table.add_column("Test Case ๐งช", style="cyan", no_wrap=True)
|
|
148
|
+
table.add_column("Min (ms)", style="magenta", justify="right")
|
|
149
|
+
table.add_column("Max (ms)", style="magenta", justify="right")
|
|
150
|
+
table.add_column("Mean (ms)", style="green", justify="right")
|
|
151
|
+
table.add_column("StdDev (ms)", style="yellow", justify="right")
|
|
152
|
+
table.add_column("Median (ms)", style="blue", justify="right")
|
|
153
|
+
table.add_column("Rounds ๐", justify="right")
|
|
154
|
+
table.add_column("Iter/Round ๐", justify="right")
|
|
155
|
+
|
|
156
|
+
for result in benchmark_results:
|
|
157
|
+
status_emoji = "โ
"
|
|
158
|
+
table.add_row(
|
|
159
|
+
f"{status_emoji} {result.name}",
|
|
160
|
+
format_time(result.min),
|
|
161
|
+
format_time(result.max),
|
|
162
|
+
format_time(result.mean),
|
|
163
|
+
format_time(result.stddev),
|
|
164
|
+
format_time(result.median),
|
|
165
|
+
str(result.rounds),
|
|
166
|
+
str(result.iterations),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
console.print(table)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Core pytest plugin for async benchmarking."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Callable, Optional
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from .display import format_time
|
|
11
|
+
from .runner import AsyncBenchmarkRunner
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncBenchmarkFixture:
|
|
15
|
+
"""Fixture for benchmarking async functions."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, request: pytest.FixtureRequest):
|
|
18
|
+
self.request = request
|
|
19
|
+
self.console = Console()
|
|
20
|
+
self.results: dict[str, Any] = {}
|
|
21
|
+
|
|
22
|
+
def __call__(
|
|
23
|
+
self,
|
|
24
|
+
func: Callable,
|
|
25
|
+
*args,
|
|
26
|
+
rounds: Optional[int] = None,
|
|
27
|
+
iterations: Optional[int] = None,
|
|
28
|
+
warmup_rounds: int = 1,
|
|
29
|
+
**kwargs,
|
|
30
|
+
):
|
|
31
|
+
"""Benchmark an async function."""
|
|
32
|
+
if not asyncio.iscoroutinefunction(func):
|
|
33
|
+
raise ValueError("Function must be async (coroutine function)")
|
|
34
|
+
|
|
35
|
+
runner = AsyncBenchmarkRunner(
|
|
36
|
+
rounds=rounds, iterations=iterations, warmup_rounds=warmup_rounds
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
loop = asyncio.get_running_loop()
|
|
41
|
+
except RuntimeError:
|
|
42
|
+
loop = asyncio.new_event_loop()
|
|
43
|
+
asyncio.set_event_loop(loop)
|
|
44
|
+
|
|
45
|
+
result = loop.run_until_complete(runner.run(func, *args, **kwargs))
|
|
46
|
+
|
|
47
|
+
test_name = self.request.node.name
|
|
48
|
+
self.results[test_name] = result
|
|
49
|
+
|
|
50
|
+
self._display_results(test_name, result)
|
|
51
|
+
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
def _display_results(self, test_name: str, result: dict[str, Any]):
|
|
55
|
+
"""Display benchmark results using rich."""
|
|
56
|
+
table = Table(title=f"๐ Async Benchmark Results: {test_name}")
|
|
57
|
+
|
|
58
|
+
table.add_column("Metric", style="cyan")
|
|
59
|
+
table.add_column("Value", style="green")
|
|
60
|
+
|
|
61
|
+
table.add_row("Min", format_time(result["min"]))
|
|
62
|
+
table.add_row("Max", format_time(result["max"]))
|
|
63
|
+
table.add_row("Mean", format_time(result["mean"]))
|
|
64
|
+
table.add_row("Median", format_time(result["median"]))
|
|
65
|
+
table.add_row("Std Dev", format_time(result["stddev"]))
|
|
66
|
+
table.add_row("Rounds", str(result["rounds"]))
|
|
67
|
+
table.add_row("Iterations", str(result["iterations"]))
|
|
68
|
+
|
|
69
|
+
self.console.print("\n")
|
|
70
|
+
self.console.print(table)
|
|
71
|
+
self.console.print("โ
Benchmark completed successfully!\n")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.fixture
|
|
75
|
+
def async_benchmark(request: pytest.FixtureRequest) -> AsyncBenchmarkFixture:
|
|
76
|
+
"""Pytest fixture for async benchmarking."""
|
|
77
|
+
return AsyncBenchmarkFixture(request)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def pytest_configure(config):
|
|
81
|
+
"""Configure pytest plugin."""
|
|
82
|
+
config.addinivalue_line(
|
|
83
|
+
"markers", "async_benchmark: mark test as an async benchmark"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def pytest_collection_modifyitems(config, items):
|
|
88
|
+
"""Modify collected test items."""
|
|
89
|
+
for item in items:
|
|
90
|
+
if "async_benchmark" in item.fixturenames:
|
|
91
|
+
item.add_marker(pytest.mark.async_benchmark)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Core benchmarking logic for async functions."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import statistics
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsyncBenchmarkRunner:
|
|
10
|
+
"""Runner for async function benchmarks."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
rounds: Optional[int] = None,
|
|
15
|
+
iterations: Optional[int] = None,
|
|
16
|
+
warmup_rounds: int = 1,
|
|
17
|
+
):
|
|
18
|
+
self.rounds = rounds or 5
|
|
19
|
+
self.iterations = iterations or 1
|
|
20
|
+
self.warmup_rounds = warmup_rounds
|
|
21
|
+
|
|
22
|
+
async def run(self, func: Callable, *args, **kwargs) -> dict[str, Any]:
|
|
23
|
+
"""Run benchmark for an async function."""
|
|
24
|
+
if not asyncio.iscoroutinefunction(func):
|
|
25
|
+
raise ValueError("Function must be async (coroutine function)")
|
|
26
|
+
|
|
27
|
+
for _ in range(self.warmup_rounds):
|
|
28
|
+
await func(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
times: list[float] = []
|
|
31
|
+
|
|
32
|
+
for _ in range(self.rounds):
|
|
33
|
+
round_times: list[float] = []
|
|
34
|
+
|
|
35
|
+
for _ in range(self.iterations):
|
|
36
|
+
start_time = time.perf_counter()
|
|
37
|
+
await func(*args, **kwargs)
|
|
38
|
+
end_time = time.perf_counter()
|
|
39
|
+
round_times.append(end_time - start_time)
|
|
40
|
+
|
|
41
|
+
times.append(statistics.mean(round_times))
|
|
42
|
+
|
|
43
|
+
return self._calculate_stats(times)
|
|
44
|
+
|
|
45
|
+
def _calculate_stats(self, times: list[float]) -> dict[str, Any]:
|
|
46
|
+
"""Calculate statistics from timing data."""
|
|
47
|
+
return {
|
|
48
|
+
"min": min(times),
|
|
49
|
+
"max": max(times),
|
|
50
|
+
"mean": statistics.mean(times),
|
|
51
|
+
"median": statistics.median(times),
|
|
52
|
+
"stddev": statistics.stdev(times) if len(times) > 1 else 0.0,
|
|
53
|
+
"rounds": self.rounds,
|
|
54
|
+
"iterations": self.iterations,
|
|
55
|
+
"raw_times": times,
|
|
56
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Statistics calculation for async benchmarks."""
|
|
2
|
+
|
|
3
|
+
import statistics
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StatsContainer:
|
|
8
|
+
"""Container for benchmark statistics calculation."""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.measurements: list[float] = []
|
|
12
|
+
|
|
13
|
+
def add_measurement(self, duration: float):
|
|
14
|
+
"""Add a timing measurement."""
|
|
15
|
+
self.measurements.append(duration)
|
|
16
|
+
|
|
17
|
+
def calculate_stats(self) -> dict[str, Any]:
|
|
18
|
+
"""Calculate comprehensive statistics."""
|
|
19
|
+
if not self.measurements:
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
"min": min(self.measurements),
|
|
24
|
+
"max": max(self.measurements),
|
|
25
|
+
"mean": statistics.mean(self.measurements),
|
|
26
|
+
"median": statistics.median(self.measurements),
|
|
27
|
+
"stddev": statistics.stdev(self.measurements)
|
|
28
|
+
if len(self.measurements) > 1
|
|
29
|
+
else 0.0,
|
|
30
|
+
"count": len(self.measurements),
|
|
31
|
+
"raw_times": self.measurements.copy(),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def reset(self):
|
|
35
|
+
"""Reset all measurements."""
|
|
36
|
+
self.measurements.clear()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Utility functions for async benchmarking."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_time(seconds: float) -> str:
|
|
7
|
+
"""Format time duration in human-readable format."""
|
|
8
|
+
if seconds >= 1.0:
|
|
9
|
+
return f"{seconds:.3f}s"
|
|
10
|
+
elif seconds >= 0.001:
|
|
11
|
+
return f"{seconds * 1000:.3f}ms"
|
|
12
|
+
elif seconds >= 0.000001:
|
|
13
|
+
return f"{seconds * 1000000:.3f}ฮผs"
|
|
14
|
+
else:
|
|
15
|
+
return f"{seconds * 1000000000:.3f}ns"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_async_function(func) -> bool:
|
|
19
|
+
"""Validate that a function is async."""
|
|
20
|
+
return asyncio.iscoroutinefunction(func)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_event_loop():
|
|
24
|
+
"""Get or create an event loop safely."""
|
|
25
|
+
try:
|
|
26
|
+
return asyncio.get_running_loop()
|
|
27
|
+
except RuntimeError:
|
|
28
|
+
loop = asyncio.new_event_loop()
|
|
29
|
+
asyncio.set_event_loop(loop)
|
|
30
|
+
return loop
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-async-benchmark
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: pytest-async-benchmark: Modern pytest benchmarking for async code. ๐
|
|
5
|
+
Author-email: Mihai Farcas <contact@mihai.ltd>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Mihai Farcas <contact@mihai.ltd>
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Requires-Dist: pytest>=8.3.5
|
|
30
|
+
Requires-Dist: rich>=14.0.0
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# pytest-async-benchmark ๐
|
|
34
|
+
|
|
35
|
+
Modern pytest benchmarking for async code with beautiful terminal output and advanced comparison tools.
|
|
36
|
+
|
|
37
|
+
## โจ Features
|
|
38
|
+
|
|
39
|
+
- ๐ฏ **Async-First**: Designed specifically for benchmarking `async def` functions
|
|
40
|
+
- ๐ **Pytest Integration**: Seamless integration as a pytest plugin
|
|
41
|
+
- ๐จ **Rich Output**: Beautiful terminal reporting!
|
|
42
|
+
- ๐ **Comprehensive Stats**: Min, max, mean, median, std dev, percentiles, and more
|
|
43
|
+
- โ๏ธ **A vs B Comparisons**: Compare different implementations side-by-side
|
|
44
|
+
- ๐ **Multi-Scenario Analysis**: Benchmark multiple scenarios with detailed comparison tables
|
|
45
|
+
- ๐ฏ **Performance Grading**: Automatic performance scoring and analysis
|
|
46
|
+
- โก **Auto Calibration**: Intelligent round and iteration detection
|
|
47
|
+
- ๐ **Quick Compare**: One-line comparison utilities
|
|
48
|
+
- ๐ **Winner Detection**: Automatic identification of best-performing implementation
|
|
49
|
+
- ๐ **Easy to Use**: Simple fixture-based API
|
|
50
|
+
|
|
51
|
+
## ๐ฆ Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv add pytest-async-benchmark
|
|
55
|
+
# or
|
|
56
|
+
pip install pytest-async-benchmark
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## ๐ Quick Start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import asyncio
|
|
63
|
+
import pytest
|
|
64
|
+
|
|
65
|
+
async def slow_async_operation():
|
|
66
|
+
await asyncio.sleep(0.01) # 10ms
|
|
67
|
+
return "result"
|
|
68
|
+
|
|
69
|
+
def test_async_performance(async_benchmark):
|
|
70
|
+
result = async_benchmark(slow_async_operation, rounds=5)
|
|
71
|
+
|
|
72
|
+
# Your assertions here
|
|
73
|
+
assert result['mean'] < 0.02 # Should complete in under 20ms
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## ๐ฏ Core Usage Examples
|
|
77
|
+
|
|
78
|
+
### Basic Benchmarking
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
def test_my_async_function(async_benchmark):
|
|
82
|
+
async def my_function():
|
|
83
|
+
# Your async code here
|
|
84
|
+
await some_async_operation()
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
# Benchmark with default settings (5 rounds, 1 iteration each)
|
|
88
|
+
stats = async_benchmark(my_function)
|
|
89
|
+
|
|
90
|
+
# Access comprehensive timing statistics
|
|
91
|
+
print(f"Mean execution time: {stats['mean']:.3f}s")
|
|
92
|
+
print(f"Standard deviation: {stats['stddev']:.3f}s")
|
|
93
|
+
print(f"95th percentile: {stats['p95']:.3f}s")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Advanced Configuration
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
def test_with_custom_settings(async_benchmark):
|
|
100
|
+
result = async_benchmark(
|
|
101
|
+
my_async_function,
|
|
102
|
+
rounds=10, # Number of rounds to run
|
|
103
|
+
iterations=5, # Iterations per round
|
|
104
|
+
warmup_rounds=2 # Warmup rounds before measurement
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### With Function Arguments
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
def test_with_args(async_benchmark):
|
|
112
|
+
async def process_data(data, multiplier=1):
|
|
113
|
+
# Process the data
|
|
114
|
+
await asyncio.sleep(0.01)
|
|
115
|
+
return len(data) * multiplier
|
|
116
|
+
|
|
117
|
+
result = async_benchmark(
|
|
118
|
+
process_data,
|
|
119
|
+
"test_data", # positional arg
|
|
120
|
+
multiplier=2, # keyword arg
|
|
121
|
+
rounds=3
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## โ๏ธ A vs B Comparison Features
|
|
126
|
+
|
|
127
|
+
### Quick Comparison
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from pytest_async_benchmark import quick_compare
|
|
131
|
+
|
|
132
|
+
async def algorithm_v1():
|
|
133
|
+
await asyncio.sleep(0.002) # 2ms
|
|
134
|
+
return "v1_result"
|
|
135
|
+
|
|
136
|
+
async def algorithm_v2():
|
|
137
|
+
await asyncio.sleep(0.0015) # 1.5ms - optimized
|
|
138
|
+
return "v2_result"
|
|
139
|
+
|
|
140
|
+
# Quick one-liner comparison
|
|
141
|
+
def test_algorithm_comparison():
|
|
142
|
+
winner, results = quick_compare(algorithm_v1, algorithm_v2, rounds=5)
|
|
143
|
+
assert winner == "algorithm_v2" # v2 should be faster
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Detailed A vs B Analysis
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from pytest_async_benchmark import a_vs_b_comparison
|
|
150
|
+
|
|
151
|
+
def test_detailed_comparison():
|
|
152
|
+
# Compare with beautiful terminal output
|
|
153
|
+
a_vs_b_comparison(
|
|
154
|
+
"Original Algorithm", algorithm_v1,
|
|
155
|
+
"Optimized Algorithm", algorithm_v2,
|
|
156
|
+
rounds=8, iterations=20
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Multi-Scenario Benchmarking
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from pytest_async_benchmark import BenchmarkComparator
|
|
164
|
+
|
|
165
|
+
def test_multi_scenario():
|
|
166
|
+
comparator = BenchmarkComparator()
|
|
167
|
+
|
|
168
|
+
# Add multiple scenarios
|
|
169
|
+
comparator.add_scenario(
|
|
170
|
+
"Database Query v1", db_query_v1,
|
|
171
|
+
rounds=5, iterations=10,
|
|
172
|
+
description="Original database implementation"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
comparator.add_scenario(
|
|
176
|
+
"Database Query v2", db_query_v2,
|
|
177
|
+
rounds=5, iterations=10,
|
|
178
|
+
description="Optimized with connection pooling"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Run comparison and get results
|
|
182
|
+
results = comparator.run_comparison()
|
|
183
|
+
|
|
184
|
+
# Beautiful comparison table automatically displayed
|
|
185
|
+
# Access programmatic results
|
|
186
|
+
fastest = results.get_fastest_scenario()
|
|
187
|
+
assert fastest.name == "Database Query v2"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## ๐ Comprehensive Statistics
|
|
191
|
+
|
|
192
|
+
Each benchmark returns detailed statistics:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
{
|
|
196
|
+
'min': 0.001234, # Minimum execution time
|
|
197
|
+
'max': 0.005678, # Maximum execution time
|
|
198
|
+
'mean': 0.002456, # Mean execution time
|
|
199
|
+
'median': 0.002123, # Median execution time
|
|
200
|
+
'stddev': 0.000234, # Standard deviation
|
|
201
|
+
'p50': 0.002123, # 50th percentile (median)
|
|
202
|
+
'p90': 0.003456, # 90th percentile
|
|
203
|
+
'p95': 0.004123, # 95th percentile
|
|
204
|
+
'p99': 0.004789, # 99th percentile
|
|
205
|
+
'rounds': 5, # Number of rounds executed
|
|
206
|
+
'iterations': 1, # Number of iterations per round
|
|
207
|
+
'raw_times': [...], # List of raw timing measurements
|
|
208
|
+
'grade': 'A', # Performance grade (A-F)
|
|
209
|
+
'grade_score': 87.5 # Numeric grade score (0-100)
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## ๐จ Beautiful Terminal Output
|
|
214
|
+
|
|
215
|
+
### Basic Benchmark Output
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
๐ Async Benchmark Results: test_my_function
|
|
219
|
+
โโโโโโโโโโโโโโโณโโโโโโโโโโโโโ
|
|
220
|
+
โ Metric โ Value โ
|
|
221
|
+
โกโโโโโโโโโโโโโโโโโโโโโโโโโโโฉ
|
|
222
|
+
โ Min โ 10.234ms โ
|
|
223
|
+
โ Max โ 15.678ms โ
|
|
224
|
+
โ Mean โ 12.456ms โ
|
|
225
|
+
โ Median โ 12.123ms โ
|
|
226
|
+
โ Std Dev โ 1.234ms โ
|
|
227
|
+
โ 95th %ile โ 14.567ms โ
|
|
228
|
+
โ 99th %ile โ 15.234ms โ
|
|
229
|
+
โ Grade โ A (87.5) โ
|
|
230
|
+
โ Rounds โ 5 โ
|
|
231
|
+
โ Iterations โ 1 โ
|
|
232
|
+
โโโโโโโโโโโโโโโดโโโโโโโโโโโโโ
|
|
233
|
+
โ
Benchmark completed successfully!
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### A vs B Comparison Output
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
โ๏ธ A vs B Comparison Results
|
|
240
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโณโโโโโโโโโโโโโโณโโโโโโโโโโโโโโณโโโโโโโโโโโโ
|
|
241
|
+
โ Scenario โ Algorithm A โ Algorithm B โ Winner โ
|
|
242
|
+
โกโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฉ
|
|
243
|
+
โ Mean Time โ 2.456ms โ 1.789ms โ B ๐ โ
|
|
244
|
+
โ Median Time โ 2.234ms โ 1.678ms โ B ๐ โ
|
|
245
|
+
โ 95th Percentile โ 3.456ms โ 2.345ms โ B ๐ โ
|
|
246
|
+
โ Standard Deviation โ 0.567ms โ 0.234ms โ B ๐ โ
|
|
247
|
+
โ Performance Grade โ B (76.2) โ A (89.1) โ B ๐ โ
|
|
248
|
+
โ Improvement โ - โ 27.2% โ - โ
|
|
249
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโดโโโโโโโโโโโโโโดโโโโโโโโโโโโ
|
|
250
|
+
๐ Winner: Algorithm B (27.2% faster)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## ๐๏ธ Project Structure
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
pytest-async-benchmark/
|
|
257
|
+
โโโ src/
|
|
258
|
+
โ โโโ pytest_async_benchmark/
|
|
259
|
+
โ โโโ __init__.py # Main exports and API
|
|
260
|
+
โ โโโ plugin.py # Pytest plugin and fixtures
|
|
261
|
+
โ โโโ runner.py # Core benchmarking engine
|
|
262
|
+
โ โโโ display.py # Rich terminal output formatting
|
|
263
|
+
โ โโโ stats.py # Statistical calculations
|
|
264
|
+
โ โโโ utils.py # Utility functions
|
|
265
|
+
โ โโโ analytics.py # Performance analysis tools
|
|
266
|
+
โ โโโ comparison.py # A vs B comparison functionality
|
|
267
|
+
โโโ examples/
|
|
268
|
+
โ โโโ pytest_examples.py # Comprehensive pytest usage examples
|
|
269
|
+
โ โโโ quart_api_comparison.py # Real-world API endpoint comparison
|
|
270
|
+
โ โโโ comparison_examples.py # Advanced comparison features demo
|
|
271
|
+
โโโ tests/
|
|
272
|
+
โ โโโ test_async_bench.py # Core functionality tests
|
|
273
|
+
โ โโโ test_comparison.py # Comparison feature tests
|
|
274
|
+
โ โโโ test_demo.py # Demo test cases
|
|
275
|
+
โ โโโ conftest.py # Test configuration
|
|
276
|
+
โโโ pyproject.toml # Package configuration
|
|
277
|
+
โโโ README.md # This file
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## ๐ Example Files Guide
|
|
281
|
+
|
|
282
|
+
### ๐ง [`examples/pytest_examples.py`](examples/pytest_examples.py)
|
|
283
|
+
Comprehensive pytest usage examples including:
|
|
284
|
+
- Basic benchmarking with the `async_benchmark` fixture
|
|
285
|
+
- Advanced configuration options
|
|
286
|
+
- Performance assertions and testing patterns
|
|
287
|
+
- Using markers for benchmark organization
|
|
288
|
+
|
|
289
|
+
### ๐ [`examples/quart_api_comparison.py`](examples/quart_api_comparison.py)
|
|
290
|
+
Real-world API endpoint comparison demo featuring:
|
|
291
|
+
- Quart web framework setup
|
|
292
|
+
- API v1 vs v2 endpoint benchmarking
|
|
293
|
+
- Live server testing with actual HTTP requests
|
|
294
|
+
- Performance regression detection
|
|
295
|
+
|
|
296
|
+
### โ๏ธ [`examples/comparison_examples.py`](examples/comparison_examples.py)
|
|
297
|
+
Advanced comparison features showcase:
|
|
298
|
+
- Multi-scenario benchmark comparisons
|
|
299
|
+
- A vs B testing with detailed analysis
|
|
300
|
+
- Performance grading and scoring
|
|
301
|
+
- Statistical comparison utilities
|
|
302
|
+
|
|
303
|
+
## ๐ Real-World Examples
|
|
304
|
+
|
|
305
|
+
### FastAPI Endpoint Benchmarking
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from fastapi import FastAPI
|
|
309
|
+
from fastapi.testclient import TestClient
|
|
310
|
+
import pytest
|
|
311
|
+
|
|
312
|
+
app = FastAPI()
|
|
313
|
+
|
|
314
|
+
@app.get("/api/data")
|
|
315
|
+
async def get_data():
|
|
316
|
+
# Simulate database query
|
|
317
|
+
await asyncio.sleep(0.005)
|
|
318
|
+
return {"data": "example"}
|
|
319
|
+
|
|
320
|
+
def test_fastapi_endpoint_performance(async_benchmark):
|
|
321
|
+
async def make_request():
|
|
322
|
+
with TestClient(app) as client:
|
|
323
|
+
response = client.get("/api/data")
|
|
324
|
+
return response.json()
|
|
325
|
+
|
|
326
|
+
result = async_benchmark(make_request, rounds=10)
|
|
327
|
+
assert result['mean'] < 0.1 # Should respond within 100ms
|
|
328
|
+
assert result['grade'] in ['A', 'B'] # Should have good performance grade
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Quart API Endpoint Comparison
|
|
332
|
+
|
|
333
|
+
See the complete example in [`examples/quart_api_comparison.py`](examples/quart_api_comparison.py):
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
from pytest_async_benchmark import a_vs_b_comparison
|
|
337
|
+
import asyncio
|
|
338
|
+
import aiohttp
|
|
339
|
+
|
|
340
|
+
async def test_api_v1():
|
|
341
|
+
async with aiohttp.ClientSession() as session:
|
|
342
|
+
async with session.get('http://localhost:5000/api/v1/data') as resp:
|
|
343
|
+
return await resp.json()
|
|
344
|
+
|
|
345
|
+
async def test_api_v2():
|
|
346
|
+
async with aiohttp.ClientSession() as session:
|
|
347
|
+
async with session.get('http://localhost:5000/api/v2/data') as resp:
|
|
348
|
+
return await resp.json()
|
|
349
|
+
|
|
350
|
+
# Compare API versions
|
|
351
|
+
a_vs_b_comparison(
|
|
352
|
+
"API v1", test_api_v1,
|
|
353
|
+
"API v2 (Optimized)", test_api_v2,
|
|
354
|
+
rounds=10, iterations=5
|
|
355
|
+
)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Database Query Benchmarking
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
async def test_database_query_performance(async_benchmark):
|
|
362
|
+
async def fetch_user_data(user_id):
|
|
363
|
+
async with database.connection() as conn:
|
|
364
|
+
return await conn.fetch_one(
|
|
365
|
+
"SELECT * FROM users WHERE id = ?", user_id
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
result = async_benchmark(fetch_user_data, 123, rounds=5)
|
|
369
|
+
assert result['mean'] < 0.05 # Should complete within 50ms
|
|
370
|
+
assert result['p95'] < 0.1 # 95% of queries under 100ms
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## ๐ฏ Using Markers
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
@pytest.mark.async_benchmark
|
|
377
|
+
def test_performance(async_benchmark):
|
|
378
|
+
# Your benchmark test
|
|
379
|
+
result = async_benchmark(my_async_function)
|
|
380
|
+
assert result is not None
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## ๐ API Reference
|
|
384
|
+
|
|
385
|
+
### `async_benchmark(func, *args, rounds=None, iterations=None, warmup_rounds=1, **kwargs)`
|
|
386
|
+
|
|
387
|
+
**Parameters:**
|
|
388
|
+
- `func`: The async function to benchmark
|
|
389
|
+
- `*args`: Positional arguments to pass to the function
|
|
390
|
+
- `rounds`: Number of measurement rounds (default: 5)
|
|
391
|
+
- `iterations`: Number of iterations per round (default: 1)
|
|
392
|
+
- `warmup_rounds`: Number of warmup rounds before measurement (default: 1)
|
|
393
|
+
- `**kwargs`: Keyword arguments to pass to the function
|
|
394
|
+
|
|
395
|
+
**Returns:**
|
|
396
|
+
A dictionary with comprehensive statistics including min, max, mean, median, stddev, percentiles, performance grade, and raw measurements.
|
|
397
|
+
|
|
398
|
+
### Comparison Functions
|
|
399
|
+
|
|
400
|
+
- `quick_compare(func_a, func_b, **kwargs)`: Quick comparison returning winner and results
|
|
401
|
+
- `a_vs_b_comparison(name_a, func_a, name_b, func_b, **kwargs)`: Detailed comparison with terminal output
|
|
402
|
+
- `BenchmarkComparator`: Class for multi-scenario benchmarking and analysis
|
|
403
|
+
|
|
404
|
+
## ๐ Requirements
|
|
405
|
+
|
|
406
|
+
- Python โฅ 3.9
|
|
407
|
+
- pytest โฅ 8.3.5
|
|
408
|
+
|
|
409
|
+
Note: Rich (for beautiful terminal output) is automatically installed as a dependency.
|
|
410
|
+
|
|
411
|
+
## ๐ Development
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# Clone the repository
|
|
415
|
+
git clone https://github.com/yourusername/pytest-async-benchmark.git
|
|
416
|
+
cd pytest-async-benchmark
|
|
417
|
+
|
|
418
|
+
# Install dependencies
|
|
419
|
+
uv sync
|
|
420
|
+
|
|
421
|
+
# Run tests
|
|
422
|
+
uv run pytest tests/ -v
|
|
423
|
+
|
|
424
|
+
# Run examples
|
|
425
|
+
uv run pytest examples/pytest_examples.py -v
|
|
426
|
+
|
|
427
|
+
# Test real-world Quart API comparison
|
|
428
|
+
uv run python examples/quart_api_comparison.py
|
|
429
|
+
|
|
430
|
+
# See advanced comparison features
|
|
431
|
+
uv run python examples/comparison_examples.py
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### ๐ ๏ธ Code Quality and Formatting
|
|
435
|
+
|
|
436
|
+
This project uses [Ruff](https://docs.astral.sh/ruff/) for both linting and formatting:
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
# Check code for linting issues
|
|
440
|
+
uv run ruff check .
|
|
441
|
+
|
|
442
|
+
# Fix auto-fixable linting issues
|
|
443
|
+
uv run ruff check . --fix
|
|
444
|
+
|
|
445
|
+
# Check code formatting
|
|
446
|
+
uv run ruff format --check .
|
|
447
|
+
|
|
448
|
+
# Format code automatically
|
|
449
|
+
uv run ruff format .
|
|
450
|
+
|
|
451
|
+
# Run both linting and formatting in one go
|
|
452
|
+
uv run ruff check . --fix && uv run ruff format .
|
|
453
|
+
|
|
454
|
+
# Run all quality checks at once (linting, formatting, and tests)
|
|
455
|
+
uv run python scripts/quality-check.py
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### ๐ Release Readiness Check
|
|
459
|
+
|
|
460
|
+
Before creating a release, verify everything is ready:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
# Run comprehensive release check
|
|
464
|
+
uv run python scripts/release-check.py
|
|
465
|
+
|
|
466
|
+
# This checks:
|
|
467
|
+
# โ
Git repository status
|
|
468
|
+
# โ
Version consistency
|
|
469
|
+
# โ
Code formatting and linting
|
|
470
|
+
# โ
Test suite passes
|
|
471
|
+
# โ
Package builds successfully
|
|
472
|
+
# โ
All required files exist
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### ๐ Quick Quality Check
|
|
476
|
+
|
|
477
|
+
Run all quality checks at once:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
# Run linting, formatting, tests, and release checks
|
|
481
|
+
python scripts/quality-check.py
|
|
482
|
+
|
|
483
|
+
# This will:
|
|
484
|
+
# ๐ง Fix linting issues automatically
|
|
485
|
+
# ๐จ Format code with Ruff
|
|
486
|
+
# ๐งช Run the full test suite
|
|
487
|
+
# ๐ Check release readiness
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## ๐ Automated Releases
|
|
491
|
+
|
|
492
|
+
This project uses GitHub Actions for automated testing and publishing to PyPI:
|
|
493
|
+
|
|
494
|
+
- **Continuous Integration**: Tests run on every push for Python 3.9-3.13
|
|
495
|
+
- **Test Publishing**: Automatic uploads to TestPyPI for testing releases
|
|
496
|
+
- **Production Releases**: Secure publishing to PyPI using trusted publishing
|
|
497
|
+
- **Release Validation**: Comprehensive checks ensure package quality
|
|
498
|
+
|
|
499
|
+
### Creating a Release
|
|
500
|
+
|
|
501
|
+
1. Update version in `pyproject.toml` and `src/pytest_async_benchmark/__init__.py`
|
|
502
|
+
2. Run `uv run python scripts/release-check.py` to verify readiness
|
|
503
|
+
3. Create a git tag: `git tag v1.0.0 && git push origin v1.0.0`
|
|
504
|
+
4. Create a GitHub release to trigger automated PyPI publishing
|
|
505
|
+
|
|
506
|
+
See [RELEASE_GUIDE.md](RELEASE_GUIDE.md) for detailed release instructions.
|
|
507
|
+
|
|
508
|
+
## ๐ค Contributing
|
|
509
|
+
|
|
510
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
511
|
+
|
|
512
|
+
## ๐ License
|
|
513
|
+
|
|
514
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
*Built with โค๏ธ for the async Python community*
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pytest_async_benchmark/__init__.py,sha256=i_vjO6kOxhDSBXhBuVo9mgvItucMaTAne_jaVorarNg,825
|
|
2
|
+
pytest_async_benchmark/analytics.py,sha256=7lR-GF0rqS8X1pVtFucmyO-GXvzYXoX0I9bOh6uzUhQ,1710
|
|
3
|
+
pytest_async_benchmark/comparison.py,sha256=-TXaUSpFNKzroPRRILw_7aAOte5uARSiaDeifShUgUQ,8025
|
|
4
|
+
pytest_async_benchmark/display.py,sha256=WRoiSZK03XNAtfeMzBniJeHra2YzPB4_DbWsMp30BVI,5357
|
|
5
|
+
pytest_async_benchmark/plugin.py,sha256=P8TcblejlVejdDIowy4Zeame-C4pa23IEBS_rFqqIgs,2857
|
|
6
|
+
pytest_async_benchmark/runner.py,sha256=HFRZ8kc9fDNGy9IorZawAKN9KnQjQv_YqdvV-qbAt_Y,1775
|
|
7
|
+
pytest_async_benchmark/stats.py,sha256=I0hpi-TqcstOdXxLW03p1ZA7TX1qz9JnJNu_vSxWqWA,1074
|
|
8
|
+
pytest_async_benchmark/utils.py,sha256=-oiEiuOXyiU614wkBgS67em1KVQCnaJW4z_x3lNnSKs,797
|
|
9
|
+
pytest_async_benchmark-0.1.1.dist-info/METADATA,sha256=XgpF-xFS1Ndw5_juXkzRAE5s6K3YEpzLFf26q0yTV8E,16976
|
|
10
|
+
pytest_async_benchmark-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
pytest_async_benchmark-0.1.1.dist-info/entry_points.txt,sha256=5PoCegPmQcqj3Y5iGuiW9pGvYxnxirJ-P93NekT7aYg,55
|
|
12
|
+
pytest_async_benchmark-0.1.1.dist-info/licenses/LICENSE,sha256=ZzNL_t8ZeKS3i3duN9drmwyTr5DiTTCAtpgTB8JXH1k,1088
|
|
13
|
+
pytest_async_benchmark-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mihai Farcas <contact@mihai.ltd>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|