Repository: strawberry-graphql/strawberry

Issue:
Slow performance in async execution because of DirectivesExtension
The use of DirectivesExtension in async graphql execution wraps every field of the result graph in an async function. This slows down performance. 
We have one resolver returning a huge list of objects where every field of the model is already filled with a native type (str, int ..). So there is no need for an asynchronous resolve of these model fields. 
But the DirectivesExtension calls `await_maybe` for each field of each model in the result list. Now there are thousands of async function calls which should be awaited. 
We don't use custom directives so I replaced the Schema.execute function (from strawberry.schema.schema.py). In this function I only register the DirectivesExtension if there are custom directives. For us this solves the problem, as long as we don't use custom directives. But I think it would be a modification that could also be merged in the strawberry sourcecode. 

Here is a demo script with the used patch, showing the dramatic performance improvement for this case. 

``` 
import asyncio
import time
import typing
from typing import Any, Dict, Optional

import strawberry
from strawberry import Schema
from strawberry.extensions.directives import DirectivesExtension
from strawberry.schema.execute import execute
from strawberry.types import ExecutionContext, ExecutionResult


@strawberry.type
class Book:
    author: str
    title: str


async def get_books():
    res = []
    await asyncio.sleep(0.1)
    for i in range(200000):
        res.append(
            Book(
                author="Unknown",
                title=f"Book nr {i}",
            )
        )
    return res


@strawberry.type
class DemoQuery:
    books: typing.List[Book] = strawberry.field(resolver=get_books)


async def test():
    start_time = time.time()
    schema = strawberry.Schema(query=DemoQuery)
    query = "{ books { title author } }"
    result = await schema.execute(
        query,
    )
    if result.errors:
        print(result.errors)

    print("complete", time.time() - start_time)


async def schema_execute(
    self,
    query: str,
    variable_values: Optional[Dict[str, Any]] = None,
    context_value: Optional[Any] = None,
    root_value: Optional[Any] = None,
    operation_name: Optional[str] = None,
) -> ExecutionResult:
    # Create execution context
    execution_context = ExecutionContext(
        query=query,
        schema=self,
        context=context_value,
        root_value=root_value,
        variables=variable_values,
        provided_operation_name=operation_name,
    )

    # BEGIN - CHANGED
    extensions = list(self.extensions)
    if len(self.directives) > 0:
        extensions += [DirectivesExtension]

    result = await execute(
        self._schema,  # pylint: disable=protected-access
        query,
        extensions=extensions,
        execution_context_class=self.execution_context_class,
        execution_context=execution_context,
    )
    # END - CHANGED

    if result.errors:
        self.process_errors(result.errors, execution_context=execution_context)

    return result


def apply_directives_patch():
    Schema.execute = schema_execute


def run():
    asyncio.run(test())


def run_with_patch():
    apply_directives_patch()
    asyncio.run(test())


if __name__ == "__main__":
    print("run")
    run()
    print("run with patch")
    run_with_patch()

``` 

On my Machine I get the following output:

```
run
complete 24.10178303718567
run with patch
complete 4.264432191848755
```



Discussion:
hi @QSHolzner, I'll take a look at this, ut I think it makes sense. I wonder if we can make it faster but still enable directives.

But in any case I think we can disable the extension when there's no custom directive added 
@patrick91 Thanks
Hello, we have witnessed the same issue and applied the same patch. This drastically reduce the time took by strawberry to serialize the response (divided by 4 or 5) but i still find it quite slow especially as the data we hand out to strawberry is basically already serialized - Do you see any other area of improvements in the near future ?

Resolving patch:
diff --git a/strawberry/schema/schema.py b/strawberry/schema/schema.py
--- a/strawberry/schema/schema.py
+++ b/strawberry/schema/schema.py
@@ -1,5 +1,5 @@
 from functools import lru_cache
-from typing import Any, Dict, Iterable, Optional, Sequence, Type, Union
+from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
 
 from graphql import (
     ExecutionContext as GraphQLExecutionContext,
@@ -119,6 +119,16 @@ def __init__(
 
         self.query = self.schema_converter.type_map[query_type.name]
 
+    def get_extensions(
+        self, sync: bool = False
+    ) -> List[Union[Type[Extension], Extension]]:
+        extensions = list(self.extensions)
+
+        if self.directives:
+            extensions.append(DirectivesExtensionSync if sync else DirectivesExtension)
+
+        return extensions
+
     @lru_cache()
     def get_type_by_name(  # type: ignore  # lru_cache makes mypy complain
         self, name: str
@@ -186,7 +196,7 @@ async def execute(
         result = await execute(
             self._schema,
             query,
-            extensions=list(self.extensions) + [DirectivesExtension],
+            extensions=self.get_extensions(),
             execution_context_class=self.execution_context_class,
             execution_context=execution_context,
             allowed_operation_types=allowed_operation_types,
@@ -221,7 +231,7 @@ def execute_sync(
         result = execute_sync(
             self._schema,
             query,
-            extensions=list(self.extensions) + [DirectivesExtensionSync],
+            extensions=self.get_extensions(sync=True),
             execution_context_class=self.execution_context_class,
             execution_context=execution_context,
             allowed_operation_types=allowed_operation_types,


Test patch:
diff --git a/tests/schema/test_get_extensions.py b/tests/schema/test_get_extensions.py
new file mode 100644
--- /dev/null
+++ b/tests/schema/test_get_extensions.py
@@ -0,0 +1,61 @@
+import strawberry
+from strawberry.directive import DirectiveLocation
+from strawberry.extensions import Extension
+from strawberry.extensions.directives import (
+    DirectivesExtension,
+    DirectivesExtensionSync,
+)
+
+
+@strawberry.type
+class Query:
+    example: str
+
+
+@strawberry.directive(locations=[DirectiveLocation.FIELD])
+def uppercase(value: str) -> str:
+    return value.upper()
+
+
+class MyExtension(Extension):
+    ...
+
+
+def test_returns_empty_list_when_no_custom_directives():
+    schema = strawberry.Schema(query=Query)
+
+    assert schema.get_extensions() == []
+
+
+def test_returns_extension_passed_by_user():
+    schema = strawberry.Schema(query=Query, extensions=[MyExtension])
+
+    assert schema.get_extensions() == [MyExtension]
+
+
+def test_returns_directives_extension_when_passing_directives():
+    schema = strawberry.Schema(query=Query, directives=[uppercase])
+
+    assert schema.get_extensions() == [DirectivesExtension]
+
+
+def test_returns_extension_passed_by_user_and_directives_extension():
+    schema = strawberry.Schema(
+        query=Query, extensions=[MyExtension], directives=[uppercase]
+    )
+
+    assert schema.get_extensions() == [MyExtension, DirectivesExtension]
+
+
+def test_returns_directives_extension_when_passing_directives_sync():
+    schema = strawberry.Schema(query=Query, directives=[uppercase])
+
+    assert schema.get_extensions(sync=True) == [DirectivesExtensionSync]
+
+
+def test_returns_extension_passed_by_user_and_directives_extension_sync():
+    schema = strawberry.Schema(
+        query=Query, extensions=[MyExtension], directives=[uppercase]
+    )
+
+    assert schema.get_extensions(sync=True) == [MyExtension, DirectivesExtensionSync]