Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perf improv #2074

Merged
merged 4 commits into from
Mar 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 10 additions & 15 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,27 +676,23 @@ async def handle_request(self, request: Request):
response = None
try:
# Fetch handler from router
(
route,
handler,
kwargs,
) = self.router.get(request)
route, handler, kwargs = self.router.get(
request.path, request.method, request.headers.get("host")
)

request._match_info = kwargs
request.route = route
request.name = route.name
request.uri_template = f"/{route.path}"
request.endpoint = request.name

if (
request.stream
and request.stream.request_body
request.stream.request_body # type: ignore
and not route.ctx.ignore_body
):

if hasattr(handler, "is_stream"):
# Streaming handler: lift the size limit
request.stream.request_max_size = float("inf")
request.stream.request_max_size = float( # type: ignore
"inf"
)
else:
# Non-streaming handler: preload body
await request.receive_body()
Expand Down Expand Up @@ -730,8 +726,7 @@ async def handle_request(self, request: Request):
if response:
response = await request.respond(response)
else:
if request.stream:
response = request.stream.response
response = request.stream.response # type: ignore
# Make sure that response is finished / run StreamingHTTP callback

if isinstance(response, BaseHTTPResponse):
Expand All @@ -757,9 +752,9 @@ async def _websocket_handler(
):
request.app = self
if not getattr(handler, "__blueprintname__", False):
request.endpoint = handler.__name__
request._name = handler.__name__
else:
request.endpoint = (
request._name = (
getattr(handler, "__blueprintname__", "") + handler.__name__
)

Expand Down
24 changes: 18 additions & 6 deletions sanic/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,14 @@ class Request:
"_remote_addr",
"_socket",
"_match_info",
"_name",
"app",
"body",
"conn_info",
"ctx",
"endpoint",
"head",
"headers",
"method",
"name",
"parsed_args",
"parsed_not_grouped_args",
"parsed_files",
Expand All @@ -106,7 +105,6 @@ class Request:
"route",
"stream",
"transport",
"uri_template",
"version",
)

Expand All @@ -124,6 +122,7 @@ def __init__(
# TODO: Content-Encoding detection
self._parsed_url = parse_url(url_bytes)
self._id: Optional[Union[uuid.UUID, str, int]] = None
self._name: Optional[str] = None
self.app = app

self.headers = headers
Expand All @@ -136,7 +135,6 @@ def __init__(
self.body = b""
self.conn_info: Optional[ConnInfo] = None
self.ctx = SimpleNamespace()
self.name: Optional[str] = None
self.parsed_forwarded: Optional[Options] = None
self.parsed_json = None
self.parsed_form = None
Expand All @@ -147,12 +145,10 @@ def __init__(
self.parsed_not_grouped_args: DefaultDict[
Tuple[bool, bool, str, str], List[Tuple[str, str]]
] = defaultdict(list)
self.uri_template: Optional[str] = None
self.request_middleware_started = False
self._cookies: Optional[Dict[str, str]] = None
self._match_info: Dict[str, Any] = {}
self.stream: Optional[Http] = None
self.endpoint: Optional[str] = None
self.route: Optional[Route] = None
self._protocol = None

Expand Down Expand Up @@ -207,6 +203,22 @@ async def receive_body(self):
if not self.body:
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
def protocol(self):
if not self._protocol:
Expand Down
23 changes: 5 additions & 18 deletions sanic/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from sanic.constants import HTTP_METHODS
from sanic.exceptions import MethodNotSupported, NotFound, SanicException
from sanic.models.handler_types import RouteHandler
from sanic.request import Request


ROUTER_CACHE_SIZE = 1024
Expand All @@ -27,16 +26,11 @@ class Router(BaseRouter):
DEFAULT_METHOD = "GET"
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(
self, path, method, host
self, path: str, method: str, host: Optional[str]
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
try:
route, handler, params = self.resolve(
return self.resolve(
path=path,
method=method,
extra={"host": host},
Expand All @@ -50,14 +44,9 @@ def _get(
allowed_methods=e.allowed_methods,
)

return (
route,
handler,
params,
)

@lru_cache(maxsize=ROUTER_CACHE_SIZE)
def get( # type: ignore
self, request: Request
self, path: str, method: str, host: Optional[str]
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
"""
Retrieve a `Route` object containg the details about how to handle
Expand All @@ -69,9 +58,7 @@ def get( # type: ignore
correct response
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
"""
return self._get(
request.path, request.method, request.headers.get("host")
)
return self._get(path, method, host)

def add( # type: ignore
self,
Expand Down
38 changes: 22 additions & 16 deletions tests/benchmark/test_route_resolution_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,21 @@ async def test_resolve_route_no_arg_string_path(
)
router, simple_routes = sanic_router(route_details=simple_routes)
route_to_call = choice(simple_routes)
request = Request(
"/{}".format(route_to_call[-1]).encode(),
{"host": "localhost"},
"v1",
route_to_call[0],
None,
None,
)

result = benchmark.pedantic(
router.get,
(
Request(
"/{}".format(route_to_call[-1]).encode(),
{"host": "localhost"},
"v1",
route_to_call[0],
None,
None,
),
request.path,
request.method,
request.headers.get("host"),
),
iterations=1000,
rounds=1000,
Expand All @@ -56,18 +59,21 @@ async def test_resolve_route_with_typed_args(
)

print("{} -> {}".format(route_to_call[-1], url))
request = Request(
"/{}".format(url).encode(),
{"host": "localhost"},
"v1",
route_to_call[0],
None,
None,
)

result = benchmark.pedantic(
router.get,
(
Request(
"/{}".format(url).encode(),
{"host": "localhost"},
"v1",
route_to_call[0],
None,
None,
),
request.path,
request.method,
request.headers.get("host"),
),
iterations=1000,
rounds=1000,
Expand Down
21 changes: 21 additions & 0 deletions tests/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ def test_request_id_defaults_uuid():
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(
"request_id,expected_type",
(
Expand Down
4 changes: 3 additions & 1 deletion tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ def handler(request):
request = Request(path, headers, None, "GET", None, app)

try:
app.router.get(request=request)
app.router.get(
request.path, request.method, request.headers.get("host")
)
except NotFound:
response = 404
except Exception:
Expand Down