Skip to main content

Plan diagrams

A plan diagram is a directed acyclic graph made of a number of step nodes connected by arrows which show the flow of data. It also details the LayerPlans (aka "buckets") and the relationships between them.

Example

For the following GraphQL request:

{
currentUser {
name
friends {
name
}
}
}

You might see a plan diagram such as:

%%{init: {'themeVariables': { 'fontSize': '12px'}}}%% flowchart TD classDef path fill:#eee,stroke:#000,color:#000 classDef plan fill:#fff,stroke-width:1px,color:#000 classDef itemplan fill:#fff,stroke-width:2px,color:#000 classDef unbatchedplan fill:#dff,stroke-width:1px,color:#000 classDef sideeffectplan fill:#fcc,stroke-width:2px,color:#000 classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left %% plan dependencies Access6{{"Access[6∈0] ➊<br />ᐸ2.currentUserIdᐳ"}}:::plan __Value2["__Value[2∈0] ➊<br />ᐸcontextᐳ"]:::plan __Value2 --> Access6 Load7[["Load[7∈0] ➊<br />ᐸuserByIdᐳ"]]:::plan Access6 --> Load7 Load10[["Load[10∈0] ➊<br />ᐸfriendshipsByUserIdᐳ"]]:::plan Access6 --> Load10 __Value4["__Value[4∈0] ➊<br />ᐸrootValueᐳ"]:::plan __Item14[/"__Item[14∈3]<br />ᐸ10ᐳ"\]:::itemplan Load10 ==> __Item14 Access16{{"Access[16∈3]<br />ᐸ14.friend_idᐳ"}}:::plan __Item14 --> Access16 Load17[["Load[17∈3]<br />ᐸuserByIdᐳ"]]:::plan Access16 --> Load17 %% define steps Bucket0("Bucket 0 (root)<br /><br />1: <br />ᐳ: Access[6]<br />2: Load[7], Load[10]"):::bucket classDef bucket0 stroke:#696969 class Bucket0,__Value2,__Value4,Access6,Load7,Load10 bucket0 Bucket1("Bucket 1 (nullableBoundary)<br />Deps: 7, 10<br /><br />ROOT LoadᐸuserByIdᐳ[7]"):::bucket classDef bucket1 stroke:#00bfff class Bucket1 bucket1 Bucket3("Bucket 3 (listItem)<br /><br />ROOT __Item{3}ᐸ10ᐳ[14]<br />1: <br />ᐳ: Access[16]<br />2: Load[17]"):::bucket classDef bucket3 stroke:#ffa500 class Bucket3,__Item14,Access16,Load17 bucket3 Bucket4("Bucket 4 (nullableBoundary)<br />Deps: 17<br /><br />ROOT Load{3}ᐸuserByIdᐳ[17]"):::bucket classDef bucket4 stroke:#0000ff class Bucket4 bucket4 Bucket0 --> Bucket1 Bucket1 --> Bucket3 Bucket3 --> Bucket4

This diagram has two main sections: the steps, and the buckets. We'll talk about the buckets first.

note

If you're able to convince mermaid to reliably render these two things on the same diagram in a more independent manner, please get in touch!

Buckets (aka layer plans)

A bucket, or layer plan, is where data for steps at a similar "layer" in the operation go. For each layer, the cardinality is the same for every step - every step deals with the same number of results. Generally buckets are introduced where this may no longer be the case - for example when we start handling more results due to processing the items in a list, or fewer results due to polymorphic filtering or excluding null results from further processing.

Here's an example of a bucket node:

%%{init: {'themeVariables': { 'fontSize': '12px'}}}%% flowchart TD classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left Bucket1("Bucket 1 (nullableBoundary)<br />Deps: 8<br /><br />ROOT LoadOneᐸuserByIdᐳ[8]<br />1: <br />ᐳ: Access[10]<br />2: Load[11]"):::bucket classDef bucket1 stroke:#00bfff class Bucket1,Access10,Load11 bucket1

First line (e.g. Bucket 1 (nullableBoundary))

The first line in the bucket node always start with the word Bucket followed by the bucket number. Buckets are numbered from 0, but during the optimization of the operation plan the need for certain buckets may be eradicated, resulting in "gaps" in the numbering. Afterwards comes the reason for the bucket, in parenthesis: root, nullableBoundary, listItem, subscription, mutationField, defer, polymorphic, subroutine or other.

Deps:

A line starting with Deps: indicates the buckets "dependencies" - that is to say the data from the parent bucket that will be copied into this bucket when it is created, before execution of any of the steps within the bucket commences.

ROOT

A line starting with ROOT indicates the step that represents the "root" of the bucket, this is often used for checking for nulls/errors and similar purposes.

1:

Lines beginning with a number indicate the order in which the steps will be executed in that bucket. Numbered lines with multiple steps listed will execute those steps in parallel since they are independent of each other.

ᐳ:

Immediately following a numbered line, there may be a line that begins with "ᐳ:". This indicates the "synchronous and safe" unbatched steps that will be executed immediately after the numbered line - this is an optimization that means the system doesn't need to loop multiple times to satisfy these needs.

Steps

Generally when looking at a plan diagram, you care more about the steps than the buckets. Each step contains a first line that consists of the step class name with Step removed (for brevity), followed by [X∈Y] where X is the id of the step, and Y is the number of the bucket to which it belongs - the latter of which is also indicated by the border colour of the step.

Often steps will have a second line of text such as ᐸ3.currentUserIdᐳ - this is metadata specific to that particular step class that gives more detail on what this step is doing.

The shape of the step's border also reveals more details about the step:

Standard synchronous steps

%%{init: {'themeVariables': { 'fontSize': '16px'}}}%% flowchart TD classDef path fill:#eee,stroke:#000,color:#000 classDef plan fill:#fff,stroke-width:1px,color:#000 classDef itemplan fill:#fff,stroke-width:2px,color:#000 classDef sideeffectplan fill:#fcc,stroke-width:2px,color:#000 classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left classDef bucket0 stroke:#696969 classDef bucket2 stroke:#7f007f __Value5["__Value[5∈0]<br />ᐸrootValueᐳ"]:::plan class __Value5 bucket0

A standard synchronous step is represented by a simple rectangle.

In this example:

  • __Value is the name of the Step (but with the redundant 'Step' removed - truly it's called __ValueStep).
  • 5 is the step ID - every step has a unique identifier.
  • ∈0 means that the step "belongs" to LayerPlan (aka "bucket") number 0.
  • The next line contains additional step-specific metadata; in this case it's telling us that the step represents the GraphQL rootValue

Asynchronous steps

%%{init: {'themeVariables': { 'fontSize': '16px'}}}%% flowchart TD classDef path fill:#eee,stroke:#000,color:#000 classDef plan fill:#fff,stroke-width:1px,color:#000 classDef itemplan fill:#fff,stroke-width:2px,color:#000 classDef sideeffectplan fill:#fcc,stroke-width:2px,color:#000 classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left classDef bucket0 stroke:#696969 classDef bucket2 stroke:#7f007f LoadOne18[["LoadOne[18∈3]<br />ᐸuserByIdᐳ"]]:::plan class LoadOne18 bucket2

These appear very similar to synchronous steps, except that they have a double border on the left and right. The key difference with these steps, and why they render more prominently, is that this is typically where your work will take place - they execute asynchronously, and so can communicate with remote services and resources.

All step classes you create will generate asynchronous steps unless you specifically opt them in to one of the optimized forms.

Item steps

%%{init: {'themeVariables': { 'fontSize': '16px'}}}%% flowchart TD classDef path fill:#eee,stroke:#000,color:#000 classDef plan fill:#fff,stroke-width:1px,color:#000 classDef itemplan fill:#fff,stroke-width:2px,color:#000 classDef sideeffectplan fill:#fcc,stroke-width:2px,color:#000 classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left classDef bucket0 stroke:#696969 classDef bucket2 stroke:#7f007f __Item15[/"__Item[15∈3]<br />ᐸ11ᐳ"\]:::itemplan class __Item15 bucket3

__ItemStep steps never execute, they're managed by Grafast manually to represent individual entries in a list or stream (including subscription event streams). They look like a trapezoid (US) / trapezium (UK) to imply that they are going from a smaller set to a larger set, though this isn't always the case.

Unbatched synchronous steps

%%{init: {'themeVariables': { 'fontSize': '16px'}}}%% flowchart TD classDef path fill:#eee,stroke:#000,color:#000 classDef plan fill:#fff,stroke-width:1px,color:#000 classDef itemplan fill:#fff,stroke-width:2px,color:#000 classDef sideeffectplan fill:#fcc,stroke-width:2px,color:#000 classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left classDef bucket0 stroke:#696969 classDef bucket2 stroke:#7f007f Access17{{"Access[17∈3]<br />ᐸ15.friend_idᐳ"}}:::plan class Access17 bucket3

An unbatched synchronous step is a special variant of a synchronous step that confers no benefit from batching. This allows the system to calculate their values alongside their dependencies without having to loop multiple times. Typically they're used for trivial operations such as accessing a named property from an object or accessing the first/last entry in a list.

Side-effect steps

Certain steps may have side effects (e.g. cause mutations in your data sources, etc), and thus they have a significant impact on the plan. Side effect steps are rendered with a slightly reddish-hued background.

%%{init: {'themeVariables': { 'fontSize': '16px'}}}%% flowchart TD classDef path fill:#eee,stroke:#000,color:#000 classDef plan fill:#fff,stroke-width:1px,color:#000 classDef itemplan fill:#fff,stroke-width:2px,color:#000 classDef sideeffectplan fill:#fcc,stroke-width:2px,color:#000 classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left classDef bucket0 stroke:#696969 classDef bucket2 stroke:#7f007f SideEffect18[["SideEffect[18∈3]"]]:::sideeffectplan

Implicit side effect dependency

If a step is implicitly dependent on a side effect due to occuring after the side effect, a dotted line ending in a circle will join the nodes to indicate the step is implicitly dependent on the previous step:

%%{init: {'themeVariables': { 'fontSize': '16px'}}}%% flowchart TD classDef path fill:#eee,stroke:#000,color:#000 classDef plan fill:#fff,stroke-width:1px,color:#000 classDef itemplan fill:#fff,stroke-width:2px,color:#000 classDef sideeffectplan fill:#fcc,stroke-width:2px,color:#000 classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left classDef bucket0 stroke:#696969 classDef bucket2 stroke:#7f007f Access17{{"Access[17∈3]<br />ᐸ15.friend_idᐳ"}}:::plan SideEffect18[["SideEffect[18∈3]"]]:::sideeffectplan LoadOne19{{"LoadOne[19∈3]"}}:::plan Access17 --> SideEffect18 Access17 --> LoadOne19 SideEffect18 -.-o LoadOne19 class Access17 bucket3 class SideEffect18 bucket3 class LoadOne19 bucket3

Unary steps

During planning, Grafast may determine that there will only ever be one value (per request) for a given step, and thus it demarks this step as a "unary step". This can be important for the step's dependents since they know they'll receive exactly one value from this step, rather than a batch of values, and thus they may be able to simplify their logic. This can be critically important for things such as pagination limits, and steps are allowed to require that a dependency is unary.

In plan diagrams, unary steps are denoted by a in their Node:

%%{init: {'themeVariables': { 'fontSize': '16px'}}}%% flowchart TD classDef path fill:#eee,stroke:#000,color:#000 classDef plan fill:#fff,stroke-width:1px,color:#000 classDef itemplan fill:#fff,stroke-width:2px,color:#000 classDef sideeffectplan fill:#fcc,stroke-width:2px,color:#000 classDef bucket fill:#f6f6f6,color:#000,stroke-width:2px,text-align:left classDef bucket0 stroke:#696969 classDef bucket2 stroke:#7f007f Access17{{"Access[16∈0] ➊<br />ᐸ2.pgSettingsᐳ"}}:::plan

How to see a request's plan diagram

Your server must be configured to expose plans for you to be able to see them; if it is then you can use a tool such as Ruru to view the execution plan, or you can render it directly from the JSON response. You can convert the plan JSON into mermaid format via the planToMermaid function so you can load them into the mermaid live editor.