]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
👷 Add CI memory benchmark (#16046)
authorSebastián Ramírez <tiangolo@gmail.com>
Fri, 24 Jul 2026 20:21:58 +0000 (22:21 +0200)
committerGitHub <noreply@github.com>
Fri, 24 Jul 2026 20:21:58 +0000 (20:21 +0000)
.github/workflows/test.yml
pyproject.toml
tests/memory_benchmarks/__init__.py [new file with mode: 0644]
tests/memory_benchmarks/test_dependency_graph.py [new file with mode: 0644]
uv.lock

index 6855f0d48e6db54ea079cde3200c6a90be5d9a7e..25af318cf82cccebf81659c7ffe38f27b49143b2 100644 (file)
@@ -195,6 +195,11 @@ jobs:
         with:
           mode: simulation
           run: uv run --no-sync pytest tests/benchmarks --codspeed
+      - name: CodSpeed memory benchmark
+        uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1
+        with:
+          mode: memory
+          run: uv run --no-sync pytest tests/memory_benchmarks --codspeed
 
   coverage-combine:
     needs:
index e33e46fd818fd8039dfee31a9391a93cf632cb38..2fd2f2e5d13928c253c285a1534892a4bb567fe0 100644 (file)
@@ -168,7 +168,7 @@ tests = [
     "pwdlib[argon2] >=0.2.1",
     "pyjwt >=2.9.0",
     "pytest >=9.0.0",
-    "pytest-codspeed >=4.2.0",
+    "pytest-codspeed >=4.3.0",
     "pyyaml >=5.3.1,<7.0.0",
     "sqlmodel >=0.0.31",
     "strawberry-graphql >=0.200.0,<1.0.0",
@@ -244,6 +244,7 @@ relative_files = true
 context = '${CONTEXT}'
 omit = [
     "tests/benchmarks/*",
+    "tests/memory_benchmarks/*",
     "docs_src/response_model/tutorial003_04_py310.py",
     "docs_src/dependencies/tutorial013_an_py310.py",  # temporary code example?
     "docs_src/dependencies/tutorial014_an_py310.py",  # temporary code example?
diff --git a/tests/memory_benchmarks/__init__.py b/tests/memory_benchmarks/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/memory_benchmarks/test_dependency_graph.py b/tests/memory_benchmarks/test_dependency_graph.py
new file mode 100644 (file)
index 0000000..330fe6e
--- /dev/null
@@ -0,0 +1,86 @@
+import inspect
+import sys
+from collections.abc import Callable
+from typing import Any
+
+import pytest
+from fastapi import Depends, FastAPI
+from fastapi.routing import APIRoute
+
+if "--codspeed" not in sys.argv:
+    pytest.skip(
+        "Benchmark tests are skipped by default; run with --codspeed.",
+        allow_module_level=True,
+    )
+
+LAST_DEPENDENCY_INDEX = 100
+ENDPOINT_PARAMETER_COUNT = 50
+ROUTE_PATH = "/dynamic-health"
+
+
+def _create_app() -> FastAPI:
+    app = FastAPI()
+    dependencies: dict[int, Callable[..., Any]] = {}
+
+    def create_dependency(index: int) -> Callable[..., Any]:
+        if index == LAST_DEPENDENCY_INDEX:
+
+            def dependency() -> str:
+                return str(index)
+
+            dependency.__name__ = f"dependency_{index}"
+            return dependency
+
+        next_dependency = dependencies[index + 1]
+
+        async def dependency(
+            sub_dependency: str = Depends(next_dependency),
+        ) -> str:
+            return f"{index} -> {sub_dependency}"
+
+        dependency.__name__ = f"dependency_{index}"
+        return dependency
+
+    for index in reversed(range(LAST_DEPENDENCY_INDEX + 1)):
+        dependencies[index] = create_dependency(index)
+
+    def create_endpoint() -> Callable[..., Any]:
+        parameters = [
+            inspect.Parameter(
+                name=f"arg_{index}",
+                kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
+                default=Depends(dependencies[index]),
+                annotation=str,
+            )
+            for index in range(ENDPOINT_PARAMETER_COUNT)
+        ]
+
+        async def endpoint(**kwargs: str) -> dict[str, int]:
+            return {"parameter_count": len(kwargs)}
+
+        endpoint_with_signature: Any = endpoint
+        endpoint_with_signature.__signature__ = inspect.Signature(parameters)
+        return endpoint
+
+    for method in ("GET", "POST"):
+        app.add_api_route(
+            ROUTE_PATH,
+            create_endpoint(),
+            methods=[method],
+        )
+
+    return app
+
+
+def test_dependency_graph(benchmark) -> None:
+    app = benchmark(_create_app)
+    dynamic_routes = [
+        route
+        for route in app.routes
+        if isinstance(route, APIRoute) and route.path == ROUTE_PATH
+    ]
+    assert len(dynamic_routes) == 2
+    assert {method for route in dynamic_routes for method in route.methods} == {
+        "GET",
+        "POST",
+    }
diff --git a/uv.lock b/uv.lock
index 1918bdb67b67cd8afc51195a633226641b30e898..564fcc19041fcc5da5f1c777440aa08ba973420c 100644 (file)
--- a/uv.lock
+++ b/uv.lock
@@ -1064,7 +1064,7 @@ dev = [
     { name = "pygithub", specifier = ">=2.8.1" },
     { name = "pyjwt", specifier = ">=2.9.0" },
     { name = "pytest", specifier = ">=9.0.0" },
-    { name = "pytest-codspeed", specifier = ">=4.2.0" },
+    { name = "pytest-codspeed", specifier = ">=4.3.0" },
     { name = "pytest-cov", specifier = ">=4.0.0" },
     { name = "pytest-sugar", specifier = ">=1.0.0" },
     { name = "pytest-timeout", specifier = ">=2.4.0" },
@@ -1124,7 +1124,7 @@ tests = [
     { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" },
     { name = "pyjwt", specifier = ">=2.9.0" },
     { name = "pytest", specifier = ">=9.0.0" },
-    { name = "pytest-codspeed", specifier = ">=4.2.0" },
+    { name = "pytest-codspeed", specifier = ">=4.3.0" },
     { name = "pytest-cov", specifier = ">=4.0.0" },
     { name = "pytest-sugar", specifier = ">=1.0.0" },
     { name = "pytest-timeout", specifier = ">=2.4.0" },