From e2a459e03f21211b8708a01d53082a1af76f7112 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 5 Nov 2025 19:40:33 -0800 Subject: [PATCH 01/10] Add FastAPI HTTP request benchmark --- pyperformance/data-files/benchmarks/MANIFEST | 1 + .../benchmarks/bm_fastapi/pyproject.toml | 15 ++++ .../benchmarks/bm_fastapi/requirements.txt | 17 ++++ .../benchmarks/bm_fastapi/run_benchmark.py | 88 +++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 pyperformance/data-files/benchmarks/bm_fastapi/pyproject.toml create mode 100644 pyperformance/data-files/benchmarks/bm_fastapi/requirements.txt create mode 100644 pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py diff --git a/pyperformance/data-files/benchmarks/MANIFEST b/pyperformance/data-files/benchmarks/MANIFEST index 570d45aa..4d19dc3d 100644 --- a/pyperformance/data-files/benchmarks/MANIFEST +++ b/pyperformance/data-files/benchmarks/MANIFEST @@ -42,6 +42,7 @@ django_template dulwich_log docutils fannkuch +fastapi float genshi go diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/pyproject.toml b/pyperformance/data-files/benchmarks/bm_fastapi/pyproject.toml new file mode 100644 index 00000000..f8dd68d9 --- /dev/null +++ b/pyperformance/data-files/benchmarks/bm_fastapi/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "pyperformance_bm_fastapi" +requires-python = ">=3.10" +dependencies = [ + "pyperf", + "fastapi", + "uvicorn", + "httpx", +] +urls = {repository = "https://github.com/python/pyperformance"} +dynamic = ["version"] + +[tool.pyperformance] +name = "fastapi" +tags = "apps" diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/requirements.txt b/pyperformance/data-files/benchmarks/bm_fastapi/requirements.txt new file mode 100644 index 00000000..7ee4162e --- /dev/null +++ b/pyperformance/data-files/benchmarks/bm_fastapi/requirements.txt @@ -0,0 +1,17 @@ +annotated-doc==0.0.3 +annotated-types==0.7.0 +anyio==4.11.0 +certifi==2025.10.5 +click==8.3.0 +fastapi==0.121.0 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.11 +pydantic==2.12.4 +pydantic-core==2.41.5 +sniffio==1.3.1 +starlette==0.49.3 +typing-extensions==4.15.0 +typing-inspection==0.4.2 +uvicorn==0.38.0 diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py new file mode 100644 index 00000000..659f1985 --- /dev/null +++ b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py @@ -0,0 +1,88 @@ +""" +Test the performance of simple HTTP serving with FastAPI. + +This benchmark tests FastAPI's request handling, including: +- Path parameter extraction and validation +- Pydantic model serialization +- JSON response encoding + +The bench serves a REST API endpoint that returns JSON objects, +simulating a typical web application scenario. + +Author: Savannah Ostrowski +""" + +import asyncio +import socket +import time +from typing import List + +import httpx +import pyperf +import uvicorn +from fastapi import FastAPI +from pydantic import BaseModel + +HOST = "127.0.0.1" + +CONCURRENCY = 150 + +class Item(BaseModel): + id: int + name: str + price: float + tags: List[str] = [] + +app = FastAPI() + +@app.get("/items/{item_id}", response_model=Item) +async def get_item(item_id: int): + return { + "id": item_id, + "name": "Sample Item", + "price": 9.99, + "tags": ["sample", "item", "fastapi"] + } + + +async def run_server_and_benchmark(loops): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((HOST, 0)) + s.listen(1) + port = s.getsockname()[1] + + config = uvicorn.Config(app, host=HOST, port=port, log_level="error") + server = uvicorn.Server(config) + server_task = asyncio.create_task(server.serve()) + + await asyncio.sleep(0.5) + + async with httpx.AsyncClient() as client: + t0 = time.perf_counter() + + for i in range(loops): + tasks = [ + client.get(f"http://{HOST}:{port}/items/{i}") + for _ in range(CONCURRENCY) + ] + responses = await asyncio.gather(*tasks) + for response in responses: + response.raise_for_status() + data = response.json() + assert data["id"] == i + assert "tags" in data + + elapsed = time.perf_counter() - t0 + + server.should_exit = True + await server_task + return elapsed + +def bench_fastapi(loops): + return asyncio.run(run_server_and_benchmark(loops)) + + +if __name__ == "__main__": + runner = pyperf.Runner() + runner.metadata['description'] = "Test the performance of HTTP requests with FastAPI" + runner.bench_time_func("fastapi_http", bench_fastapi) \ No newline at end of file From 3cef6be2404a4b9664946ab62f058e576a734806 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 5 Nov 2025 19:41:06 -0800 Subject: [PATCH 02/10] Add new line --- pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py index 659f1985..66a641d9 100644 --- a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py +++ b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py @@ -85,4 +85,4 @@ def bench_fastapi(loops): if __name__ == "__main__": runner = pyperf.Runner() runner.metadata['description'] = "Test the performance of HTTP requests with FastAPI" - runner.bench_time_func("fastapi_http", bench_fastapi) \ No newline at end of file + runner.bench_time_func("fastapi_http", bench_fastapi) From 1a172af356f492525afe85d8c91bc92fa7373148 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 5 Nov 2025 19:54:56 -0800 Subject: [PATCH 03/10] Use tabs in MANIFEST --- pyperformance/data-files/benchmarks/MANIFEST | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyperformance/data-files/benchmarks/MANIFEST b/pyperformance/data-files/benchmarks/MANIFEST index 4d19dc3d..dd22b77a 100644 --- a/pyperformance/data-files/benchmarks/MANIFEST +++ b/pyperformance/data-files/benchmarks/MANIFEST @@ -42,7 +42,7 @@ django_template dulwich_log docutils fannkuch -fastapi +fastapi float genshi go From 6f5bcfe0cac014b6c36b541f19bfccde5804b1c9 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 5 Nov 2025 20:05:33 -0800 Subject: [PATCH 04/10] Remove extraneous pinned deps --- .../benchmarks/bm_fastapi/requirements.txt | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/requirements.txt b/pyperformance/data-files/benchmarks/bm_fastapi/requirements.txt index 7ee4162e..bbcf5238 100644 --- a/pyperformance/data-files/benchmarks/bm_fastapi/requirements.txt +++ b/pyperformance/data-files/benchmarks/bm_fastapi/requirements.txt @@ -1,17 +1,3 @@ -annotated-doc==0.0.3 -annotated-types==0.7.0 -anyio==4.11.0 -certifi==2025.10.5 -click==8.3.0 fastapi==0.121.0 -h11==0.16.0 -httpcore==1.0.9 httpx==0.28.1 -idna==3.11 -pydantic==2.12.4 -pydantic-core==2.41.5 -sniffio==1.3.1 -starlette==0.49.3 -typing-extensions==4.15.0 -typing-inspection==0.4.2 uvicorn==0.38.0 From 556e08f726881777d82978ae04d421fc54b43ed6 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 5 Nov 2025 20:08:45 -0800 Subject: [PATCH 05/10] Syntax cleanup --- .../data-files/benchmarks/bm_fastapi/run_benchmark.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py index 66a641d9..2c8bad5f 100644 --- a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py +++ b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py @@ -15,7 +15,6 @@ import asyncio import socket import time -from typing import List import httpx import pyperf @@ -31,7 +30,7 @@ class Item(BaseModel): id: int name: str price: float - tags: List[str] = [] + tags: list[str] = [] app = FastAPI() From 3d5699246d975cb7666822bf19df9076bea230d8 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Thu, 11 Dec 2025 08:27:21 -0800 Subject: [PATCH 06/10] Restructure to remove server config from timing --- .../benchmarks/bm_fastapi/run_benchmark.py | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py index 2c8bad5f..10d0efd3 100644 --- a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py +++ b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py @@ -14,7 +14,6 @@ import asyncio import socket -import time import httpx import pyperf @@ -44,41 +43,42 @@ async def get_item(item_id: int): } -async def run_server_and_benchmark(loops): +def bench_fastapi(loops): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind((HOST, 0)) s.listen(1) port = s.getsockname()[1] + # Setup server config = uvicorn.Config(app, host=HOST, port=port, log_level="error") server = uvicorn.Server(config) - server_task = asyncio.create_task(server.serve()) - await asyncio.sleep(0.5) + async def run_benchmark(): + server_task = asyncio.create_task(server.serve()) + await asyncio.sleep(0.5) - async with httpx.AsyncClient() as client: - t0 = time.perf_counter() + async with httpx.AsyncClient() as client: + t0 = pyperf.perf_counter() - for i in range(loops): - tasks = [ - client.get(f"http://{HOST}:{port}/items/{i}") - for _ in range(CONCURRENCY) - ] - responses = await asyncio.gather(*tasks) - for response in responses: - response.raise_for_status() - data = response.json() - assert data["id"] == i - assert "tags" in data + for i in range(loops): + tasks = [ + client.get(f"http://{HOST}:{port}/items/{i}") + for _ in range(CONCURRENCY) + ] + responses = await asyncio.gather(*tasks) + for response in responses: + response.raise_for_status() + data = response.json() + assert data["id"] == i + assert "tags" in data - elapsed = time.perf_counter() - t0 + elapsed = pyperf.perf_counter() - t0 - server.should_exit = True - await server_task - return elapsed + server.should_exit = True + await server_task + return elapsed -def bench_fastapi(loops): - return asyncio.run(run_server_and_benchmark(loops)) + return asyncio.run(run_benchmark()) if __name__ == "__main__": From 28148ed9429d95534e79a1663bcd9aad0472d19e Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Thu, 11 Dec 2025 08:52:15 -0800 Subject: [PATCH 07/10] Use polling --- .../data-files/benchmarks/bm_fastapi/run_benchmark.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py index 10d0efd3..3f662e36 100644 --- a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py +++ b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py @@ -55,7 +55,8 @@ def bench_fastapi(loops): async def run_benchmark(): server_task = asyncio.create_task(server.serve()) - await asyncio.sleep(0.5) + while not server.started: + await asyncio.sleep(0.01) async with httpx.AsyncClient() as client: t0 = pyperf.perf_counter() From 5b14cad1a2b02f4cadb00f167650e80b130309be Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Thu, 11 Dec 2025 08:52:34 -0800 Subject: [PATCH 08/10] Cleanup --- pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py index 3f662e36..50ce87dc 100644 --- a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py +++ b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py @@ -49,7 +49,6 @@ def bench_fastapi(loops): s.listen(1) port = s.getsockname()[1] - # Setup server config = uvicorn.Config(app, host=HOST, port=port, log_level="error") server = uvicorn.Server(config) From fa4e05f08a8c4959586be0eb8eec1d7af25baadc Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Thu, 11 Dec 2025 09:10:49 -0800 Subject: [PATCH 09/10] Move setup --- .../benchmarks/bm_fastapi/run_benchmark.py | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py index 50ce87dc..229d1dcd 100644 --- a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py +++ b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py @@ -43,26 +43,14 @@ async def get_item(item_id: int): } -def bench_fastapi(loops): - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind((HOST, 0)) - s.listen(1) - port = s.getsockname()[1] - - config = uvicorn.Config(app, host=HOST, port=port, log_level="error") - server = uvicorn.Server(config) - +def bench_fastapi(loops, url): async def run_benchmark(): - server_task = asyncio.create_task(server.serve()) - while not server.started: - await asyncio.sleep(0.01) - async with httpx.AsyncClient() as client: t0 = pyperf.perf_counter() for i in range(loops): tasks = [ - client.get(f"http://{HOST}:{port}/items/{i}") + client.get(f"{url}/items/{i}") for _ in range(CONCURRENCY) ] responses = await asyncio.gather(*tasks) @@ -72,16 +60,30 @@ async def run_benchmark(): assert data["id"] == i assert "tags" in data - elapsed = pyperf.perf_counter() - t0 - - server.should_exit = True - await server_task - return elapsed + return pyperf.perf_counter() - t0 return asyncio.run(run_benchmark()) if __name__ == "__main__": + import threading + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((HOST, 0)) + s.listen(1) + port = s.getsockname()[1] + + config = uvicorn.Config(app, host=HOST, port=port, log_level="error") + server = uvicorn.Server(config) + + server_thread = threading.Thread(target=server.run, daemon=True) + server_thread.start() + + while not server.started: + pass + + url = f"http://{HOST}:{port}" + runner = pyperf.Runner() runner.metadata['description'] = "Test the performance of HTTP requests with FastAPI" - runner.bench_time_func("fastapi_http", bench_fastapi) + runner.bench_time_func("fastapi_http", bench_fastapi, url) From 38b1567ff0e09e58a2061ff4730754fc2eb130c3 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Thu, 11 Dec 2025 09:45:44 -0800 Subject: [PATCH 10/10] Remove setup to function --- .../benchmarks/bm_fastapi/run_benchmark.py | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py index 229d1dcd..3e440325 100644 --- a/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py +++ b/pyperformance/data-files/benchmarks/bm_fastapi/run_benchmark.py @@ -17,6 +17,7 @@ import httpx import pyperf +import threading import uvicorn from fastapi import FastAPI from pydantic import BaseModel @@ -42,6 +43,23 @@ async def get_item(item_id: int): "tags": ["sample", "item", "fastapi"] } +def setup_server(): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((HOST, 0)) + s.listen(1) + port = s.getsockname()[1] + + config = uvicorn.Config(app, host=HOST, port=port, log_level="error") + server = uvicorn.Server(config) + + server_thread = threading.Thread(target=server.run, daemon=True) + server_thread.start() + + while not server.started: + pass + + url = f"http://{HOST}:{port}" + return url def bench_fastapi(loops, url): async def run_benchmark(): @@ -66,24 +84,7 @@ async def run_benchmark(): if __name__ == "__main__": - import threading - - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind((HOST, 0)) - s.listen(1) - port = s.getsockname()[1] - - config = uvicorn.Config(app, host=HOST, port=port, log_level="error") - server = uvicorn.Server(config) - - server_thread = threading.Thread(target=server.run, daemon=True) - server_thread.start() - - while not server.started: - pass - - url = f"http://{HOST}:{port}" - + url = setup_server() runner = pyperf.Runner() runner.metadata['description'] = "Test the performance of HTTP requests with FastAPI" runner.bench_time_func("fastapi_http", bench_fastapi, url)