Repository: aio-libs/aiohttp
Issue category: Performance Issue

Issue:
RequestInfo is documented to be a namedtuple but its actually a dataclass or attrs
https://docs.aiohttp.org/en/stable/client_reference.html#aiohttp.ClientResponse.request_info

It would be about 2x as fast to create if it were a namedtuple though, and although dataclasses are faster to access its rarely accessed so namedtuple probably is a better choice.  frozen dataclasses are also slower to create https://docs.python.org/3.9/library/dataclasses.html#frozen-instances.... and we create them for every request.
> There is a tiny performance penalty when using frozen=True: [__init__()](https://docs.python.org/3.9/reference/datamodel.html#object.__init__) cannot use simple assignment to initialize fields, and must use [object.__setattr__()](https://docs.python.org/3.9/reference/datamodel.html#object.__setattr__).

Additionally the attrs default isn't used on 3.11/3.10 at all

https://github.com/aio-libs/aiohttp/blob/13dc0200d855bdfec1d4c4f3e1c9a1c66f88f1eb/aiohttp/client_reqrep.py#L110

```diff
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
index dad7fe78e..1eeaf7068 100644
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -105,16 +105,12 @@ class ContentDisposition:
     filename: Optional[str]
 
 
-@attr.s(auto_attribs=True, frozen=True, slots=True)
-class RequestInfo:
+class RequestInfo(NamedTuple):
     url: URL
     method: str
     headers: "CIMultiDictProxy[str]"
-    real_url: URL = attr.ib()
+    real_url: URL
 
-    @real_url.default
-    def real_url_default(self) -> URL:
-        return self.url
 
 
 class Fingerprint:
@@ -401,7 +397,7 @@ class ClientRequest:
     @property
     def request_info(self) -> RequestInfo:
         headers: CIMultiDictProxy[str] = CIMultiDictProxy(self.headers)
-        return RequestInfo(self.url, self.method, headers, self.original_url)
+        return tuple.__new__(RequestInfo, (self.url, self.method, headers, self.original_url))
 
     def update_host(self, url: URL) -> None:
         """Update destination host, port and connection type (ssl)."""
```


Hints:
<img width="1500" alt="Screenshot 2024-11-05 at 8 42 07 PM" src="https://github.com/user-attachments/assets/92add3df-591f-4bfd-a138-db724e9414b6">

Production shows quite a bit of time making this one
I really doubt if we have the real bottleneck here. Attrs/dataclasses have better human-readable interface than namedtuple, that's why I accepted the current code.

Technically every namedtuple <-> attrs <-> dataclasses change is not 100% backward compatible, that's why I'm not super exciting in mutation of such things. It may slightly break some existing code, which is not good.
> Technically every namedtuple <-> attrs <-> dataclasses change is not 100% backward compatible, that's why I'm not super exciting in mutation of such things. It may slightly break some existing code, which is not good.

I think v4 is already migrating from attrs to dataclasses though. So, if we want to make any other changes, we should be able to get them in before the v4 release.
This keeps coming up on production profiles so there is something there. It needs to be benchmarked and quantified first.
One thing to keep in mind is all the profiles I see it on are 3.10 and 3.11 beta so it could not be concern on master 

Patch:
diff --git a/CHANGES/9692.breaking.rst b/CHANGES/9692.breaking.rst
new file mode 100644
index 00000000000..e0fdae11416
--- /dev/null
+++ b/CHANGES/9692.breaking.rst
@@ -0,0 +1,1 @@
+Changed ``ClientRequest.request_info`` to be a `NamedTuple` to improve client performance -- by :user:`bdraco`.
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
index 9c86d935011..59a191d19fe 100644
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -106,8 +106,7 @@ class ContentDisposition:
     filename: Optional[str]
 
 
-@dataclasses.dataclass(frozen=True)
-class RequestInfo:
+class RequestInfo(NamedTuple):
     url: URL
     method: str
     headers: "CIMultiDictProxy[str]"
@@ -330,7 +329,11 @@ def port(self) -> Optional[int]:
     @property
     def request_info(self) -> RequestInfo:
         headers: CIMultiDictProxy[str] = CIMultiDictProxy(self.headers)
-        return RequestInfo(self.url, self.method, headers, self.original_url)
+        # These are created on every request, so we use a NamedTuple
+        # for performance reasons.
+        return tuple.__new__(
+            RequestInfo, (self.url, self.method, headers, self.original_url)
+        )
 
     def update_host(self, url: URL) -> None:
         """Update destination host, port and connection type (ssl)."""
diff --git a/docs/client_reference.rst b/docs/client_reference.rst
index f9f8ac28617..dfe93bda0d9 100644
--- a/docs/client_reference.rst
+++ b/docs/client_reference.rst
@@ -1467,7 +1467,7 @@ Response object
 
    .. attribute:: request_info
 
-       A namedtuple with request URL and headers from :class:`~aiohttp.ClientRequest`
+       A :class:`typing.NamedTuple` with request URL and headers from :class:`~aiohttp.ClientRequest`
        object, :class:`aiohttp.RequestInfo` instance.
 
    .. method:: get_encoding()
@@ -1824,7 +1824,7 @@ Utilities
 
 .. class:: RequestInfo()
 
-   A data class with request URL and headers from :class:`~aiohttp.ClientRequest`
+   A :class:`typing.NamedTuple` with request URL and headers from :class:`~aiohttp.ClientRequest`
    object, available as :attr:`ClientResponse.request_info` attribute.
 
    .. attribute:: url


Test patch: