Repository: strawberry-graphql/strawberry

Issue:
Increased CPU usage when subscribing with the graphql-transport-ws protocol
<!-- Provide a general summary of the bug in the title above. -->

<!--- This template is entirely optional and can be removed, but is here to help both you and us. -->
<!--- Anything on lines wrapped in comments like these will not show up in the final text. -->

## Describe the Bug
We have a Strawberry GraphQL server that we have been stress testing and running CPU performance tests on. We have found that there is a noticeable and consistent increase in the CPU usage of our server application when our client subscribes using the _graphql-transport-ws_ protocol compared to using the _graphql-ws_ protocol. 

I have done a bit of investigating and further profiling using py-spy and discovered that the Strawberry code is creating a `NextMessage` object ([here](https://github.com/strawberry-graphql/strawberry/blob/db9c22a53205cd82330a9c84d44ac1ee2731eafb/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py#L261)) for each message, which it then converts to a dictionary ([here](https://github.com/strawberry-graphql/strawberry/blob/db9c22a53205cd82330a9c84d44ac1ee2731eafb/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py#L283)) using the `dataclasses` `asdict() `method ([here](https://github.com/strawberry-graphql/strawberry/blob/db9c22a53205cd82330a9c84d44ac1ee2731eafb/strawberry/subscriptions/protocols/graphql_transport_ws/types.py#L12)). Some internet research shows that this `asdict()` method is doing a `deepcopy` of everything within the class. I ran a few timing tests and the `asdict()` method takes an order of magnitude longer than doing a simple `.__dict__` on the object. This is only done in the _graphql-transport-ws_ implementation and not the _graphql-ws_ implementation which explains why there is a difference in CPU usage between the 2 protocols.

I do not believe that we need to be doing a deepcopy when turning the class into a dictionary. What's more, I wonder whether we need to even be creating the `NextMessage` object because as far as I can see, we create it and pass it to a function that immediately turns it into a dictionary. So why don't we just create it as a dictionary and send it instead. This would bypass having to do any sort of conversion costing time. 

I.e. instead of line 261 and 262 ([here](https://github.com/strawberry-graphql/strawberry/blob/db9c22a53205cd82330a9c84d44ac1ee2731eafb/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py#L261)) which do:
```
    next_message = NextMessage(id=operation_id, payload=next_payload)
    await self.send_message(next_message)`
```
we could do something like:
```
    next_message = {"id":operation_id, "payload": next_payload, "type": "next"}
    await self.send_json(next_message)
```

When I ran the performance tests with the above change the CPU usage dropped and was consistent with the _graphql-ws_ protocol performance.
<!-- A clear and concise description of what the bug is. -->

## System Information

 - Operating system: Centos 7
 - Strawberry version (if applicable): 0.154.1

## Additional Context
I have created a simple demo Strawberry GraphQL server and Python client on GitHub, available at: https://github.com/rjwills28/strawberry_cpu_demo/tree/master.
Instructions on how to install and run are in the readme. It simulates the tests that we were running where we have a server providing subscription updates at 10Hz and a client that creates 100 different subscriptions. Follow the example in the readme to first run with the _graphql-ws_ protocol (command line argument (`-p 1`) and then with the _graphql-transport-ws_ protocol (`-p 2`). Run both a few times and you should see that the average CPU usage is on the whole higher for the latter protocol. Please let me know if you have any problems running this. 


<!-- POLAR PLEDGE BADGE START -->
## Upvote & Fund

- We're using [Polar.sh](https://polar.sh/strawberry-graphql) so you can upvote and help fund this issue.
- We receive the funding once the issue is completed & confirmed by you.
- Thank you in advance for helping prioritize & fund our backlog.

<a href="https://polar.sh/strawberry-graphql/strawberry/issues/2479">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/strawberry-graphql/strawberry/issues/2479/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/strawberry-graphql/strawberry/issues/2479/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
I am happy to collaborate on this and put in a PR with a suggested fix if you think applicable.
I haven't investigated it yet, but I'm totally in favour of replacing the class with a dictionary, maybe we can use a TypedDict instead of dataclass (we wouldn't need to change much code with that)

Thanks for the great write up! I'll be happy to review and test the PR.

Do you we could also get a benchmark for https://github.com/strawberry-graphql/benchmarks/blob/main/benchmarks/benchmarks.py ?

It would be neat to track this there too!

Resolving patch:
diff --git a/strawberry/subscriptions/protocols/graphql_transport_ws/types.py b/strawberry/subscriptions/protocols/graphql_transport_ws/types.py
--- a/strawberry/subscriptions/protocols/graphql_transport_ws/types.py
+++ b/strawberry/subscriptions/protocols/graphql_transport_ws/types.py
@@ -85,6 +85,9 @@ class NextMessage(GraphQLTransportMessage):
     payload: Dict[str, Any]  # TODO: shape like ExecutionResult
     type: str = "next"
 
+    def as_dict(self) -> dict:
+        return {"id": self.id, "payload": self.payload, "type": self.type}
+
 
 @dataclass
 class ErrorMessage(GraphQLTransportMessage):


Test patch: