Skip to content

Commit 3454d0a

Browse files
committed
Add benchmarks
1 parent ac9f249 commit 3454d0a

6 files changed

Lines changed: 961 additions & 1 deletion

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@
1414
<a href="https://feverup.github.io/aiocop/"><img src="https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg" alt="Documentation"></a>
1515
</p>
1616

17+
## Performance
18+
19+
aiocop adds approximately **13 microseconds** of overhead per async task:
20+
21+
| Scenario | Overhead | Impact on 50ms Request |
22+
|----------|----------|------------------------|
23+
| Pure async (no blocking I/O) | ~1 us | 0.002% |
24+
| Light blocking (os.stat) | ~14 us | 0.03% |
25+
| Moderate blocking (file read) | ~12 us | 0.02% |
26+
| Realistic HTTP handler | ~22 us | 0.04% |
27+
28+
For typical web applications, this means **less than 0.05% overhead**.
29+
30+
Run the benchmark yourself: `python benchmarks/run_benchmark.py`
31+
1732
## Features
1833

1934
* **Production-Safe & Low Overhead**: Leverages Python's `sys.audit` hooks for minimal runtime overhead, making it safe for production use

benchmarks/run_benchmark.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#!/usr/bin/env python3
2+
"""
3+
aiocop Benchmark Script
4+
5+
Measures the overhead of aiocop monitoring on async workloads.
6+
7+
Usage:
8+
python benchmarks/run_benchmark.py
9+
10+
Or with uv:
11+
uv run python benchmarks/run_benchmark.py
12+
"""
13+
14+
import asyncio
15+
import gc
16+
import os
17+
import statistics
18+
import sys
19+
import time
20+
from dataclasses import dataclass
21+
from pathlib import Path
22+
from typing import Callable
23+
24+
# Add parent directory to path for importing aiocop
25+
sys.path.insert(0, str(Path(__file__).parent.parent))
26+
27+
import aiocop
28+
29+
30+
@dataclass
31+
class BenchmarkResult:
32+
"""Result of a single benchmark run."""
33+
34+
name: str
35+
num_tasks: int
36+
without_aiocop_ms: float
37+
with_aiocop_ms: float
38+
overhead_per_task_us: float # microseconds per task
39+
40+
41+
def format_results(results: list[BenchmarkResult]) -> str:
42+
"""Format benchmark results."""
43+
lines = []
44+
lines.append("")
45+
lines.append("=" * 70)
46+
lines.append("aiocop Benchmark Results")
47+
lines.append("=" * 70)
48+
lines.append("")
49+
lines.append("Per-Task Overhead (lower is better):")
50+
lines.append("")
51+
lines.append(f" {'Scenario':<40} {'Overhead':<15} {'Impact on 50ms request'}")
52+
lines.append(f" {'-' * 40} {'-' * 15} {'-' * 22}")
53+
54+
for r in results:
55+
# Calculate impact on a 50ms request
56+
impact_percent = (r.overhead_per_task_us / 50000) * 100 # 50ms = 50000us
57+
lines.append(
58+
f" {r.name:<40} {r.overhead_per_task_us:>8.1f} us {impact_percent:.3f}%"
59+
)
60+
61+
lines.append("")
62+
63+
# Calculate average
64+
avg_overhead = statistics.mean(r.overhead_per_task_us for r in results)
65+
avg_impact = (avg_overhead / 50000) * 100
66+
67+
lines.append(f" Average: {avg_overhead:.1f} us per task ({avg_impact:.3f}% on 50ms request)")
68+
lines.append("")
69+
lines.append("-" * 70)
70+
lines.append("")
71+
lines.append("What this means:")
72+
lines.append(f" - Each async task adds ~{avg_overhead:.0f} microseconds of overhead")
73+
lines.append(f" - A typical 50ms HTTP request sees {avg_impact:.2f}% overhead")
74+
lines.append(f" - A typical 100ms database query sees {avg_impact/2:.2f}% overhead")
75+
lines.append("")
76+
77+
return "\n".join(lines)
78+
79+
80+
async def run_scenario(
81+
name: str,
82+
task_fn: Callable[[], asyncio.Future],
83+
num_tasks: int,
84+
iterations: int = 5,
85+
) -> BenchmarkResult:
86+
"""Run a benchmark scenario with and without aiocop."""
87+
88+
async def run_tasks():
89+
tasks = [asyncio.create_task(task_fn()) for _ in range(num_tasks)]
90+
await asyncio.gather(*tasks)
91+
92+
# Warmup
93+
await run_tasks()
94+
gc.collect()
95+
96+
# Benchmark WITHOUT aiocop
97+
aiocop.deactivate()
98+
times_without = []
99+
for _ in range(iterations):
100+
gc.collect()
101+
start = time.perf_counter()
102+
await run_tasks()
103+
elapsed = (time.perf_counter() - start) * 1000
104+
times_without.append(elapsed)
105+
106+
without_ms = statistics.median(times_without)
107+
108+
# Benchmark WITH aiocop
109+
aiocop.activate()
110+
times_with = []
111+
for _ in range(iterations):
112+
gc.collect()
113+
start = time.perf_counter()
114+
await run_tasks()
115+
elapsed = (time.perf_counter() - start) * 1000
116+
times_with.append(elapsed)
117+
118+
with_ms = statistics.median(times_with)
119+
120+
overhead_ms = with_ms - without_ms
121+
overhead_per_task_us = (overhead_ms * 1000) / num_tasks # Convert to microseconds
122+
123+
return BenchmarkResult(
124+
name=name,
125+
num_tasks=num_tasks,
126+
without_aiocop_ms=without_ms,
127+
with_aiocop_ms=with_ms,
128+
overhead_per_task_us=overhead_per_task_us,
129+
)
130+
131+
132+
async def fast_async_task():
133+
"""A fast async task with no blocking I/O."""
134+
await asyncio.sleep(0)
135+
136+
137+
async def task_with_stat():
138+
"""Task that performs os.stat (light blocking)."""
139+
os.stat(".")
140+
await asyncio.sleep(0)
141+
142+
143+
async def task_with_getcwd():
144+
"""Task with trivial blocking (os.getcwd)."""
145+
os.getcwd()
146+
await asyncio.sleep(0)
147+
148+
149+
async def task_with_file_read():
150+
"""Task that reads an existing file."""
151+
try:
152+
with open(__file__) as f:
153+
f.read(100)
154+
except Exception:
155+
pass
156+
await asyncio.sleep(0)
157+
158+
159+
async def realistic_http_handler():
160+
"""
161+
Simulates a realistic async HTTP handler.
162+
Most time is spent in async I/O, with occasional light blocking.
163+
"""
164+
await asyncio.sleep(0.001) # 1ms async work
165+
os.path.exists(".")
166+
os.getcwd()
167+
await asyncio.sleep(0.001) # 1ms async work
168+
169+
170+
def noop_callback(event: aiocop.SlowTaskEvent) -> None:
171+
"""No-op callback for benchmarking."""
172+
pass
173+
174+
175+
async def main():
176+
print("")
177+
print("aiocop Performance Benchmark")
178+
print("=" * 50)
179+
print("")
180+
print("Setting up aiocop...")
181+
182+
# Setup aiocop with minimal trace depth for better performance
183+
aiocop.patch_audit_functions()
184+
aiocop.start_blocking_io_detection(trace_depth=5)
185+
aiocop.detect_slow_tasks(threshold_ms=1000, on_slow_task=noop_callback)
186+
187+
print("Running benchmarks...\n")
188+
189+
results = []
190+
191+
# Scenario 1: Pure async (baseline - no blocking I/O to detect)
192+
result = await run_scenario(
193+
name="Pure async (no blocking)",
194+
task_fn=fast_async_task,
195+
num_tasks=10_000,
196+
)
197+
results.append(result)
198+
print(f" [done] {result.name}")
199+
200+
# Scenario 2: Trivial blocking (os.getcwd - WEIGHT_TRIVIAL)
201+
result = await run_scenario(
202+
name="Trivial blocking (getcwd)",
203+
task_fn=task_with_getcwd,
204+
num_tasks=5_000,
205+
)
206+
results.append(result)
207+
print(f" [done] {result.name}")
208+
209+
# Scenario 3: Light blocking (os.stat - WEIGHT_LIGHT)
210+
result = await run_scenario(
211+
name="Light blocking (stat)",
212+
task_fn=task_with_stat,
213+
num_tasks=5_000,
214+
)
215+
results.append(result)
216+
print(f" [done] {result.name}")
217+
218+
# Scenario 4: Moderate blocking (file read - WEIGHT_MODERATE)
219+
result = await run_scenario(
220+
name="Moderate blocking (file read)",
221+
task_fn=task_with_file_read,
222+
num_tasks=2_000,
223+
)
224+
results.append(result)
225+
print(f" [done] {result.name}")
226+
227+
# Scenario 5: Realistic HTTP handler simulation
228+
result = await run_scenario(
229+
name="Realistic HTTP handler",
230+
task_fn=realistic_http_handler,
231+
num_tasks=500,
232+
)
233+
results.append(result)
234+
print(f" [done] {result.name}")
235+
236+
# Print results
237+
print(format_results(results))
238+
239+
# Print system info
240+
print("System Info:")
241+
print(f" Python: {sys.version.split()[0]}")
242+
print(f" Platform: {sys.platform}")
243+
print(f" aiocop: {aiocop.__version__}")
244+
print("")
245+
246+
247+
if __name__ == "__main__":
248+
asyncio.run(main())

0 commit comments

Comments
 (0)