Perf improv (#2074)

* handler improvements for performance

* Resovle tests

* Linting

* Add tests
This commit is contained in:
Adam Hopkins 2021-03-21 09:47:21 +02:00 committed by GitHub
parent 8a2ea626c6
commit 15a8b5c894
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 79 additions and 56 deletions

View File

@ -676,27 +676,23 @@ class Sanic(BaseSanic):
response = None response = None
try: try:
# Fetch handler from router # Fetch handler from router
( route, handler, kwargs = self.router.get(
route, request.path, request.method, request.headers.get("host")
handler, )
kwargs,
) = self.router.get(request)
request._match_info = kwargs request._match_info = kwargs
request.route = route request.route = route
request.name = route.name
request.uri_template = f"/{route.path}"
request.endpoint = request.name
if ( if (
request.stream request.stream.request_body # type: ignore
and request.stream.request_body
and not route.ctx.ignore_body and not route.ctx.ignore_body
): ):
if hasattr(handler, "is_stream"): if hasattr(handler, "is_stream"):
# Streaming handler: lift the size limit # Streaming handler: lift the size limit
request.stream.request_max_size = float("inf") request.stream.request_max_size = float( # type: ignore
"inf"
)
else: else:
# Non-streaming handler: preload body # Non-streaming handler: preload body
await request.receive_body() await request.receive_body()
@ -730,8 +726,7 @@ class Sanic(BaseSanic):
if response: if response:
response = await request.respond(response) response = await request.respond(response)
else: else:
if request.stream: response = request.stream.response # type: ignore
response = request.stream.response
# Make sure that response is finished / run StreamingHTTP callback # Make sure that response is finished / run StreamingHTTP callback
if isinstance(response, BaseHTTPResponse): if isinstance(response, BaseHTTPResponse):
@ -757,9 +752,9 @@ class Sanic(BaseSanic):
): ):
request.app = self request.app = self
if not getattr(handler, "__blueprintname__", False): if not getattr(handler, "__blueprintname__", False):
request.endpoint = handler.__name__ request._name = handler.__name__
else: else:
request.endpoint = ( request._name = (
getattr(handler, "__blueprintname__", "") + handler.__name__ getattr(handler, "__blueprintname__", "") + handler.__name__
) )

View File

@ -86,15 +86,14 @@ class Request:
"_remote_addr", "_remote_addr",
"_socket", "_socket",
"_match_info", "_match_info",
"_name",
"app", "app",
"body", "body",
"conn_info", "conn_info",
"ctx", "ctx",
"endpoint",
"head", "head",
"headers", "headers",
"method", "method",
"name",
"parsed_args", "parsed_args",
"parsed_not_grouped_args", "parsed_not_grouped_args",
"parsed_files", "parsed_files",
@ -106,7 +105,6 @@ class Request:
"route", "route",
"stream", "stream",
"transport", "transport",
"uri_template",
"version", "version",
) )
@ -124,6 +122,7 @@ class Request:
# TODO: Content-Encoding detection # TODO: Content-Encoding detection
self._parsed_url = parse_url(url_bytes) self._parsed_url = parse_url(url_bytes)
self._id: Optional[Union[uuid.UUID, str, int]] = None self._id: Optional[Union[uuid.UUID, str, int]] = None
self._name: Optional[str] = None
self.app = app self.app = app
self.headers = headers self.headers = headers
@ -136,7 +135,6 @@ class Request:
self.body = b"" self.body = b""
self.conn_info: Optional[ConnInfo] = None self.conn_info: Optional[ConnInfo] = None
self.ctx = SimpleNamespace() self.ctx = SimpleNamespace()
self.name: Optional[str] = None
self.parsed_forwarded: Optional[Options] = None self.parsed_forwarded: Optional[Options] = None
self.parsed_json = None self.parsed_json = None
self.parsed_form = None self.parsed_form = None
@ -147,12 +145,10 @@ class Request:
self.parsed_not_grouped_args: DefaultDict[ self.parsed_not_grouped_args: DefaultDict[
Tuple[bool, bool, str, str], List[Tuple[str, str]] Tuple[bool, bool, str, str], List[Tuple[str, str]]
] = defaultdict(list) ] = defaultdict(list)
self.uri_template: Optional[str] = None
self.request_middleware_started = False self.request_middleware_started = False
self._cookies: Optional[Dict[str, str]] = None self._cookies: Optional[Dict[str, str]] = None
self._match_info: Dict[str, Any] = {} self._match_info: Dict[str, Any] = {}
self.stream: Optional[Http] = None self.stream: Optional[Http] = None
self.endpoint: Optional[str] = None
self.route: Optional[Route] = None self.route: Optional[Route] = None
self._protocol = None self._protocol = None
@ -207,6 +203,22 @@ class Request:
if not self.body: if not self.body:
self.body = b"".join([data async for data in self.stream]) self.body = b"".join([data async for data in self.stream])
@property
def name(self):
if self._name:
return self._name
elif self.route:
return self.route.name
return None
@property
def endpoint(self):
return self.name
@property
def uri_template(self):
return f"/{self.route.path}"
@property @property
def protocol(self): def protocol(self):
if not self._protocol: if not self._protocol:

View File

@ -11,7 +11,6 @@ from sanic_routing.route import Route # type: ignore
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.exceptions import MethodNotSupported, NotFound, SanicException from sanic.exceptions import MethodNotSupported, NotFound, SanicException
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
from sanic.request import Request
ROUTER_CACHE_SIZE = 1024 ROUTER_CACHE_SIZE = 1024
@ -27,16 +26,11 @@ class Router(BaseRouter):
DEFAULT_METHOD = "GET" DEFAULT_METHOD = "GET"
ALLOWED_METHODS = HTTP_METHODS ALLOWED_METHODS = HTTP_METHODS
# Putting the lru_cache on Router.get() performs better for the benchmarsk
# at tests/benchmark/test_route_resolution_benchmark.py
# However, overall application performance is significantly improved
# with the lru_cache on this method.
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def _get( def _get(
self, path, method, host self, path: str, method: str, host: Optional[str]
) -> Tuple[Route, RouteHandler, Dict[str, Any]]: ) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
try: try:
route, handler, params = self.resolve( return self.resolve(
path=path, path=path,
method=method, method=method,
extra={"host": host}, extra={"host": host},
@ -50,14 +44,9 @@ class Router(BaseRouter):
allowed_methods=e.allowed_methods, allowed_methods=e.allowed_methods,
) )
return ( @lru_cache(maxsize=ROUTER_CACHE_SIZE)
route,
handler,
params,
)
def get( # type: ignore def get( # type: ignore
self, request: Request self, path: str, method: str, host: Optional[str]
) -> Tuple[Route, RouteHandler, Dict[str, Any]]: ) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
""" """
Retrieve a `Route` object containg the details about how to handle Retrieve a `Route` object containg the details about how to handle
@ -69,9 +58,7 @@ class Router(BaseRouter):
correct response correct response
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]] :rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
""" """
return self._get( return self._get(path, method, host)
request.path, request.method, request.headers.get("host")
)
def add( # type: ignore def add( # type: ignore
self, self,

View File

@ -23,18 +23,21 @@ class TestSanicRouteResolution:
) )
router, simple_routes = sanic_router(route_details=simple_routes) router, simple_routes = sanic_router(route_details=simple_routes)
route_to_call = choice(simple_routes) route_to_call = choice(simple_routes)
request = Request(
result = benchmark.pedantic(
router.get,
(
Request(
"/{}".format(route_to_call[-1]).encode(), "/{}".format(route_to_call[-1]).encode(),
{"host": "localhost"}, {"host": "localhost"},
"v1", "v1",
route_to_call[0], route_to_call[0],
None, None,
None, None,
), )
result = benchmark.pedantic(
router.get,
(
request.path,
request.method,
request.headers.get("host"),
), ),
iterations=1000, iterations=1000,
rounds=1000, rounds=1000,
@ -56,18 +59,21 @@ class TestSanicRouteResolution:
) )
print("{} -> {}".format(route_to_call[-1], url)) print("{} -> {}".format(route_to_call[-1], url))
request = Request(
result = benchmark.pedantic(
router.get,
(
Request(
"/{}".format(url).encode(), "/{}".format(url).encode(),
{"host": "localhost"}, {"host": "localhost"},
"v1", "v1",
route_to_call[0], route_to_call[0],
None, None,
None, None,
), )
result = benchmark.pedantic(
router.get,
(
request.path,
request.method,
request.headers.get("host"),
), ),
iterations=1000, iterations=1000,
rounds=1000, rounds=1000,

View File

@ -35,6 +35,27 @@ def test_request_id_defaults_uuid():
assert request.id == request.id == request._id assert request.id == request.id == request._id
def test_name_none():
request = Request(b"/", {}, None, "GET", None, None)
assert request.name is None
def test_name_from_route():
request = Request(b"/", {}, None, "GET", None, None)
route = Mock()
request.route = route
assert request.name == route.name
def test_name_from_set():
request = Request(b"/", {}, None, "GET", None, None)
request._name = "foo"
assert request.name == "foo"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"request_id,expected_type", "request_id,expected_type",
( (

View File

@ -166,7 +166,9 @@ def test_matching(path, headers, expected):
request = Request(path, headers, None, "GET", None, app) request = Request(path, headers, None, "GET", None, app)
try: try:
app.router.get(request=request) app.router.get(
request.path, request.method, request.headers.get("host")
)
except NotFound: except NotFound:
response = 404 response = 404
except Exception: except Exception: