When Grafast sees an operation for the first time, it builds an operation
plan, which is the combination of an execution plan and
an output plan. To do so, it walks the selection sets
calling the user-provided
plan method for each field to determine the steps
that need to be executed for that field. That series of steps, which we'll call
the field plan, is then woven with all the other field plans in the
operation, to form a directed acyclic graph we call the execution plan.
Whilst doing this, Grafast keeps track of which fields returned which
steps, and uses this information to form the output plan. Finally the
execution plan is optimized and finalized and the output plan is finalized,
then they are ready for execution.
A simplified version of the process is this:
- Start at the root selection set.
- For each field in the current selection set:
- Tree shake
- Tree shake
The execution plan is (generally1) asynchronous. It is responsible for fetching all the data required by the operation in as efficient a manner as possible, and can evolve quite significantly as a result of deduplication, tree shaking and optimization.
The data fetched by the execution plan will not be in any particular format,
and may have been significantly deduplicated and simplified as part of the
operations. The output plan is responsible for taking this bundle of data and
formatting it back out as a valid GraphQL response, including serializing all
the leaves correctly and handling nulls and errors following the GraphQL
specification. The output plan always runs synchronously, though in streaming
situations such as GraphQL subscriptions or incremental delivery
@defer) part of the output plan may be executed for each payload
in the underlying stream.
Whilst Grafast is building the operation plan, it may also determined
particular constraints that govern whether the operation plan may be used for a
future request or not. For example, if the request contains
@skip(if: $variable) then a different operation plan would be needed depending
false. Where possible, constraints are
kept as narrow as possible - for example "variable
$foo is a list" is preferred over "variable
$foo is the list [1,2,3]" - to
When an operation is seen a future time, Grafast first looks for an existing operation plan whose constraints fit the request before falling back to creating a new operation plan.
Once a field is fully planned, Grafast will deduplicate the new steps it has produced, attempting to replace them with existing "peer" steps that already existed. These "peer" steps will have been constructed via the same step class, and will have the same dependencies. By deduplicating at this stage (before planning the child selection set) we help to ensure that the schema remains in adherence to the GraphQL specification. Deduplication can reduce the number of steps in the plan, leading to greater efficiency (and easier to understand plans).
Once every selection set has been fully visited and every field has been planned, the execution plan is complete.
Grafast then walks through the output plans, their required steps and those steps dependencies (and their dependencies and so on), marking them as active. Any step that is not active is "unreachable" and thus is no longer needed, therefore it can be be removed from the execution plan.
Certain steps are immune to tree shaking (they're seen as always active), in particular these include certain system steps, and any step that has side effects.
Once the execution plan is complete and the unnecessary steps have been tree
shaken away, Grafast optimizes the plan by calling the
method on each step that supports it. This gives each step a chance to replace
themselves with more optimal forms by inspecting and interacting with their
For example if a "first" step is to optimize itself, and its parent is a "list" step, then it can simply replace itself with the first entry in the list of plans the "list" step contains. More advanced examples may include topics such as joining tables in a database, or adding selection sets to a remote GraphQL operation.
The optimize method is called starting with the dependencies (leaves) and working its way up the dependents (trunk) of the execution plan's directed acyclic graph. Since plans should only talk to their ancestors (and not their descendents) during optimize, this ensures that their dependencies remain what the class expects until after it is optimized.
Once optimization is complete, Grafast tree shakes again to remove any unnecessary steps from the execution plan.
Grafast then finalizes the plan by calling the
finalize method on steps
that support it. This gives each step a chance to do work that need only be done
once, for example:
- a step that talks to a database might compile the SQL it needs and cache it for later
- a step that performs templating may build an optimized template function
Output plan finalize
Finally, the output plans are finalized, building optimized output functions and referencing the latest optimized steps.
- If the execution plan contains no asynchronous steps, then it can be executed synchronously. This is rare, though, as typically it will contain steps that fetch data from remote data sources such as databases or web services.↩