diff --git a/tests/benchmarks/__init__.py b/tests/benchmarks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/benchmarks/test_general_performance.py b/tests/benchmarks/test_general_performance.py new file mode 100644 index 000000000..0fc44fed5 --- /dev/null +++ b/tests/benchmarks/test_general_performance.py @@ -0,0 +1,378 @@ +import json +from collections.abc import Iterator +from typing import Any, Dict, List, Tuple + +import pytest +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel +from typing_extensions import Annotated + + +class ItemIn(BaseModel): + name: str + value: int + + +class ItemOut(BaseModel): + name: str + value: int + dep: int + + +class LargeIn(BaseModel): + items: List[Dict[str, Any]] + metadata: Dict[str, Any] + + +class LargeOut(BaseModel): + items: List[Dict[str, Any]] + metadata: Dict[str, Any] + + +LARGE_ITEMS: List[Dict[str, Any]] = [ + { + "id": i, + "name": f"item-{i}", + "values": list(range(25)), + "meta": { + "active": True, + "group": i % 10, + "tag": f"t{i % 5}", + }, + } + for i in range(300) +] + +LARGE_METADATA: Dict[str, Any] = { + "source": "benchmark", + "version": 1, + "flags": {"a": True, "b": False, "c": True}, + "notes": ["x" * 50, "y" * 50, "z" * 50], +} + +LARGE_PAYLOAD: Dict[str, Any] = {"items": LARGE_ITEMS, "metadata": LARGE_METADATA} + + +def dep_a(): + return 40 + + +def dep_b(a: Annotated[int, Depends(dep_a)]): + return a + 2 + + +@pytest.fixture(scope="module") +def app() -> FastAPI: + app = FastAPI() + + @app.post("/sync/validated", response_model=ItemOut) + def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name=item.name, value=item.value, dep=dep) + + @app.get("/sync/dict-no-response-model") + def sync_dict_no_response_model(): + return {"name": "foo", "value": 123} + + @app.get("/sync/dict-with-response-model", response_model=ItemOut) + def sync_dict_with_response_model( + dep: Annotated[int, Depends(dep_b)], + ): + return {"name": "foo", "value": 123, "dep": dep} + + @app.get("/sync/model-no-response-model") + def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name="foo", value=123, dep=dep) + + @app.get("/sync/model-with-response-model", response_model=ItemOut) + def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]): + return ItemOut(name="foo", value=123, dep=dep) + + @app.post("/async/validated", response_model=ItemOut) + async def async_validated( + item: ItemIn, + dep: Annotated[int, Depends(dep_b)], + ): + return ItemOut(name=item.name, value=item.value, dep=dep) + + @app.post("/sync/large-receive") + def sync_large_receive(payload: LargeIn): + return {"received": len(payload.items)} + + @app.post("/async/large-receive") + async def async_large_receive(payload: LargeIn): + return {"received": len(payload.items)} + + @app.get("/sync/large-dict-no-response-model") + def sync_large_dict_no_response_model(): + return LARGE_PAYLOAD + + @app.get("/sync/large-dict-with-response-model", response_model=LargeOut) + def sync_large_dict_with_response_model(): + return LARGE_PAYLOAD + + @app.get("/sync/large-model-no-response-model") + def sync_large_model_no_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + @app.get("/sync/large-model-with-response-model", response_model=LargeOut) + def sync_large_model_with_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + @app.get("/async/large-dict-no-response-model") + async def async_large_dict_no_response_model(): + return LARGE_PAYLOAD + + @app.get("/async/large-dict-with-response-model", response_model=LargeOut) + async def async_large_dict_with_response_model(): + return LARGE_PAYLOAD + + @app.get("/async/large-model-no-response-model") + async def async_large_model_no_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + @app.get("/async/large-model-with-response-model", response_model=LargeOut) + async def async_large_model_with_response_model(): + return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA) + + @app.get("/async/dict-no-response-model") + async def async_dict_no_response_model(): + return {"name": "foo", "value": 123} + + @app.get("/async/dict-with-response-model", response_model=ItemOut) + async def async_dict_with_response_model( + dep: Annotated[int, Depends(dep_b)], + ): + return {"name": "foo", "value": 123, "dep": dep} + + @app.get("/async/model-no-response-model") + async def async_model_no_response_model( + dep: Annotated[int, Depends(dep_b)], + ): + return ItemOut(name="foo", value=123, dep=dep) + + @app.get("/async/model-with-response-model", response_model=ItemOut) + async def async_model_with_response_model( + dep: Annotated[int, Depends(dep_b)], + ): + return ItemOut(name="foo", value=123, dep=dep) + + return app + + +@pytest.fixture(scope="module") +def client(app: FastAPI) -> Iterator[TestClient]: + with TestClient(app) as client: + yield client + + +def _bench_get(benchmark, client: TestClient, path: str) -> Tuple[int, bytes]: + warmup = client.get(path) + assert warmup.status_code == 200 + + def do_request() -> Tuple[int, bytes]: + response = client.get(path) + return response.status_code, response.content + + return benchmark(do_request) + + +def _bench_post_json( + benchmark, client: TestClient, path: str, json: Dict[str, Any] +) -> Tuple[int, bytes]: + warmup = client.post(path, json=json) + assert warmup.status_code == 200 + + def do_request() -> Tuple[int, bytes]: + response = client.post(path, json=json) + return response.status_code, response.content + + return benchmark(do_request) + + +def test_sync_receiving_validated_pydantic_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_post_json( + benchmark, + client, + "/sync/validated", + json={"name": "foo", "value": 123}, + ) + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_sync_return_dict_without_response_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_get(benchmark, client, "/sync/dict-no-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123}' + + +def test_sync_return_dict_with_response_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_get(benchmark, client, "/sync/dict-with-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_sync_return_model_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get(benchmark, client, "/sync/model-no-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_sync_return_model_with_response_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_get(benchmark, client, "/sync/model-with-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_async_receiving_validated_pydantic_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_post_json( + benchmark, client, "/async/validated", json={"name": "foo", "value": 123} + ) + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_async_return_dict_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get(benchmark, client, "/async/dict-no-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123}' + + +def test_async_return_dict_with_response_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_get(benchmark, client, "/async/dict-with-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_async_return_model_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get(benchmark, client, "/async/model-no-response-model") + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_async_return_model_with_response_model(benchmark, client: TestClient) -> None: + status_code, body = _bench_get( + benchmark, client, "/async/model-with-response-model" + ) + assert status_code == 200 + assert body == b'{"name":"foo","value":123,"dep":42}' + + +def test_sync_receiving_large_payload(benchmark, client: TestClient) -> None: + status_code, body = _bench_post_json( + benchmark, + client, + "/sync/large-receive", + json=LARGE_PAYLOAD, + ) + assert status_code == 200 + assert body == b'{"received":300}' + + +def test_async_receiving_large_payload(benchmark, client: TestClient) -> None: + status_code, body = _bench_post_json( + benchmark, + client, + "/async/large-receive", + json=LARGE_PAYLOAD, + ) + assert status_code == 200 + assert body == b'{"received":300}' + + +def _expected_large_payload_json_bytes() -> bytes: + return json.dumps( + LARGE_PAYLOAD, + ensure_ascii=False, + allow_nan=False, + separators=(",", ":"), + ).encode("utf-8") + + +def test_sync_return_large_dict_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/sync/large-dict-no-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_sync_return_large_dict_with_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/sync/large-dict-with-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_sync_return_large_model_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/sync/large-model-no-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_sync_return_large_model_with_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/sync/large-model-with-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_async_return_large_dict_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/async/large-dict-no-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_async_return_large_dict_with_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/async/large-dict-with-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_async_return_large_model_without_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/async/large-model-no-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes() + + +def test_async_return_large_model_with_response_model( + benchmark, client: TestClient +) -> None: + status_code, body = _bench_get( + benchmark, client, "/async/large-model-with-response-model" + ) + assert status_code == 200 + assert body == _expected_large_payload_json_bytes()