Skip to content
This repository was archived by the owner on Jul 25, 2024. It is now read-only.

Commit f6d3651

Browse files
author
Josh Price
committed
Merge pull request #90 from freshtonic/performance/reduce-types-once
Performance tweaks and visitor semantics simplification
2 parents 0cb1d6c + 70fdbcb commit f6d3651

29 files changed

Lines changed: 239 additions & 179 deletions

lib/graphql/execution/executor.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ defmodule GraphQL.Execution.Executor do
2323
"""
2424
@spec execute(GraphQL.Schema.t, GraphQL.Document.t, list) :: result_data | {:error, %{errors: list}}
2525
def execute(schema, document, opts \\ []) do
26+
schema = Schema.with_type_cache(schema)
2627
root_value = Keyword.get(opts, :root_value, %{})
2728
variable_values = Keyword.get(opts, :variable_values, %{})
2829
operation_name = Keyword.get(opts, :operation_name, nil)

lib/graphql/lang/ast/composite_visitor.ex

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,43 +42,32 @@ defimpl Visitor, for: GraphQL.Lang.AST.CompositeVisitor do
4242
Invoke *enter* on the outer visitor first, passing the resulting accumulator to the *enter*
4343
call on the *inner* visitor.
4444
45-
If either visitor's enter method returns :break, both visitors will still be executed, but
45+
If either visitor's enter method returns :skip, both visitors will still be executed, but
4646
then execution will cease.
4747
"""
4848
def enter(composite_visitor, node, accumulator) do
49-
call_in_order(
50-
composite_visitor.outer_visitor,
51-
composite_visitor.inner_visitor,
52-
node, accumulator, &Visitor.enter/3)
49+
{v1_next_action, v1_accumulator}
50+
= Visitor.enter(composite_visitor.outer_visitor, node, accumulator)
51+
accumulator = Map.merge(accumulator, v1_accumulator)
52+
53+
if v1_next_action == :skip do
54+
{:skip, accumulator}
55+
else
56+
Visitor.enter(composite_visitor.inner_visitor, node, accumulator)
57+
end
5358
end
5459

5560
@doc """
5661
Invoke *leave* on the inner visitor first, passing the resulting accumulator to the *leave*
5762
call on the *outer* visitor.
5863
59-
If either visitor's enter method returns :break, both visitors will still be executed, but
64+
If either visitor's enter method returns :skip, both visitors will still be executed, but
6065
then execution will cease.
6166
"""
6267
def leave(composite_visitor, node, accumulator) do
63-
call_in_order(
64-
composite_visitor.inner_visitor,
65-
composite_visitor.outer_visitor,
66-
node, accumulator, &Visitor.leave/3)
67-
end
68-
69-
# Visits two visitors in the order specified and invokes the supplied Visitor function.
70-
defp call_in_order(v1, v2, node, accumulator, fun) do
71-
{v1_next_action, v1_accumulator} = fun.(v1, node, accumulator)
72-
accumulator = Map.merge(accumulator, v1_accumulator)
73-
{v2_next_action, v2_accumulator} = fun.(v2, node, accumulator)
74-
75-
next_action = if v1_next_action == :break do
76-
:break
77-
else
78-
v2_next_action
79-
end
80-
81-
{next_action, v2_accumulator}
68+
v1_accumulator = Visitor.leave(composite_visitor.inner_visitor, node, accumulator)
69+
v2_accumulator = Visitor.leave(composite_visitor.outer_visitor, node, Map.merge(accumulator, v1_accumulator))
70+
v2_accumulator
8271
end
8372
end
8473

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
defmodule GraphQL.Lang.AST.DocumentInfo do
3+
4+
defstruct schema: nil,
5+
document: nil,
6+
lookups: %{}
7+
8+
def new(schema, document) do
9+
%GraphQL.Lang.AST.DocumentInfo{schema: schema, lookups: precompute_lookups(schema, document)}
10+
end
11+
12+
def get_fragment_definition(document_info, name) do
13+
document_info.lookups[:fragment_definitions][name]
14+
end
15+
16+
defp precompute_lookups(_schema, document) do
17+
%{
18+
fragment_definitions: Enum.reduce(document.definitions, %{}, fn(definition, acc) ->
19+
if definition[:kind] == :FragmentDefinition do
20+
put_in(acc[definition.name.value], definition)
21+
else
22+
acc
23+
end
24+
end)
25+
}
26+
end
27+
end

lib/graphql/lang/ast/parallel_visitor.ex

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule GraphQL.Lang.AST.ParallelVisitor do
44
A ParallelVisitor runs all child visitors in parallel instead of serially like the CompositeVisitor.
55
66
In this context, 'in parallel' really means that for each node in the AST, each visitor will be invoked
7-
for each node in the AST, but the :break/:continue return value of enter and leave is maintained per-visitor.
7+
for each node in the AST, but the :skip/:continue return value of enter and leave is maintained per-visitor.
88
99
This means invividual visitors can bail out of AST processing as soon as possible and not waste cycles.
1010
@@ -19,31 +19,33 @@ defmodule GraphQL.Lang.AST.ParallelVisitor do
1919

2020
defimpl Visitor do
2121
def enter(visitor, node, accumulator) do
22-
accumulator = Enum.reduce(visitor.visitors, accumulator, fn(child_visitor, acc) ->
23-
if !skipping?(accumulator, child_visitor) do
24-
acc = case child_visitor |> Visitor.enter(node, accumulator) do
25-
{:continue, next_accumulator} -> next_accumulator
26-
{:break, next_accumulator} ->
27-
put_in(next_accumulator[:skipping][child_visitor], node)
28-
end
22+
visitors = Enum.filter(visitor.visitors, fn(child_visitor) ->
23+
!skipping?(accumulator, child_visitor)
24+
end)
25+
accumulator = Enum.reduce(visitors, accumulator, fn(child_visitor, accumulator) ->
26+
case child_visitor |> Visitor.enter(node, accumulator) do
27+
{:continue, next_accumulator} -> next_accumulator
28+
{:skip, next_accumulator} ->
29+
put_in(next_accumulator[:skipping][child_visitor], node)
2930
end
30-
acc
3131
end)
32-
{:continue, accumulator}
32+
if length(visitors) > 0 do
33+
{:continue, accumulator}
34+
else
35+
{:skip, accumulator}
36+
end
3337
end
3438

3539
def leave(visitor, node, accumulator) do
36-
accumulator = Enum.reduce(visitor.visitors, accumulator, fn(child_visitor, acc) ->
40+
Enum.reduce visitor.visitors, accumulator, fn(child_visitor, accumulator) ->
3741
cond do
38-
!skipping?(acc, child_visitor) ->
39-
{_, next_accumulator} = child_visitor |> Visitor.leave(node, acc)
40-
next_accumulator
42+
!skipping?(accumulator, child_visitor) ->
43+
child_visitor |> Visitor.leave(node, accumulator)
4144
accumulator[:skipping][child_visitor] == node ->
42-
put_in(acc[:skipping][child_visitor], false)
45+
Map.delete(accumulator[:skipping], child_visitor)
4346
true -> accumulator
4447
end
45-
end)
46-
{:continue, accumulator}
48+
end
4749
end
4850

4951
defp skipping?(accumulator, child_visitor) do

lib/graphql/lang/ast/reducer.ex

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,27 @@ defmodule GraphQL.Lang.AST.Reducer do
1313

1414
def reduce(node, visitor, accumulator) do
1515
accumulator = InitialisingVisitor.init(visitor, accumulator)
16-
{_, accumulator} = visit(node, visitor, accumulator)
16+
accumulator = visit(node, visitor, accumulator)
1717
PostprocessingVisitor.finish(visitor, accumulator)
1818
end
1919

2020
defp visit([child|rest], visitor, accumulator) do
21-
{next_action, accumulator} = visit(child, visitor, accumulator)
22-
case next_action do
23-
:continue -> visit(rest, visitor, accumulator)
24-
:break -> {:break, accumulator}
25-
end
21+
accumulator = visit(child, visitor, accumulator)
22+
visit(rest, visitor, accumulator)
2623
end
2724

28-
defp visit([], _visitor, accumulator), do: {:continue, accumulator}
25+
defp visit([], _visitor, accumulator), do: accumulator
2926

30-
# FIXME: we need to enforce an invariant that if a enter is called for a node, we guarantee
31-
# that leave is called on a node. That means :break means "do not go deeper".
3227
defp visit(node, visitor, accumulator) do
3328
{next_action, accumulator} = Visitor.enter(visitor, node, accumulator)
34-
case next_action do
35-
:continue ->
36-
{next_action, accumulator} = visit_children(node, visitor, accumulator)
37-
case next_action do
38-
:continue -> Visitor.leave(visitor, node, accumulator)
39-
:break -> {:break, accumulator}
40-
end
41-
:break -> {:break, accumulator}
29+
30+
accumulator = if next_action != :skip do
31+
visit_children(node, visitor, accumulator)
32+
else
33+
accumulator
4234
end
35+
36+
Visitor.leave(visitor, node, accumulator)
4337
end
4438

4539
defp visit_children(node = %{kind: kind}, visitor, accumulator) when is_atom(kind) do
@@ -48,12 +42,10 @@ defmodule GraphQL.Lang.AST.Reducer do
4842
end
4943

5044
defp visit_each_child([child|rest], visitor, accumulator) do
51-
{next_action, accumulator} = visit(child, visitor, accumulator)
52-
case next_action do
53-
:continue -> visit_each_child(rest, visitor, accumulator)
54-
:break -> {:break, accumulator}
55-
end
45+
accumulator = visit(child, visitor, accumulator)
46+
accumulator = visit_each_child(rest, visitor, accumulator)
47+
accumulator
5648
end
5749

58-
defp visit_each_child([], _visitor, accumulator), do: {:continue, accumulator}
50+
defp visit_each_child([], _visitor, accumulator), do: accumulator
5951
end

lib/graphql/lang/ast/type_info_visitor.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ defmodule GraphQL.Lang.AST.TypeInfoVisitor do
168168
_ ->
169169
:ignore
170170
end
171-
{:continue, accumulator}
171+
accumulator
172172
end
173173
end
174174
end

lib/graphql/lang/ast/visitor.ex

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,14 @@ defmodule GraphQL.Lang.AST do
3131
3232
The return value should be:
3333
34-
{next_action, acc}
35-
36-
where next_action is either :break or :continue and acc is the new value of the accumulator.
37-
38-
:break will abort the visitor and AST traversal will cease returning the current value of the accumulator.
34+
acc
3935
"""
4036
def leave(visitor, node, accumulator)
4137
end
4238

4339
defimpl Visitor, for: Any do
4440
def enter(_visitor, _node, accumulator), do: {:continue, accumulator}
45-
def leave(_visitor, _node, accumulator), do: {:continue, accumulator}
41+
def leave(_visitor, _node, accumulator), do: accumulator
4642
end
4743

4844
defprotocol InitialisingVisitor do

lib/graphql/type/interface.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ defmodule GraphQL.Type.Interface do
3030
"""
3131
def possible_types(interface, schema) do
3232
# get the complete typemap from this schema
33-
GraphQL.Schema.reduce_types(schema)
33+
schema.type_cache
3434
# filter them down to a list of types that implement this interface
3535
|> Enum.filter(fn {_, typedef} -> GraphQL.Type.implements?(typedef, interface) end)
3636
# then return the type, instead of the {name, type} tuple that comes from
37-
# the reduce_types call
37+
# the type_cache
3838
|> Enum.map(fn({_,v}) -> v end)
3939
end
4040

lib/graphql/type/introspection.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ defmodule GraphQL.Type.Introspection do
3636
description: "A list of all types supported by this server.",
3737
type: %NonNull{ofType: %List{ofType: %NonNull{ofType: Type}}},
3838
resolve: fn(schema, _, _) ->
39-
Map.values(GraphQL.Schema.reduce_types(schema))
39+
Map.values(schema.type_cache)
4040
end
4141
},
4242
queryType: %{
@@ -161,7 +161,7 @@ defmodule GraphQL.Type.Introspection do
161161
(%GraphQL.Type.Interface{name: _name} = interface, _args, info) ->
162162
AbstractType.possible_types(interface, info.schema)
163163
(%GraphQL.Type.Union{name: name}, _args, info) ->
164-
GraphQL.Schema.reduce_types(info.schema)[name].types
164+
info.schema.type_cache[name].types
165165
(_, _, _) -> nil
166166
end
167167
},
@@ -407,7 +407,7 @@ defmodule GraphQL.Type.Introspection do
407407
name: %{type: %NonNull{ofType: %String{}}}
408408
},
409409
resolve: fn(_, %{name: name}, %{schema: schema}) ->
410-
GraphQL.Schema.reduce_types(schema)[name]
410+
schema.type_cache[name]
411411
end
412412
}
413413
end

lib/graphql/type/schema.ex

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ defmodule GraphQL.Schema do
22

33
@type t :: %GraphQL.Schema{
44
query: Map,
5-
mutation: Map,
6-
types: [GraphQL.Type.AbstractType.t | GraphQL.Type.ObjectType.t]
5+
mutation: Map
76
}
87

98
alias GraphQL.Type.Interface
@@ -13,7 +12,16 @@ defmodule GraphQL.Schema do
1312
alias GraphQL.Type.CompositeType
1413
alias GraphQL.Lang.AST.Nodes
1514

16-
defstruct query: nil, mutation: nil, types: []
15+
defstruct query: nil, mutation: nil, type_cache: nil
16+
17+
def with_type_cache(schema = %{type_cache: nil}), do: new(schema)
18+
def with_type_cache(schema), do: schema
19+
20+
def new(%{query: query, mutation: mutation}) do
21+
%GraphQL.Schema{query: query, mutation: mutation, type_cache: do_reduce_types(query, mutation)}
22+
end
23+
def new(%{mutation: mutation}), do: new(%{query: nil, mutation: mutation})
24+
def new(%{query: query}), do: new(%{query: query, mutation: nil})
1725

1826
# FIXME: I think *schema* should be the first argument in this module.
1927
def type_from_ast(nil, _), do: nil
@@ -24,32 +32,32 @@ defmodule GraphQL.Schema do
2432
%GraphQL.Type.List{ofType: type_from_ast(input_type_ast.type, schema)}
2533
end
2634
def type_from_ast(%{kind: :NamedType} = input_type_ast, schema) do
27-
reduce_types(schema) |> Map.get(input_type_ast.name.value, :not_found)
35+
schema.type_cache |> Map.get(input_type_ast.name.value, :not_found)
2836
end
2937

30-
def reduce_types(type) do
38+
defp do_reduce_types(query, mutation) do
3139
%{}
32-
|> reduce_types(type.query)
33-
|> reduce_types(type.mutation)
40+
|> reduce_types(query)
41+
|> reduce_types(mutation)
3442
|> reduce_types(Introspection.Schema.type)
3543
end
3644

37-
def reduce_types(typemap, %{ofType: list_type}) do
45+
defp reduce_types(typemap, %{ofType: list_type}) do
3846
reduce_types(typemap, list_type)
3947
end
4048

41-
def reduce_types(typemap, %Interface{} = type) do
49+
defp reduce_types(typemap, %Interface{} = type) do
4250
Map.put(typemap, type.name, type)
4351
end
4452

45-
def reduce_types(typemap, %Union{} = type) do
53+
defp reduce_types(typemap, %Union{} = type) do
4654
typemap = Map.put(typemap, type.name, type)
4755
Enum.reduce(type.types, typemap, fn(fieldtype,map) ->
4856
reduce_types(map, fieldtype)
4957
end)
5058
end
5159

52-
def reduce_types(typemap, %ObjectType{} = type) do
60+
defp reduce_types(typemap, %ObjectType{} = type) do
5361
if Map.has_key?(typemap, type.name) do
5462
typemap
5563
else
@@ -65,10 +73,10 @@ defmodule GraphQL.Schema do
6573
end
6674
end
6775

68-
def reduce_types(typemap, %{name: name} = type), do: Map.put(typemap, name, type)
69-
def reduce_types(typemap, nil), do: typemap
76+
defp reduce_types(typemap, %{name: name} = type), do: Map.put(typemap, name, type)
77+
defp reduce_types(typemap, nil), do: typemap
7078

71-
def reduce_types(typemap, type_module) when is_atom(type_module) do
79+
defp reduce_types(typemap, type_module) when is_atom(type_module) do
7280
reduce_types(typemap, apply(type_module, :type, []))
7381
end
7482

0 commit comments

Comments
 (0)