Alternative approach to lazy type loading #1046
Replies: 4 comments
-
In order to have solid grounds for this discussion, let's disambiguate the terminology. I consider a type loader to be a user defined function with the signature In the latter half of your post, you seem to refer to the method Please reformulate your proposal by clearly disambiguating between the configuration option
You can combine both
It would not be too difficult to wire up multiple separate type loaders into one. function combinedTypeLoader(string $name): ?Type {
foreach ($typeLoaders as $typeLoader) {
if ($type = $typeLoader($name)) return $type;
}
return null;
}
The |
Beta Was this translation helpful? Give feedback.
-
To clarify why I referred to The signature of the user defined type loader is indeed simple. An implementation of an aggregate loader is also quite trivial, as you have shown. However, I think the userland implementation of a type loader is anything but simple. It requires a significant amount of code which mirrors the type definition elsewhere, violating the DRY principle. This is obviously the trade-off that the current solution using the type loader makes. And that is absolutely fine, the benefit is there. But I do not think it is necessary.
I propose to remove the calls of Entirely removing those calls is unlikely to be an option, but it would be nice to have the option available in production/non-debug. If it helps, I've attached a patch with the very rough direction I'm thinking of.
This is probably the core point of this discussion. I'm not convinced this is necessary. The getType method on the schema is absolutely vital when doing schema introspection. But during a "normal" query all the necessary types are already available! The types are already directly defined on their parent type. We would miss out on the "proper resolution" assertion, but I'm hoping it is enough for those to be part of the static assertion in |
Beta Was this translation helpful? Give feedback.
-
We can remove some redundant checks at runtime and minimize the calls to query ($foo: Foo <-- has to be looked up by name) { ... } |
Beta Was this translation helpful? Give feedback.
-
Good catch. Though it would probably be acceptable to remove that validation in production if you are not worried about creating nice error message for those using the api. There are some other usages where I'm currently thinking it should be possible to change their logic to work with the available type object (fragment matching in resolvers). I think it is possible, but any performance bugs from this proposal will be hard to catch. I also think that a method of statically definining implementors on an abstract type will become a necessity. Do you see any way this approach would work out? I think it would help us and other users which are executing queries like this (would it be useful to demonstrate this through a screen sharing session?). Otherwise we'll just have to do the alternative using the current mechanism of the type loader. |
Beta Was this translation helpful? Give feedback.
-
Hey,
Our current graphql endpoint using webonyx/graphql-php makes use of a large (partially generated) schema (1k types, 10k fields) using the standard approach where types are recursively defined. This has become a performance problem because of the initialization that happens on every request. We initialize the schema on every query, the graphql endpoint could run as a server ofcourse, but that has other challenges.
The suggested solution of using a type loader in this scenario is not great.
Some developer-friendly solutions can be created to help with the second point. Different parts of the schema could be made responsible for loading different types. But this does not help with the first point and there is little help from the library as the type loader is not standardized. Understandable, because the current implementation takes the run-as-server case as a starting point where lazy type loading plays no role.
Looking through the code (which I'm quite impressed by, thanks!), I noticed that the use of the type loader is limited. Within a regular query (so no schema/type introspection) there are only a couple of usages. This makes me hopeful that we may be able to find a way to lazily load the necessary types during most queries without the use of the type loader. For those cases where it's still necessary, the user supplied loader would still be useful. This would remove the complexity that implementing a type loader in userland would have.
Executor\ReferenceExecutor::completeValue
) is a check that the type is the exact same object as the one currently available through the schema with its type loader. This is only relevant during development. I believe this runtime assertion can be removed as it is also duplicated within the schema validator step which can be executed statically.Executor\ReferenceExecutor::ensureValidRuntimeType
, called fromcompleteAbstractValue
) loads a type if it is referenced as a string. When this occurs it is not strange that the type is loaded. The current documentation does not mention that types are (only when resolved in interfaces?) able to be referenced by their name, but it probably happens.Type\Definition\QueryPlan
class. I'm not familiar with this code, but I think it's reasonable to expect that it will cause the type loader to be used.In a POC I've been able to replicate the benchmark numbers for the HugeSchemaBench::benchSmallQueryLazy benchmark without the use of a custom type loader.
My conclusion: It should be possible to remove the usages of the type loader within common queries. This would drastically improve the performance without the hit on developer experience in the situtation where the graphql endpoint is not used as a server process. However, this would lose some runtime checks which are only partially replaced by an existing static check. The method of resolving concrete types from abstract types/interfaces is especially problematic, but I expect it can be done without breaking backwards compatibility. Though it may be beneficial to consider making this resolve step statically available without runtime checks in the future (but this would break consistency with the graphql-js reference implementation).
Would it be useful to pursue this? I can turn my POC into a PR, but it's not entirely trivial and would have some impact for which it would be nice to have some support.
Beta Was this translation helpful? Give feedback.
All reactions