From 55c5f1fc16e61f576f05d31c1f9bbd324943729c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Mar 2025 12:01:20 -1000 Subject: [PATCH 1/2] Add benchmark for JSON post requests that check the content type (#10553) --- tests/test_benchmarks_client.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py index ac3131e9750..ae89bc1f667 100644 --- a/tests/test_benchmarks_client.py +++ b/tests/test_benchmarks_client.py @@ -319,3 +319,30 @@ async def run_client_benchmark() -> None: @benchmark def _run() -> None: loop.run_until_complete(run_client_benchmark()) + + +def test_one_hundred_json_post_requests( + loop: asyncio.AbstractEventLoop, + aiohttp_client: AiohttpClient, + benchmark: BenchmarkFixture, +) -> None: + """Benchmark 100 JSON POST requests that check the content-type.""" + message_count = 100 + + async def handler(request: web.Request) -> web.Response: + _ = request.content_type + _ = request.charset + return web.Response() + + app = web.Application() + app.router.add_route("POST", "/", handler) + + async def run_client_benchmark() -> None: + client = await aiohttp_client(app) + for _ in range(message_count): + await client.post("/", json={"key": "value"}) + await client.close() + + @benchmark + def _run() -> None: + loop.run_until_complete(run_client_benchmark()) From 44e669be1ab1a60c40183f92f172670d912cb834 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Mar 2025 12:56:28 -1000 Subject: [PATCH 2/2] Cache parsing of the content-type (#10552) --- CHANGES/10552.misc.rst | 1 + aiohttp/helpers.py | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 CHANGES/10552.misc.rst diff --git a/CHANGES/10552.misc.rst b/CHANGES/10552.misc.rst new file mode 100644 index 00000000000..6755cbf7396 --- /dev/null +++ b/CHANGES/10552.misc.rst @@ -0,0 +1 @@ +Improved performance of parsing content types by adding a cache in the same manner currently done with mime types -- by :user:`bdraco`. diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index e53647e274c..22a459586c7 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -24,7 +24,7 @@ from http.cookies import SimpleCookie from math import ceil from pathlib import Path -from types import TracebackType +from types import MappingProxyType, TracebackType from typing import ( TYPE_CHECKING, Any, @@ -367,6 +367,20 @@ def parse_mimetype(mimetype: str) -> MimeType: ) +@functools.lru_cache(maxsize=56) +def parse_content_type(raw: str) -> Tuple[str, MappingProxyType[str, str]]: + """Parse Content-Type header. + + Returns a tuple of the parsed content type and a + MappingProxyType of parameters. + """ + msg = HeaderParser().parsestr(f"Content-Type: {raw}") + content_type = msg.get_content_type() + params = msg.get_params(()) + content_dict = dict(params[1:]) # First element is content type again + return content_type, MappingProxyType(content_dict) + + def guess_filename(obj: Any, default: Optional[str] = None) -> Optional[str]: name = getattr(obj, "name", None) if name and isinstance(name, str) and name[0] != "<" and name[-1] != ">": @@ -733,10 +747,10 @@ def _parse_content_type(self, raw: Optional[str]) -> None: self._content_type = "application/octet-stream" self._content_dict = {} else: - msg = HeaderParser().parsestr("Content-Type: " + raw) - self._content_type = msg.get_content_type() - params = msg.get_params(()) - self._content_dict = dict(params[1:]) # First element is content type again + content_type, content_mapping_proxy = parse_content_type(raw) + self._content_type = content_type + # _content_dict needs to be mutable so we can update it + self._content_dict = content_mapping_proxy.copy() @property def content_type(self) -> str: