-
Notifications
You must be signed in to change notification settings - Fork 179
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
perf(graphql): cache attribute hashes #723
Conversation
@@ -73,26 +126,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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@rmosolgo TIL about Is this allocation problem specific to OTel or have you also identified this problem with the existing Tracers that included in the graphql gem? I would like to confirm with you that these types will not change at runtime, i.e. there isn't any logic that would result in dynamically generated types or Module instances that would result in the hashes growing out of control. Is that correct? Also, do you have any concerns about thread safe access to these cache Hashes in JRuby? Is there a way to eagerly seed the hashes at startup as opposed to using lazy initialization? |
Broadly speaking, it's definitely a general problem. The reason I re-wrote GraphQL-Ruby's tracing to use methods with keyword arguments was to avoid allocating hashes for the old As for the built-in tracers, I definitely try to keep allocations in these paths to a minimum, although I know the datadog one is not so great 🙈 .
It is technically possible to create GraphQL types on-the-fly (https://graphql-ruby.org/schema/dynamic_types.html), but in any case, these hashes only live for the duration of a single query. The
I don't have specific insight into this, but GraphQL-Ruby uses tons of caches just like this:
And of course, the existing platform_*_key hashes work this way, too. Since those work properly, I'm guessing these will too!
It might be possible to do this via
Yes, it's new to me too, but I've started using it all over GraphQL-Ruby for this kind of cache! It ends up saving a good bit of time on hot paths, when you know the keys will be exactly the same object (eg rmosolgo/graphql-ruby#4431 (comment), rmosolgo/graphql-ruby#4443) |
I appreciate the context and explanations. I'll take a look in later today if I have enough time. I'll be OOO until Monday. This review will be at the top of my list if I am unable to get to it today. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I see. I did not realize these are following the existing caching patterns that exist for field
, authorize
and resolve_type
.
The difference being that it uses compare_by_identiy
and adds support for caching lazy types.
Is there a similar optimization that can be done for the older tracer implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rmosolgo it looks like the tests for the legacy tracer are failing now.
I did not mean for my comments to have you optimize the legacy tracer as part of this PR and I am amenable to deferring optimizations of the legacy tracer as part of a separate one.
Since the test suite is broken I will change the review status to "Request Changes" until that is fixed.
Derp -- I'll revert those changes. It looks like that tracer expects a slightly different behavior when tracing fields that are defined in an interface module. |
This is to reduce memory consumption by this tracer when running GraphQL queries. In my benchmark (below), this resulted in a 97% reduction in this library's footprint and a 10% reduction overall (~7% of objects).
otel_bench.rb
I also added
.compare_by_identity
to these caches because they always receive the same objects -- GraphQL definition objects -- as keys.