diff --git a/CHANGES/9158.misc.rst b/CHANGES/9158.misc.rst new file mode 100644 index 00000000000..8d87623c056 --- /dev/null +++ b/CHANGES/9158.misc.rst @@ -0,0 +1,3 @@ +Significantly improved performance of middlewares -- by :user:`bdraco`. + +The construction of the middleware wrappers is now cached and is built once per handler instead of on every request. diff --git a/aiohttp/web_app.py b/aiohttp/web_app.py index 516d5e619b5..dd13ea00714 100644 --- a/aiohttp/web_app.py +++ b/aiohttp/web_app.py @@ -1,7 +1,7 @@ import asyncio import logging import warnings -from functools import partial, update_wrapper +from functools import cache, partial, update_wrapper from typing import ( TYPE_CHECKING, Any, @@ -16,6 +16,7 @@ MutableMapping, Optional, Sequence, + Tuple, Type, TypeVar, Union, @@ -30,7 +31,7 @@ from . import hdrs from .helpers import AppKey from .log import web_logger -from .typedefs import Middleware +from .typedefs import Handler, Middleware from .web_exceptions import NotAppKeyWarning from .web_middlewares import _fix_request_current_app from .web_request import Request @@ -69,6 +70,18 @@ _Resource = TypeVar("_Resource", bound=AbstractResource) +@cache +def _build_middlewares( + handler: Handler, apps: Tuple["Application", ...] +) -> Callable[[Request], Awaitable[StreamResponse]]: + """Apply middlewares to handler.""" + for app in apps: + assert app.pre_frozen, "middleware handlers are not ready" + for m in app._middlewares_handlers: + handler = update_wrapper(partial(m, handler=handler), handler) + return handler + + @final class Application(MutableMapping[Union[str, AppKey[Any]], Any]): __slots__ = ( @@ -186,6 +199,9 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[Union[str, AppKey[Any]]]: return iter(self._state) + def __hash__(self) -> int: + return id(self) + @overload # type: ignore[override] def get(self, key: AppKey[_T], default: None = ...) -> Optional[_T]: ... @@ -380,10 +396,7 @@ async def _handle(self, request: Request) -> StreamResponse: handler = match_info.handler if self._run_middlewares: - for app in match_info.apps[::-1]: - assert app.pre_frozen, "middleware handlers are not ready" - for m in app._middlewares_handlers: - handler = update_wrapper(partial(m, handler=handler), handler) + handler = _build_middlewares(handler, match_info.apps[::-1]) resp = await handler(request) diff --git a/tests/test_web_middleware.py b/tests/test_web_middleware.py index 1e5b75ce2f6..57f4670c8fa 100644 --- a/tests/test_web_middleware.py +++ b/tests/test_web_middleware.py @@ -23,10 +23,13 @@ async def middleware(request, handler: Handler): app.middlewares.append(middleware) app.router.add_route("GET", "/", handler) client = await aiohttp_client(app) - resp = await client.get("/") - assert 201 == resp.status - txt = await resp.text() - assert "OK[MIDDLEWARE]" == txt + + # Call twice to verify cache works + for _ in range(2): + resp = await client.get("/") + assert 201 == resp.status + txt = await resp.text() + assert "OK[MIDDLEWARE]" == txt async def test_middleware_handles_exception(loop: Any, aiohttp_client: Any) -> None: @@ -42,10 +45,13 @@ async def middleware(request, handler: Handler): app.middlewares.append(middleware) app.router.add_route("GET", "/", handler) client = await aiohttp_client(app) - resp = await client.get("/") - assert 501 == resp.status - txt = await resp.text() - assert "Error text[MIDDLEWARE]" == txt + + # Call twice to verify cache works + for _ in range(2): + resp = await client.get("/") + assert 501 == resp.status + txt = await resp.text() + assert "Error text[MIDDLEWARE]" == txt async def test_middleware_chain(loop: Any, aiohttp_client: Any) -> None: