Skip to content

Commit d2d45c8

Browse files
add sync benchmark
1 parent 3daef50 commit d2d45c8

File tree

3 files changed

+95
-13
lines changed

3 files changed

+95
-13
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ werkzeug<2.1 # See: https://github.com/psf/httpbin/issues/35
2525
# Benchmarking and profiling
2626
uvicorn==0.30.1
2727
aiohttp==3.9.5
28+
requests==2.32.3
2829
matplotlib==3.7.5
2930
pyinstrument==4.6.2

scripts/benchmark

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#!/bin/sh -e
22

3+
# Usage: scripts/benchmark async|sync
4+
35
export PREFIX=""
46
if [ -d 'venv' ] ; then
57
export PREFIX="venv/bin/"
@@ -10,6 +12,6 @@ set -x
1012
${PREFIX}python tests/benchmark/server.py &
1113
SERVER_PID=$!
1214
EXIT_CODE=0
13-
${PREFIX}python tests/benchmark/client.py || EXIT_CODE=$?
15+
${PREFIX}python tests/benchmark/client.py "$@" || EXIT_CODE=$?
1416
kill $SERVER_PID
1517
exit $EXIT_CODE

tests/benchmark/client.py

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import asyncio
22
import os
3+
import sys
34
import time
5+
from concurrent.futures import ThreadPoolExecutor
46
from contextlib import contextmanager
5-
from typing import Any, Coroutine, Iterator, List
7+
from typing import Any, Callable, Coroutine, Iterator, List
68

79
import aiohttp
810
import matplotlib.pyplot as plt # type: ignore[import-untyped]
911
import pyinstrument
12+
import requests # type: ignore[import-untyped]
1013
from matplotlib.axes import Axes # type: ignore[import-untyped]
14+
from requests.adapters import HTTPAdapter # type: ignore[import-untyped]
1115

1216
import httpcore
1317

@@ -35,19 +39,18 @@ def profile():
3539
profiler.open_in_browser()
3640

3741

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
42+
async def run_async_requests(axis: Axes) -> None:
43+
async def gather_limited_concurrency(
44+
coros: Iterator[Coroutine[Any, Any, Any]], concurrency: int = CONCURRENCY
45+
) -> None:
46+
sem = asyncio.Semaphore(concurrency)
4647

47-
await asyncio.gather(*(coro_with_sem(c) for c in coros))
48+
async def coro_with_sem(coro: Coroutine[Any, Any, Any]) -> None:
49+
async with sem:
50+
await coro
4851

52+
await asyncio.gather(*(coro_with_sem(c) for c in coros))
4953

50-
async def run_requests(axis: Axes) -> None:
5154
async def httpcore_get(
5255
pool: httpcore.AsyncConnectionPool, timings: List[int]
5356
) -> None:
@@ -99,9 +102,85 @@ async def aiohttp_get(session: aiohttp.ClientSession, timings: List[int]) -> Non
99102
)
100103

101104

105+
def run_sync_requests(axis: Axes) -> None:
106+
def run_in_executor(
107+
fns: Iterator[Callable[[], None]], executor: ThreadPoolExecutor
108+
) -> None:
109+
futures = [executor.submit(fn) for fn in fns]
110+
for future in futures:
111+
future.result()
112+
113+
def httpcore_get(pool: httpcore.ConnectionPool, timings: List[int]) -> None:
114+
start = time.monotonic()
115+
res = pool.request("GET", URL)
116+
assert len(res.read()) == 2000
117+
assert res.status == 200, f"status_code={res.status}"
118+
timings.append(duration(start))
119+
120+
def requests_get(session: requests.Session, timings: List[int]) -> None:
121+
start = time.monotonic()
122+
res = session.get(URL)
123+
assert len(res.text) == 2000
124+
assert res.status_code == 200, f"status={res.status_code}"
125+
timings.append(duration(start))
126+
127+
with httpcore.ConnectionPool(max_connections=POOL_LIMIT) as pool:
128+
# warmup
129+
with ThreadPoolExecutor(max_workers=CONCURRENCY * 2) as exec:
130+
run_in_executor(
131+
(lambda: httpcore_get(pool, []) for _ in range(REQUESTS)),
132+
exec,
133+
)
134+
135+
timings: List[int] = []
136+
exec = ThreadPoolExecutor(max_workers=CONCURRENCY)
137+
start = time.monotonic()
138+
with profile():
139+
for _ in range(REPEATS):
140+
run_in_executor(
141+
(lambda: httpcore_get(pool, timings) for _ in range(REQUESTS)), exec
142+
)
143+
exec.shutdown(wait=True)
144+
axis.plot(
145+
[*range(len(timings))], timings, label=f"httpcore (tot={duration(start)}ms)"
146+
)
147+
148+
with requests.Session() as session:
149+
session.mount(
150+
"http://", HTTPAdapter(pool_connections=POOL_LIMIT, pool_maxsize=POOL_LIMIT)
151+
)
152+
# warmup
153+
with ThreadPoolExecutor(max_workers=CONCURRENCY * 2) as exec:
154+
run_in_executor(
155+
(lambda: requests_get(session, []) for _ in range(REQUESTS)),
156+
exec,
157+
)
158+
159+
timings = []
160+
exec = ThreadPoolExecutor(max_workers=CONCURRENCY)
161+
start = time.monotonic()
162+
for _ in range(REPEATS):
163+
run_in_executor(
164+
(lambda: requests_get(session, timings) for _ in range(REQUESTS)),
165+
exec,
166+
)
167+
exec.shutdown(wait=True)
168+
axis.plot(
169+
[*range(len(timings))], timings, label=f"requests (tot={duration(start)}ms)"
170+
)
171+
172+
102173
def main() -> None:
174+
mode = sys.argv[1] if len(sys.argv) == 2 else None
175+
assert mode in ("async", "sync"), "Usage: python client.py <async|sync>"
176+
103177
fig, ax = plt.subplots()
104-
asyncio.run(run_requests(ax))
178+
179+
if mode == "async":
180+
asyncio.run(run_async_requests(ax))
181+
else:
182+
run_sync_requests(ax)
183+
105184
plt.legend(loc="upper left")
106185
ax.set_xlabel("# request")
107186
ax.set_ylabel("[ms]")

0 commit comments

Comments
 (0)