Repository: strawberry-graphql/strawberry

Issue:
Slow performance for queries that return many items
Hello, I'm trying to improve the performance of a graphql query that looks like:

```graphql
query GetSampleEidMapQuery($project: String!) {
  project(name: $project) {
    samples {
      assays {
        id
        externalIds
        meta
      }
    }
  }
}
```

This returns a result that has around 3000 samples, and each sample has between 1 and 4 assays. So less than 10,000 objects in total. The query takes between 3 and 5 seconds and returns around 600kB of json. So it's not a small amount of data but also not exactly huge. I initially thought this might be slow SQL queries but it turns out around 85% of the query time is in strawberry processing the results. Here's the pyinstrument profiling that shows this [pyinstrument.html.zip](https://github.com/user-attachments/files/15923872/pyinstrument.html.zip)

Is there anything that can be done to reduce the time that it takes for strawberry to handle results? I've tried both the `ParserCache` and `ValidationCache` as well as disabling validation entirely but unfortunately that made very little difference. 

Not sure if it helps but this is our graphql schema: https://github.com/populationgenomics/metamist/blob/dev/api/graphql/schema.py

Thank you!




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



Discussion:
Hey dan, thanks for the report. This is a known problem in the GraphQL reference implementation and we're actively investigating ways to fix this. If you have any specific ideas, we'd highly appreciate any input.
Here are some related issues:
- https://github.com/graphql-python/graphql-core/issues/190
Hi Erik, thanks for the quick response. It does sound like a tricky one to solve! Feel free to close this if you'd like as it sounds like the issue isn't within strawberry and it is a well known issue in graphql-core. 
Actually, sorry, I've just had another look at the profiling I included in the issue, and it does seem like the majority of the time is spent within strawberry, particularly it seems like lots of time is spent in inferring the schema from the types? But I could well be reading the profiling wrong, I'm pretty new to python profiling. 

<img width="1326" alt="image" src="https://github.com/strawberry-graphql/strawberry/assets/1946998/d0225de2-79c3-4a8d-98c0-304b826d320c">

I've narrowed this down further to our usage of a input that has a generic type on the assays field on a sample. Changing this line https://github.com/populationgenomics/metamist/blob/dev/api/graphql/schema.py#L754 to use a non generic type cuts the execution time from 5 seconds down to 1
Sorry to spam messages, I've made a minimal repro repo to help show the issue:
https://github.com/dancoates/strawberry-generic-input-repro

Resolving patch:
diff --git a/strawberry/annotation.py b/strawberry/annotation.py
--- a/strawberry/annotation.py
+++ b/strawberry/annotation.py
@@ -54,7 +54,7 @@
 
 
 class StrawberryAnnotation:
-    __slots__ = "raw_annotation", "namespace", "__eval_cache__"
+    __slots__ = "raw_annotation", "namespace", "__resolve_cache__"
 
     def __init__(
         self,
@@ -65,7 +65,7 @@ def __init__(
         self.raw_annotation = annotation
         self.namespace = namespace
 
-        self.__eval_cache__: Optional[Type[Any]] = None
+        self.__resolve_cache__: Optional[Union[StrawberryType, type]] = None
 
     def __eq__(self, other: object) -> bool:
         if not isinstance(other, StrawberryAnnotation):
@@ -101,19 +101,17 @@ def annotation(self) -> Union[object, str]:
     def annotation(self, value: Union[object, str]) -> None:
         self.raw_annotation = value
 
+        self.__resolve_cache__ = None
+
     def evaluate(self) -> type:
         """Return evaluated annotation using `strawberry.util.typing.eval_type`."""
-        evaled_type = self.__eval_cache__
-        if evaled_type:
-            return evaled_type
-
         annotation = self.raw_annotation
+
         if isinstance(annotation, str):
             annotation = ForwardRef(annotation)
 
         evaled_type = eval_type(annotation, self.namespace, None)
 
-        self.__eval_cache__ = evaled_type
         return evaled_type
 
     def _get_type_with_args(
@@ -131,6 +129,12 @@ def _get_type_with_args(
 
     def resolve(self) -> Union[StrawberryType, type]:
         """Return resolved (transformed) annotation."""
+        if self.__resolve_cache__ is None:
+            self.__resolve_cache__ = self._resolve()
+
+        return self.__resolve_cache__
+
+    def _resolve(self) -> Union[StrawberryType, type]:
         evaled_type = cast(Any, self.evaluate())
 
         if is_private(evaled_type):
@@ -172,7 +176,7 @@ def set_namespace_from_field(self, field: StrawberryField) -> None:
         module = sys.modules[field.origin.__module__]
         self.namespace = module.__dict__
 
-        self.__eval_cache__ = None  # Invalidate cache to allow re-evaluation
+        self.__resolve_cache__ = None  # Invalidate cache to allow re-evaluation
 
     def create_concrete_type(self, evaled_type: type) -> type:
         if has_object_definition(evaled_type):
diff --git a/strawberry/arguments.py b/strawberry/arguments.py
--- a/strawberry/arguments.py
+++ b/strawberry/arguments.py
@@ -129,7 +129,6 @@ def type(self) -> Union[StrawberryType, type]:
 
     @property
     def is_graphql_generic(self) -> bool:
-        # TODO: double check this
         from strawberry.schema.compat import is_graphql_generic
 
         return is_graphql_generic(self.type)


Test patch: