Skip to content

Commit 54f1774

Browse files
authored
Improve performance and reorganise code (#655)
* Improve performance and reorganise code * Add internal updates for fast dispatching * Update release notes
1 parent 263449c commit 54f1774

File tree

9 files changed

+452
-125
lines changed

9 files changed

+452
-125
lines changed

docs/en/docs/release-notes.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,29 @@ hide:
55

66
# Release Notes
77

8+
## 0.3.8
9+
10+
!!! Note
11+
This release focuses on performance and request-path optimization while preserving
12+
existing routing and middleware behavior.
13+
14+
### Added
15+
16+
- New `benchmark_mode` setting for Ravyn applications.
17+
When enabled, Ravyn uses a minimal middleware pipeline that runs directly through the router,
18+
making micro-benchmark runs more representative of pure routing/handler overhead.
19+
20+
### Changed
21+
22+
- Optimized static HTTP route dispatch with an exact `method + path` fast lookup for safe route layouts.
23+
- Added a zero-kwargs handler fast path for simple endpoints, reducing per-request overhead when no request-bound kwargs are required.
24+
- Improved internal routing activation flow to precompute and refresh fast dispatch structures.
25+
26+
### Fixed
27+
28+
- Preserved route precedence guarantees for complex routing trees while introducing fast dispatch (includes, hosts, and dynamic routes continue to behave as expected).
29+
- Added tests validating benchmark mode wiring and middleware stack behavior.
30+
831
## 0.3.7
932

1033
!!! Note

ravyn/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from monkay import Monkay
44

5-
__version__ = "0.3.7"
5+
__version__ = "0.3.8"
66

77
if TYPE_CHECKING:
88
from lilya import status

ravyn/applications.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import warnings
22
from collections.abc import Callable, Iterable, Sequence
3-
from contextlib import asynccontextmanager
3+
from contextlib import asynccontextmanager, nullcontext
44
from datetime import timezone as dtimezone
55
from inspect import isclass
66
from typing import (
@@ -56,7 +56,6 @@
5656
from ravyn.middleware.cors import CORSMiddleware
5757
from ravyn.middleware.csrf import CSRFMiddleware
5858
from ravyn.middleware.exceptions import (
59-
ExceptionMiddleware,
6059
RavynAPIException,
6160
)
6261
from ravyn.middleware.trustedhost import TrustedHostMiddleware
@@ -134,6 +133,7 @@ class Application(BaseLilya):
134133
"deprecated",
135134
"description",
136135
"enable_openapi",
136+
"benchmark_mode",
137137
"enable_scheduler",
138138
"exception_handlers",
139139
"include_in_schema",
@@ -1443,6 +1443,17 @@ async def home() -> dict[str, str]:
14431443
"""
14441444
),
14451445
] = None,
1446+
benchmark_mode: Annotated[
1447+
Optional[bool],
1448+
Doc(
1449+
"""
1450+
Enables a minimal middleware pipeline optimized for benchmark runs.
1451+
1452+
This mode bypasses the app-level middleware wrappers and executes
1453+
directly through the router.
1454+
"""
1455+
),
1456+
] = None,
14461457
redirect_slashes: Annotated[
14471458
Optional[bool],
14481459
Doc(
@@ -1636,6 +1647,9 @@ def extend(self, config: PluggableConfig) -> None:
16361647
self.enable_openapi = self.load_settings_value(
16371648
"enable_openapi", enable_openapi, is_boolean=True
16381649
)
1650+
self.benchmark_mode = bool(
1651+
self.load_settings_value("benchmark_mode", benchmark_mode, is_boolean=True)
1652+
)
16391653
self.redirect_slashes = self.load_settings_value(
16401654
"redirect_slashes", redirect_slashes, is_boolean=True
16411655
)
@@ -2683,6 +2697,9 @@ def build_user_middleware_stack(self) -> list["DefineMiddleware"]:
26832697
26842698
It evaluates the middleware passed into the routes from bottom up
26852699
"""
2700+
if self.benchmark_mode:
2701+
return []
2702+
26862703
user_middleware = []
26872704

26882705
if self.allowed_hosts:
@@ -2727,6 +2744,9 @@ def build_middleware_stack(self) -> "ASGIApp":
27272744
For APIViews, since it's a "wrapper", the handler will update the current list to contain
27282745
both.
27292746
"""
2747+
if self.benchmark_mode:
2748+
return self.router
2749+
27302750
debug = self.debug
27312751
error_handler = None
27322752
exception_handlers = {}
@@ -2751,11 +2771,6 @@ def build_middleware_stack(self) -> "ASGIApp":
27512771
]
27522772
+ self.user_middleware
27532773
+ [
2754-
DefineMiddleware(
2755-
ExceptionMiddleware,
2756-
handlers=exception_handlers,
2757-
debug=debug,
2758-
),
27592774
DefineMiddleware(
27602775
AsyncExitStackMiddleware,
27612776
config=self.async_exit_config,
@@ -2844,15 +2859,20 @@ def default_settings(
28442859
return monkay_for_settings.settings
28452860

28462861
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
2847-
with monkay_for_settings.with_settings(self.settings):
2862+
settings_context = (
2863+
monkay_for_settings.with_settings(self.settings)
2864+
if self.settings_module is not None
2865+
else nullcontext()
2866+
)
2867+
with settings_context:
28482868
if scope["type"] == "lifespan":
28492869
await self.router.lifespan(scope, receive, send)
28502870
return
28512871

28522872
if self.root_path:
28532873
scope["root_path"] = self.root_path
28542874

2855-
scope["state"] = {}
2875+
scope.setdefault("state", {})
28562876
await super().__call__(scope, receive, send)
28572877

28582878
def websocket_route(self, path: str, name: Optional[str] = None) -> Callable:

ravyn/conf/global_settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,17 @@ class AppSettings(RavynSettings):
456456
"""
457457
),
458458
] = True
459+
benchmark_mode: Annotated[
460+
bool,
461+
Doc(
462+
"""
463+
Enables a minimal middleware pipeline optimized for benchmark runs.
464+
465+
When set to `True`, Ravyn bypasses app-level middleware wrappers and
466+
runs directly through the router.
467+
"""
468+
),
469+
] = False
459470
redirect_slashes: Annotated[
460471
bool,
461472
Doc(

ravyn/middleware/asyncexitstack.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@
88
from ravyn.core.protocols.middleware import MiddlewareProtocol
99

1010

11+
class _LazyAsyncExitStack:
12+
__slots__ = ("_stack",)
13+
14+
def __init__(self) -> None:
15+
self._stack: AsyncExitStack | None = None
16+
17+
def _ensure(self) -> AsyncExitStack:
18+
if self._stack is None:
19+
self._stack = AsyncExitStack()
20+
return self._stack
21+
22+
async def aclose(self) -> None:
23+
if self._stack is not None:
24+
await self._stack.aclose()
25+
26+
def __getattr__(self, item: str) -> object:
27+
return getattr(self._ensure(), item)
28+
29+
1130
class AsyncExitStackMiddleware(MiddlewareProtocol):
1231
def __init__(
1332
self,
@@ -32,12 +51,15 @@ async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> No
3251
await self.app(scope, receive, send) # pragma: no cover
3352

3453
exception: Optional[Exception] = None
35-
async with AsyncExitStack() as stack:
36-
scope[self.config.context_name] = stack
37-
try:
38-
await self.app(scope, receive, send)
39-
except Exception as e:
40-
exception = e
54+
lazy_stack = _LazyAsyncExitStack()
55+
scope[self.config.context_name] = lazy_stack
56+
scope["_ravyn_route_boundary"] = True
57+
try:
58+
await self.app(scope, receive, send)
59+
except Exception as e:
60+
exception = e
61+
finally:
62+
await lazy_stack.aclose()
4163

4264
if exception and self.debug:
4365
traceback.print_exception(exception, exception, exception.__traceback__) # type: ignore

ravyn/middleware/exceptions.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import Any, Callable, Mapping, Optional, Type, Union
22

33
from lilya import status
4+
from lilya.compat import is_async_callable
5+
from lilya.concurrency import run_in_threadpool
46
from lilya.exceptions import HTTPException as LilyaException
57
from lilya.middleware.exceptions import ExceptionMiddleware as LilyaExceptionMiddleware
68
from lilya.responses import Response as LilyaResponse
@@ -87,17 +89,26 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
8789
await self.app(scope, receive, send)
8890
except Exception as ex:
8991
if scope["type"] == ScopeType.HTTP:
90-
exception_handler = (
91-
self.get_exception_handler(self.exception_handlers, ex)
92-
or self.default_http_exception_handler
93-
)
94-
response = exception_handler(Request(scope, receive, send), ex)
92+
exception_handler = self.get_exception_handler(self.exception_handlers, ex)
93+
if (
94+
exception_handler is None
95+
and scope.get("_ravyn_route_boundary")
96+
and isinstance(ex, (HTTPException, LilyaException))
97+
):
98+
exception_handler = http_exception_handler # type: ignore
99+
if exception_handler is None:
100+
exception_handler = self.default_http_exception_handler # type: ignore
101+
request = Request(scope, receive, send)
102+
if is_async_callable(exception_handler):
103+
response = await exception_handler(request, ex)
104+
else:
105+
response = await run_in_threadpool(exception_handler, request, ex)
95106
await response(scope, receive, send)
96107
return
97108

98109
if isinstance(ex, WebSocketException):
99110
code = ex.code
100-
reason = ex.detail
111+
reason = ex.reason
101112
elif isinstance(ex, LilyaException):
102113
code = ex.status_code + 4000
103114
reason = ex.detail
@@ -149,6 +160,13 @@ def get_exception_handler(
149160
if not exception_handlers:
150161
return None
151162

152-
return self.exception_handlers.get(status_code) or self.exception_handlers.get(
153-
exc.__class__
154-
)
163+
handler = self.exception_handlers.get(status_code)
164+
if handler is not None:
165+
return handler
166+
167+
for exc_cls in type(exc).__mro__:
168+
handler = self.exception_handlers.get(exc_cls)
169+
if handler is not None:
170+
return handler
171+
172+
return None

0 commit comments

Comments
 (0)