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

perf(graphql): cache attribute hashes #723

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,46 @@ module GraphQLTrace # rubocop:disable Metrics/ModuleLength
def initialize(trace_scalars: false, **_options)
@trace_scalars = trace_scalars
@_otel_field_key_cache = Hash.new { |h, k| h[k] = _otel_field_key(k) }
@_otel_field_key_cache.compare_by_identity
@_otel_authorized_key_cache = Hash.new { |h, k| h[k] = _otel_authorized_key(k) }
@_otel_authorized_key_cache.compare_by_identity
@_otel_resolve_type_key_cache = Hash.new { |h, k| h[k] = _otel_resolve_type_key(k) }
@_otel_resolve_type_key_cache.compare_by_identity

@_otel_type_attrs_cache = Hash.new do |h, type|
h[type] = {
'graphql.type.name' => type.graphql_name,
'graphql.lazy' => false
}.freeze
end
@_otel_type_attrs_cache.compare_by_identity

@_otel_lazy_type_attrs_cache = Hash.new do |h, type|
h[type] = {
'graphql.type.name' => type.graphql_name,
'graphql.lazy' => true
}.freeze
end
@_otel_lazy_type_attrs_cache.compare_by_identity

@_otel_field_attrs_cache = Hash.new do |h, field|
h[field] = {
'graphql.field.parent' => field.owner&.graphql_name,
'graphql.field.name' => field.graphql_name,
'graphql.lazy' => false
}.freeze
end
@_otel_field_attrs_cache.compare_by_identity

@_otel_lazy_field_attrs_cache = Hash.new do |h, field|
h[field] = {
'graphql.field.parent' => field.owner&.graphql_name,
'graphql.field.name' => field.graphql_name,
'graphql.lazy' => true
}.freeze
end
@_otel_lazy_field_attrs_cache.compare_by_identity

super
end

Expand Down Expand Up @@ -73,26 +111,18 @@ def execute_query_lazy(query:, multiplex:, &block)

def execute_field(field:, query:, ast_node:, arguments:, object:, &block)
platform_key = _otel_execute_field_key(field: field)
return super unless platform_key
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't explain why, but replacing this super call (and the one below in execute_field_lazy) made up a big part of the memory improvement. In the "before" profile, these lines appear in the result:

       345  /Users/rmosolgo/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/graphql-2.1.6/lib/graphql/schema/object.rb:82
       345  /Users/rmosolgo/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/opentelemetry-instrumentation-graphql-0.26.7/lib/opentelemetry/instrumentation/graphql/tracers/graphql_trace.rb:104
       344  /Users/rmosolgo/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/graphql-2.1.6/lib/graphql/execution/interpreter/runtime.rb:600
       344  /Users/rmosolgo/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/opentelemetry-instrumentation-graphql-0.26.7/lib/opentelemetry/instrumentation/graphql/tracers/graphql_trace.rb:76
       343  otel_bench.rb:104

But adding these arguments caused them to not appear anymore.

Looking at the breakdown by class, my only guess is that, somewhere under the hood, Ruby was making a Hash as part of this code:

  allocated objects by class
  -----------------------------------
-     6230  Hash
+     5126  Hash
      2395  Array
-     1188  Proc
+     1194  Proc
-      871  String
+      875  String
       785  Thread::Mutex
       784  OpenTelemetry::Context
       784  OpenTelemetry::SDK::Trace::Samplers::Result
       784  OpenTelemetry::SDK::Trace::Span
       784  OpenTelemetry::Trace::SpanContext
       423  GraphQL::Execution::Lazy
       345  GraphQL::Execution::Interpreter::Runtime::GraphQLResultHash
       301  MySchema::Book

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing this. I wonder if there is a performance Rubocop linter we can leverage here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is the rubocop-performance gem, but after doing a quick test with the current instrumentation, these issues weren't pointed out.

return super(field: field, query: query, ast_node: ast_node, object: object, arguments: arguments, &block) unless platform_key

attributes = {
'graphql.field.parent' => field.owner&.graphql_name,
'graphql.field.name' => field.graphql_name,
'graphql.lazy' => false
}
attributes = @_otel_field_attrs_cache[field]

tracer.in_span(platform_key, attributes: attributes, &block)
end

def execute_field_lazy(field:, query:, ast_node:, arguments:, object:, &block)
platform_key = _otel_execute_field_key(field: field)
return super unless platform_key
return super(field: field, query: query, ast_node: ast_node, object: object, arguments: arguments, &block) unless platform_key

attributes = {
'graphql.field.parent' => field.owner&.graphql_name,
'graphql.field.name' => field.graphql_name,
'graphql.lazy' => true
}
attributes = @_otel_lazy_field_attrs_cache[field]

tracer.in_span(platform_key, attributes: attributes, &block)
end
Expand All @@ -101,10 +131,7 @@ def authorized(query:, type:, object:, &block)
platform_key = @_otel_authorized_key_cache[type]
return super unless platform_key

attributes = {
'graphql.type.name' => type.graphql_name,
'graphql.lazy' => false
}
attributes = @_otel_type_attrs_cache[type]

tracer.in_span(platform_key, attributes: attributes, &block)
end
Expand All @@ -113,33 +140,19 @@ def authorized_lazy(query:, type:, object:, &block)
platform_key = @_otel_authorized_key_cache[type]
return super unless platform_key

attributes = {
'graphql.type.name' => type.graphql_name,
'graphql.lazy' => true
}

attributes = @_otel_lazy_type_attrs_cache[type]
tracer.in_span(platform_key, attributes: attributes, &block)
end

def resolve_type(query:, type:, object:, &block)
platform_key = @_otel_resolve_type_key_cache[type]

attributes = {
'graphql.type.name' => type.graphql_name,
'graphql.lazy' => false
}

attributes = @_otel_type_attrs_cache[type]
tracer.in_span(platform_key, attributes: attributes, &block)
end

def resolve_type_lazy(query:, type:, object:, &block)
platform_key = @_otel_resolve_type_key_cache[type]

attributes = {
'graphql.type.name' => type.graphql_name,
'graphql.lazy' => true
}

attributes = @_otel_lazy_type_attrs_cache[type]
tracer.in_span(platform_key, attributes: attributes, &block)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,24 +86,59 @@ def config
end

def attributes_for(key, data)
attributes = {}
case key
when 'execute_field', 'execute_field_lazy'
attributes['graphql.field.parent'] = data[:owner]&.graphql_name # owner is the concrete type, not interface
attributes['graphql.field.name'] = data[:field]&.graphql_name
attributes['graphql.lazy'] = key == 'execute_field_lazy'
when 'authorized', 'authorized_lazy'
attributes['graphql.type.name'] = data[:type]&.graphql_name
attributes['graphql.lazy'] = key == 'authorized_lazy'
when 'resolve_type', 'resolve_type_lazy'
attributes['graphql.type.name'] = data[:type]&.graphql_name
attributes['graphql.lazy'] = key == 'resolve_type_lazy'
when 'execute_field'
field_attr_cache = data[:query].context.namespace(:otel_attrs)[:execute_field_attrs] ||= attr_cache do |field|
{
'graphql.field.parent' => field.owner.graphql_name,
'graphql.field.name' => field.graphql_name,
'graphql.lazy' => false
}.freeze
end
field_attr_cache[data[:field]]
when 'execute_field_lazy'
lazy_field_attr_cache = data[:query].context.namespace(:otel_attrs)[:execute_field_lazy_attrs] ||= attr_cache do |field|
{
'graphql.field.parent' => field.owner.graphql_name,
'graphql.field.name' => field.graphql_name,
'graphql.lazy' => true
}.freeze
end
lazy_field_attr_cache[data[:field]]
when 'authorized', 'resolve_type'
type_attrs_cache = data[:context].namespace(:otel_attrs)[:type_attrs] ||= attr_cache do |type|
{
'graphql.type.name' => type.graphql_name,
'graphql.lazy' => false
}.freeze
end
type_attrs_cache[data[:type]]
when 'authorized_lazy', 'resolve_type_lazy'
type_lazy_attrs_cache = data[:context].namespace(:otel_attrs)[:type_lazy_attrs] ||= attr_cache do |type|
{
'graphql.type.name' => type.graphql_name,
'graphql.lazy' => true
}
end
type_lazy_attrs_cache[data[:type]]
when 'execute_query'
attributes = {
'graphql.document' => data[:query].query_string,
'graphql.operation.type' => data[:query].selected_operation.operation_type
}
attributes['graphql.operation.name'] = data[:query].selected_operation_name if data[:query].selected_operation_name
attributes['graphql.operation.type'] = data[:query].selected_operation.operation_type
attributes['graphql.document'] = data[:query].query_string
attributes
else
{}
end
end

def attr_cache
cache_h = Hash.new do |h, k|
h[k] = yield(k)
end
attributes
cache_h.compare_by_identity
cache_h
end
end
end
Expand Down
Loading