Skip to content

Commit 81a49c9

Browse files
Add benchmarking script
1 parent 393035a commit 81a49c9

File tree

4 files changed

+153
-0
lines changed

4 files changed

+153
-0
lines changed

requirements.txt

+5
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@ pytest==8.2.0
2121
pytest-httpbin==2.0.0
2222
pytest-trio==0.8.0
2323
werkzeug<2.1 # See: https://github.com/psf/httpbin/issues/35
24+
25+
# Benchmarking and profiling
26+
aiohttp==3.9.5
27+
matplotlib==3.9.0
28+
pyinstrument==4.6.2

scripts/benchmark

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh -e
2+
3+
export PREFIX=""
4+
if [ -d 'venv' ] ; then
5+
export PREFIX="venv/bin/"
6+
fi
7+
8+
set -x
9+
10+
${PREFIX}python tests/benchmark/server.py &
11+
SERVER_PID=$!
12+
${PREFIX}python tests/benchmark/client.py
13+
kill $SERVER_PID

tests/benchmark/client.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import asyncio
2+
import os
3+
import time
4+
from contextlib import contextmanager
5+
from typing import Any, Coroutine, Iterator
6+
7+
import aiohttp
8+
import matplotlib.pyplot as plt
9+
import pyinstrument
10+
from matplotlib.axes import Axes
11+
12+
import httpcore
13+
14+
PORT = 1234
15+
URL = f"http://localhost:{PORT}/req"
16+
REPEATS = 10
17+
REQUESTS = 500
18+
CONCURRENCY = 20
19+
POOL_LIMIT = 100
20+
PROFILE = False
21+
os.environ["HTTPCORE_PREFER_ANYIO"] = "0"
22+
23+
24+
def duration(start: float) -> int:
25+
return int((time.monotonic() - start) * 1000)
26+
27+
28+
@contextmanager
29+
def profile():
30+
if not PROFILE:
31+
yield
32+
return
33+
with pyinstrument.Profiler() as profiler:
34+
yield
35+
profiler.open_in_browser()
36+
37+
38+
async def gather_limited_concurrency(
39+
coros: Iterator[Coroutine[Any, Any, Any]], concurrency: int = CONCURRENCY
40+
) -> None:
41+
sem = asyncio.Semaphore(concurrency)
42+
43+
async def coro_with_sem(coro: Coroutine[Any, Any, Any]) -> None:
44+
async with sem:
45+
await coro
46+
47+
await asyncio.gather(*(coro_with_sem(c) for c in coros))
48+
49+
50+
async def run_requests(axis: Axes) -> None:
51+
async def httpcore_get(
52+
pool: httpcore.AsyncConnectionPool, timings: list[int]
53+
) -> None:
54+
start = time.monotonic()
55+
res = await pool.request("GET", URL)
56+
assert len(await res.aread()) == 2000
57+
assert res.status == 200, f"status_code={res.status}"
58+
timings.append(duration(start))
59+
60+
async def aiohttp_get(session: aiohttp.ClientSession, timings: list[int]) -> None:
61+
start = time.monotonic()
62+
async with session.request("GET", URL) as res:
63+
assert len(await res.read()) == 2000
64+
assert res.status == 200, f"status={res.status}"
65+
timings.append(duration(start))
66+
67+
async with httpcore.AsyncConnectionPool(max_connections=POOL_LIMIT) as pool:
68+
# warmup
69+
await gather_limited_concurrency(
70+
(httpcore_get(pool, []) for _ in range(REQUESTS)), CONCURRENCY * 2
71+
)
72+
73+
timings: list[int] = []
74+
start = time.monotonic()
75+
with profile():
76+
for _ in range(REPEATS):
77+
await gather_limited_concurrency(
78+
(httpcore_get(pool, timings) for _ in range(REQUESTS))
79+
)
80+
axis.plot(
81+
[*range(len(timings))], timings, label=f"httpcore (tot={duration(start)}ms)"
82+
)
83+
84+
connector = aiohttp.TCPConnector(limit=POOL_LIMIT)
85+
async with aiohttp.ClientSession(connector=connector) as session:
86+
# warmup
87+
await gather_limited_concurrency(
88+
(aiohttp_get(session, []) for _ in range(REQUESTS)), CONCURRENCY * 2
89+
)
90+
91+
timings = []
92+
start = time.monotonic()
93+
for _ in range(REPEATS):
94+
await gather_limited_concurrency(
95+
(aiohttp_get(session, timings) for _ in range(REQUESTS))
96+
)
97+
axis.plot(
98+
[*range(len(timings))], timings, label=f"aiohttp (tot={duration(start)}ms)"
99+
)
100+
101+
102+
def main() -> None:
103+
fig, ax = plt.subplots()
104+
asyncio.run(run_requests(ax))
105+
plt.legend(loc="upper left")
106+
ax.set_xlabel("# request")
107+
ax.set_ylabel("[ms]")
108+
plt.show()
109+
print("DONE", flush=True)
110+
111+
112+
if __name__ == "__main__":
113+
main()

tests/benchmark/server.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import asyncio
2+
3+
from aiohttp import web
4+
5+
PORT = 1234
6+
RESP = "a" * 2000
7+
SLEEP = 0.01
8+
9+
10+
async def handle(_request):
11+
await asyncio.sleep(SLEEP)
12+
return web.Response(text=RESP)
13+
14+
15+
def main() -> None:
16+
app = web.Application()
17+
app.add_routes([web.get("/req", handle)])
18+
web.run_app(app, host="localhost", port=PORT)
19+
20+
21+
if __name__ == "__main__":
22+
main()

0 commit comments

Comments
 (0)