Skip to content
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

Interpreter performance #1996

Merged
merged 2 commits into from
Dec 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 64 additions & 22 deletions lib/graphql/execution/interpreter/runtime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ def initialize(query:, response:)
@query = query
@schema = query.schema
@context = query.context
@interpreter_context = @context.namespace(:interpreter)
@response = response
@dead_paths = {}
@types_at_paths = {}
# A cache of { Class => { String => Schema::Field } }
# Which assumes that MyObject.get_field("myField") will return the same field
# during the lifetime of a query
@fields_cache = Hash.new { |h, k| h[k] = {} }
end

def final_value
Expand Down Expand Up @@ -55,20 +60,35 @@ def gather_selections(owner_type, selections, selections_by_name)
when GraphQL::Language::Nodes::Field
if passes_skip_and_include?(node)
response_key = node.alias || node.name
s = selections_by_name[response_key] ||= []
s << node
selections = selections_by_name[response_key]
# if there was already a selection of this field,
# use an array to hold all selections,
# otherise, use the single node to represent the selection
if selections
# This field was already selected at least once,
# add this node to the list of selections
s = Array(selections)
s << node
selections_by_name[response_key] = s
else
# No selection was found for this field yet
selections_by_name[response_key] = node
end
end
when GraphQL::Language::Nodes::InlineFragment
if passes_skip_and_include?(node)
include_fragmment = if node.type
if node.type
type_defn = schema.types[node.type.name]
type_defn = type_defn.metadata[:type_class]
possible_types = query.warden.possible_types(type_defn).map { |t| t.metadata[:type_class] }
possible_types.include?(owner_type)
# Faster than .map{}.include?()
query.warden.possible_types(type_defn).each do |t|
if t.metadata[:type_class] == owner_type
gather_selections(owner_type, node.selections, selections_by_name)
break
end
end
else
true
end
if include_fragmment
# it's an untyped fragment, definitely continue
gather_selections(owner_type, node.selections, selections_by_name)
end
end
Expand All @@ -77,9 +97,11 @@ def gather_selections(owner_type, selections, selections_by_name)
fragment_def = query.fragments[node.name]
type_defn = schema.types[fragment_def.type.name]
type_defn = type_defn.metadata[:type_class]
possible_types = schema.possible_types(type_defn).map { |t| t.metadata[:type_class] }
if possible_types.include?(owner_type)
gather_selections(owner_type, fragment_def.selections, selections_by_name)
schema.possible_types(type_defn).each do |t|
if t.metadata[:type_class] == owner_type
gather_selections(owner_type, fragment_def.selections, selections_by_name)
break
end
end
end
else
Expand All @@ -91,10 +113,19 @@ def gather_selections(owner_type, selections, selections_by_name)
def evaluate_selections(path, owner_object, owner_type, selections, root_operation_type: nil)
selections_by_name = {}
gather_selections(owner_type, selections, selections_by_name)
selections_by_name.each do |result_name, field_ast_nodes|
ast_node = field_ast_nodes.first
selections_by_name.each do |result_name, field_ast_nodes_or_ast_node|
# As a performance optimization, the hash key will be a `Node` if
# there's only one selection of the field. But if there are multiple
# selections of the field, it will be an Array of nodes
if field_ast_nodes_or_ast_node.is_a?(Array)
field_ast_nodes = field_ast_nodes_or_ast_node
ast_node = field_ast_nodes.first
else
field_ast_nodes = nil
ast_node = field_ast_nodes_or_ast_node
end
field_name = ast_node.name
field_defn = owner_type.get_field(field_name)
field_defn = @fields_cache[owner_type][field_name] ||= owner_type.get_field(field_name)
is_introspection = false
if field_defn.nil?
field_defn = if owner_type == schema.query.metadata[:type_class] && (entry_point_field = schema.introspection_system.entry_point(name: field_name))
Expand Down Expand Up @@ -137,6 +168,9 @@ def evaluate_selections(path, owner_object, owner_type, selections, root_operati
when :path
kwarg_arguments[:path] = next_path
when :lookahead
if !field_ast_nodes
field_ast_nodes = [ast_node]
end
kwarg_arguments[:lookahead] = Execution::Lookahead.new(
query: query,
ast_nodes: field_ast_nodes,
Expand All @@ -147,11 +181,17 @@ def evaluate_selections(path, owner_object, owner_type, selections, root_operati
end
end

next_selections = field_ast_nodes.inject([]) { |memo, f| memo.concat(f.selections) }
# Optimize for the case that field is selected only once
if field_ast_nodes.nil? || field_ast_nodes.size == 1
next_selections = ast_node.selections
else
next_selections = []
field_ast_nodes.each { |f| next_selections.concat(f.selections) }
end

app_result = query.trace("execute_field", {field: field_defn, path: next_path}) do
context.namespace(:interpreter)[:current_path] = next_path
context.namespace(:interpreter)[:current_field] = field_defn
@interpreter_context[:current_path] = next_path
@interpreter_context[:current_field] = field_defn
field_defn.resolve(object, kwarg_arguments, context)
end

Expand Down Expand Up @@ -251,12 +291,13 @@ def continue_field(path, value, field, type, ast_node, next_selections, is_non_n
write_in_response(path, response_list)
inner_type = type.of_type
idx = 0
value.map do |inner_value|
value.each do |inner_value|
next_path = path.dup
next_path << idx
next_path.freeze
idx += 1
set_type_at_path(next_path, inner_type)
# This will update `response_list` with the lazy
after_lazy(inner_value, path: next_path, field: field) do |inner_inner_value|
# reset `is_non_null` here and below, because the inner type will have its own nullability constraint
continue_value = continue_value(next_path, inner_inner_value, field, false, ast_node)
Expand All @@ -265,6 +306,7 @@ def continue_field(path, value, field, type, ast_node, next_selections, is_non_n
end
end
end
response_list
when "NON_NULL"
inner_type = type.of_type
# For fields like `__schema: __Schema!`
Expand Down Expand Up @@ -305,12 +347,12 @@ def resolve_if_late_bound_type(type)
# @param eager [Boolean] Set to `true` for mutation root fields only
# @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
def after_lazy(obj, field:, path:, eager: false)
context.namespace(:interpreter)[:current_path] = path
context.namespace(:interpreter)[:current_field] = field
@interpreter_context[:current_path] = path
@interpreter_context[:current_field] = field
if schema.lazy?(obj)
lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
context.namespace(:interpreter)[:current_path] = path
context.namespace(:interpreter)[:current_field] = field
@interpreter_context[:current_path] = path
@interpreter_context[:current_field] = field
# Wrap the execution of _this_ method with tracing,
# but don't wrap the continuation below
inner_obj = query.trace("execute_field_lazy", {field: field, path: path}) do
Expand Down
12 changes: 11 additions & 1 deletion lib/graphql/schema/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,17 @@ def authorized?(object, context)
true
end

self_auth && arguments.each_value.all? { |a| a.authorized?(object, context) }
if self_auth
# Faster than `.any?`
arguments.each_value do |arg|
if !arg.authorized?(object, context)
return false
end
end
true
else
false
end
end

# Implement {GraphQL::Field}'s resolve API.
Expand Down
6 changes: 4 additions & 2 deletions lib/graphql/schema/member/base_dsl_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ def overridden_graphql_name
# The default name is the Ruby constant name,
# without any namespaces and with any `-Type` suffix removed
def default_graphql_name
raise NotImplementedError, 'Anonymous class should declare a `graphql_name`' if name.nil?
@default_graphql_name ||= begin
raise NotImplementedError, 'Anonymous class should declare a `graphql_name`' if name.nil?

name.split("::").last.sub(/Type\Z/, "")
name.split("::").last.sub(/Type\Z/, "")
end
end

def visible?(context)
Expand Down
12 changes: 8 additions & 4 deletions lib/graphql/schema/member/has_fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,16 @@ def fields
end

def get_field(field_name)
for ancestor in ancestors
if ancestor.respond_to?(:own_fields) && f = ancestor.own_fields[field_name]
return f
if (f = own_fields[field_name])
f
else
for ancestor in ancestors
if ancestor.respond_to?(:own_fields) && f = ancestor.own_fields[field_name]
return f
end
end
nil
end
nil
end

# Register this field with the class, overriding a previous one if needed.
Expand Down