-
Notifications
You must be signed in to change notification settings - Fork 2k
Introduce planning phase to query execution #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…utionContext type can be exported, enabling plan building to be a separate module.
…at is only used in the planning phase
… Plans, completed select support
…ing execution plans
…ve of what it is. Frees up ExecutionPlan for more generic uses
…rameter, which is also on the plan
Off the top of my head, maybe union and interface should be treated differently? In an interface, the selection plan isn't really different, except possibly for returnType, which I'm confused about anyway in this case. Maybe the typeChoices for a union type can be restricted based on what is discovered during the collectFields phase? |
A list of
Actually, unions are not easier than interfaces. Consider a schema: union Node = T1 | T2 | <...> | T10
type T1 { id: ID!, child: Node }
type T2 { id: ID!, child: Node }
<...>
type T10 { id: ID!, child: Node }
type Query { root: Node } And a query: query {
root { ...level1 }
}
fragment level1 on Node {
... on T1 { child { ...level2 } }
... on T2 { child { ...level2 } }
<...>
... on T10 { child { ...level2 } }
}
fragment level2 on Node {
... on T1 { child { ...level3 } }
... on T2 { child { ...level3 } }
<...>
... on T10 { child { ...level3 } }
}
<...>
fragment level10 on Node {
... on T1 { id }
... on T2 { id }
<...>
... on T10 { id }
} Here again, without memoization and laziness, we have to calculate and store in RAM more than 1010=10,000,000,000 plan nodes...
This won't solve the problem, because selection plans for skipped types would be empty anyway. Besides, it might be useful to be able to get a selection plan for a possible type even if it is empty. |
Unfortunately this is a common case. For example, Relay apps require many types to implement an |
I'm convinced, the plan size needs to be proportional to query complexity. I think this is doable but will be conceptually more complex. |
@JeffRMoore I'm curious about the performance metrics you quoted, and would love to see what the resolve functions look like that you wrote for it. Do you have a repo for that where I could reproduce them? I'm guessing these were for some flavor of SQL? And finally, sorry for the barrage of questions, but this looks really interesting and I want to make sure I understand it. |
@helfer At this point you should disregard the actual metrics reported. I've abandoned my original methodology. My tests were not against any particular backend. Those times were only for the time consumed by the execute process itself. I used a memory generated data structure. You can see my current work here: https://github.com/JeffRMoore/graphql-js/tree/benchmarks I'm guessing it will be ready to submit by end of next weekend. I'm standing in front of a herd of naked yaks on this one. Ended up writing a performance regression test library, which is why I've been quiet here. https://github.com/JeffRMoore/async-benchmark-runner The main difference in performance stems largely from the current implementation having to re-calculate collectFields on every iteration through the list. |
@JeffRMoore looked a bit at this PR. Am i right in saying that the core idea here is to "inline" fragments, variables and interfaces into the AST so that resolvers don't have to deal with that (and maybe provide the fieldAST in a slightly more digestible form)? |
@ruslantalpa Instead of inlining into the AST, I would say that the idea is to introduce a separate intermediate representation that is tailored to the process of resolving (an execution plan). The idea is absolutely that resolve functions should not have to digest fieldASTs and fragments, and such. |
@JeffRMoore Found this proposal not too long ago and this seems an addition really useful to some of our use-cases. What is the current state of the PR? Not too easily to figure out through this discussion. |
Would love to see this in the main codebase! Any updates? |
Sorry, got distracted, will continue to be for at least two more weeks. Still planning to return to this. |
Thanks for all the great work! Would also love to see this shipped, as it would definitely help us! |
I'm going to go ahead and close this out for the following reasons:
Thanks for your attention. Regards. |
Thanks again for your investigation, Jeff! Even though this wasn't merged
it helped identify the future direction of related changes.
…On Sat, Feb 4, 2017 at 10:27 AM Jeff Moore ***@***.***> wrote:
Closed #304 <#304>.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#304 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AADD0sAKBf3AAuidSa7WgRY-Fy359ojiks5rZMMhgaJpZM4HpJUr>
.
|
This PR is intended to address issue #26 and improve developer experience around writing resolving functions.
When writing resolving functions, it can be useful to have foreknowledge of how the execution
engine will evaluate the query. This might allow the resolve function to request only the necessary
fields or to optimistically apply joins.
Currently, applying these optimizations means processing the query AST in a way parallel to
how the execution engine might do so. One must understand aliasing, fragments and directives
and the structure of the AST.
This PR exposes query evaluation foreknowledge by splitting execution into two phases,
a planning phase and an evaluation phase.
In the planning phase the AST in analyzed and a heirarchical plan structure is created indicating
how the executor will evaluate the query. The structure of this plan mirrors the structure of the schema,
not the structure of the query.
This planning information takes the place of
GraphQLResolvingInfo
in the callsto resolving functions on the schema.
Pre-calculating this information serves two purposes:
Examples
A set of examples are available here for various resolver function cases:
https://github.com/JeffRMoore/graphql-optimization-examples
Simple Example
from https://github.com/JeffRMoore/graphql-optimization-examples/blob/master/ex1.js
For this query on a user object
Here is part of a resolver function on a user field of the root query
Which produces the following output when executed
This shows how a resolve function can use the plan structure to perform a selection
of only the fields that will be accessed later.
Complex Example
from https://github.com/JeffRMoore/graphql-optimization-examples/blob/master/ex6.js
For this query on a user object with a nested location object
Here is part of a resolver function on a user field of the root query
Which produces the following output when executed
Here, we are looking ahead into the location field of user to see what fields
of location will be queried.
We have to use forEach to do this because any given field could be resolved
multiple times by the execution engine. A field can be resolved multiple times
with different arguments or multiple times with the same arguments. The execution
engine cannot make presumptions about the return value being the same or different.
(The resolver could literally be return random numbers.)
This resolver could collect the nested field set and pass a query to a backend
document store. Or it could use the location information to add a join clause to
master query for user.
The Planning Data Structures
GraphQLResolvingPlan
The
GraphQLResolvingPlan
structure describes the process of calling theresolve
function toresolve the value of a field on a
GraphQLObjectType
parent.The
GraphQLResolvingPlan
data structure will be passed to theresolve
function as thethird parameter,
info
.The first accessible plan data structure during query execution will be a
GraphQLResolvingPlan
when
resolve
is called on the fields of the rootGraphQLObjectType
.(There is a
GraphQLOperationPlan
that describes the operation, but it is not accessibleto functions attached to the schema.)
GraphQLResolvingPlan
contains the following fields as well as the"All Fields" and "GraphQLResolveInfo" fields described later.
kind
string
GraphQLResolvingPlan
fieldDefinition
GraphQLFieldDefinition
args
{ [key: string]: mixed }
resolve
returned
GraphQLCompletionPlan
resolve
the
returned
field describes the next step the execution engine will take dependingon the type of the field on
GraphQLObjectType
.GraphQLSerializationPlan
GraphQLScalarType
andGraphQLEnumType
fields the return value will be serialized.GraphQLMappingPlan
GraphQLListType
the return value will be mapped, elements of the list will be further processed.GraphQLSelectionPlan
GraphQLObjectType
fields will have its fields selected.GraphQLCoercionPlan
GraphQLUnionType
orGraphQLInterfaceType
fields, the return value will be coerced to the proper run time typeGraphQLSerializationPlan
GraphQLSerializationPlan
describes the process of calling theserialize
functionon a
FieldDefinition
. This is the leaf node of the planning tree.GraphQLSerializationPlan
contains the following fields as well as the"All Fields" fields described later.
kind
string
GraphQLSerializationPlan
GraphQLMappingPlan
GraphQLMappingPlan
describes the processing of iterating over the elements of aGraphQLListType
and completing each element value. This is an internal node in theplanning tree and is not passed directly to functions registered with the schema, but
instead is a child plan depending on the structure of the schema.
GraphQLMappingPlan
contains the following fields as well as the"All Fields" fields described later.
kind
string
GraphQLMappingPlan
listElement
GraphQLCompletionPlan
GraphQLSelectionPlan
A
GraphQLSelectionPlan
indicates which fields will be selected from aGraphQLObjectType
as part of the return value completion process.
If the
GraphQLObjectType
has defined anisTypeOf
function, this function will be calledbefore the selection operation is applied to verify that the type of the runtime value
matches the expected type. If no
isTypeOf
is defined, the value is presumed to be ofthat type and evaluation proceeds.
isTypeOf
may also be called during the coercion process.(see Below)
The
GraphQLSelectionPlan
data structure will be passed to theisTypeOf
function as thesecond parameter,
info
.GraphQLSelectionPlan
contains the following fields as well as the"All Fields" fields described later.
kind
string
GraphQLSelectionPlan
fields
{[fieldName: string]: [ GraphQLResolvingPlan ]}
GraphQLResolvingPlan
.fieldPlansByAlias
{[alias: string]: GraphQLResolvingPlan}
GraphQLResolvingPlan
.The keys of
fields
match the names of fields on theGraphQLObjectType
so this will be the mostcommon way of access.
fieldPlansByAlias
contains the exact same plans, just with a differentorganization.
Each value in
fields
is a list because the execution engine may attempt to resolve any givenfield multiple times, if for example it had parameters or was aliased.
GraphQLCoercionPlan
A
GraphQLCoercionPlan
describes how to process a value of an abstract type(
GraphQLInterfaceType
orGraphQLUnionType
) based on the runtime type value.A plan for each possible
GraphQLObjectType
type is constructed and placed intypeChoices
by type name.A value is resolved in one of two ways:
resolveType
function, that function is called andthe name of the type is used to determine which plan to proceed with.
type's
isTypeOf
function to determine if the value to coerce is an instance of thattype. If no
isTypeOf
is defined, that plan will not be evaluated.If the type cannot be resolved it is an error. This is usually due to an error in
constructing the schema.
The
GraphQLCoercionPlan
data structure will be passed to theresolve
function as thesecond parameter,
info
.GraphQLCoercionPlan
contains the following fields as well as the"All Fields" fields described later.
kind
string
GraphQLCoercionPlan
typeChoices
{[typeName: string]: GraphQLSelectionPlan}
Fields in All Plans
All plans (
GraphQLResolvingPlan
,GraphQLSerializationPlan
,GraphQLMappingPlan
,GraphQLSelectionPlan
, andGraphQLCoercionPlan
) contain the following fields.kind
string
fieldName
string
fieldASTs
Array<Field>
returnType
GraphQLObjectType
parentType
GraphQLCompositeType
Fields in GraphQLResolveInfo Plans
The plans that can be passed to schema functions (
GraphQLResolvingPlan
,GraphQLSelectionPlan
, andGraphQLCoercionPlan
) contain the following additional fields.schema
GraphQLSchema
fragments
{ [fragmentName: string]: FragmentDefinition }
rootValue
mixed
operation
OperationDefinition
variableValues
{ [variableName: string]: mixed }
Open Issues
Big Diff
Sorry.
I know its hard to review something like this. See Changes section, hope that helps.
invariants vs Errors
This is the first time I've used typeflow so I'm not quire sure the invariant statements
I added are totally correct. Also, I was very confused about when to use invariant vs
throwing an error.
To me invariant is used when you expect the check to be removed in production. I'm not
sure that's the general usage in this code.
Please review this carefully, my concern is that I've hidden some error in my ignorance.
Backward compatibility
getObjectType
has been moved from definitions.js to execute.js to be more likedefault field resolver handling. This was necessary to avoid having
isTypeOf
accepta union of types.
An invariant test is triggered if type resolution fails where before it would silently return null.
An invariant test is triggered if two types with the same name implement the same
GraphQLInterfaceType, or two types of the same name are assigned to the same GraphQLUnionType.
I left
GraphQLResolvingInfo
in for BC even though it isn't used any more in case someonehas referenced it.
Resolving functions
I experimented with adding a resolve function to each type of plan. This would allow
more conditional logic to be moved from the evaluation phase to the planning phase.
It also creates an interesting possibility of being able to pass the top level plan to
a transformation function which might analyze the query plan and return a DIFFERENT plan
with resolver functions replaced or wrapped to implement new behaviors.
For another time.
Changes
definition.js:
execute.js
General
executeOperation
executeFields and executeFieldsSerially
resolveField
resolveOrError
completeValueCatchingError
completeValue