fastapi/tests/benchmarks/test_general_performance.py

397 lines
12 KiB
Python

import json
import sys
from collections.abc import Iterator
from typing import Annotated, Any
import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
if "--codspeed" not in sys.argv:
pytest.skip(
"Benchmark tests are skipped by default; run with --codspeed.",
allow_module_level=True,
)
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",
params=[
"pydantic-v2",
"pydantic-v1",
],
)
def basemodel_class(request: pytest.FixtureRequest) -> type[Any]:
if request.param == "pydantic-v2":
from pydantic import BaseModel
return BaseModel
else:
from pydantic.v1 import BaseModel
return BaseModel
@pytest.fixture(scope="module")
def app(basemodel_class: type[Any]) -> FastAPI:
class ItemIn(basemodel_class):
name: str
value: int
class ItemOut(basemodel_class):
name: str
value: int
dep: int
class LargeIn(basemodel_class):
items: list[dict[str, Any]]
metadata: dict[str, Any]
class LargeOut(basemodel_class):
items: list[dict[str, Any]]
metadata: dict[str, Any]
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()