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

Speed up one_of? #4680

Merged
merged 1 commit into from
Oct 24, 2023
Merged

Speed up one_of? #4680

merged 1 commit into from
Oct 24, 2023

Conversation

jbourassa
Copy link
Contributor

@jbourassa jbourassa commented Oct 20, 2023

Speed up one_of? by defining it as false, and when one_of is used, redefining the method as true.

In profiles, I noticed that oneOf? was taking some time. Looping through the directive list each time feels wasteful.

This approach borrows from Rails' Class#class_attribute and redefines the one_of? when calling the one_of class method. This respects the fact that directives are inherited from parent classes; so is the one_of? class method. That said, this is not 100% backward compatible because users could be adding the OneOf directive without using the one_of DSL, but... it still feels pretty safe?

I didn't see a benchmark that had a lot of input, and in rake bench:query, the results are well within the margin of error. In a benchmark in our app though, where we're only validating a Hash against a GraphQL type, this made a difference.

@jbourassa
Copy link
Contributor Author

Thanks for running the tests, I thought they passed but I must’ve missed something.

I’ll take a look. Marking this as draft until then.

@jbourassa jbourassa marked this pull request as draft October 21, 2023 12:41
@jbourassa
Copy link
Contributor Author

The tests failures were cases where the OneOf directive was added explicitly without calling one_of. That's the incompatibility risk I called out in the description 😅. I fixed those.

I also built a benchmark to show the gains:

Before:

  Calculating -------------------------------------
  input list - 10 elem      3.145k (± 1.4%) i/s -     15.750k in   5.008840s
  input list - 100 elem
                          726.600  (± 1.5%) i/s -      3.672k in   5.054912s
  input list - 1000 elem
                           83.429  (± 1.2%) i/s -    424.000  in   5.083546s

After:

  Calculating -------------------------------------
  input list - 10 elem      3.279k (± 2.2%) i/s -     16.500k in   5.034949s
  input list - 100 elem
                          808.152  (± 1.5%) i/s -      4.080k in   5.049768s
  input list - 1000 elem
                           94.321  (± 2.1%) i/s -    477.000  in   5.058867s

@jbourassa jbourassa marked this pull request as ready for review October 23, 2023 15:03
@rmosolgo
Copy link
Owner

Hey, thanks for this suggestion. I'm all for performance improvements. The only thing that gives me the willies is define_method. I've had enough bad experiences where it has unexpected performance impacts that I try to steer clear of it whenever I can. I can think of an alternative suggestion, tell me what you think:

  • Keep defining def one_of? to return false

  • Add a new module (say, Directive::OneOf::IsOneOf 🤷 ) that implements def one_of? to return true

  • In Directive::OneOf#initialize, call @owner.extend ... with that new module

    (#initialize receives the schema member which is getting the directive:

    def initialize(owner, **arguments)
    @owner = owner

That would help me sleep better at night -- avoiding define_method 😅 -- and I think it would also retain compatibility where the directive is added manually. What do you think of that approach?

@jbourassa
Copy link
Contributor Author

That sounds like a sensible approach to me. In this case I think the perf hit from define_method is next to none (or at least, I didn't notice anything after migrating to from another approach in my in-app monkey patch), but the incompatibility is unfortunate.

The tradeoff is that one_of? definitions are farther apart thus a little less obvious, but that's price worth paying.

@rmosolgo
Copy link
Owner

rmosolgo commented Oct 23, 2023

perf hit

I can believe it, but, what about, say, the binding? Ruby won't GC any local variables in scope because maybe they'll be used by the dynamically-defined method later on. (At least, it used to work that way. Maybe it's smarter now?) I've traced big memory bloat to stuff like that in the past -- and even if there's nothing of consequence in the local scope now, maybe something would get added later on...

@jbourassa
Copy link
Contributor Author

Updated with your suggestion 👍, tests should be green, at least they were on my machine.

Speed up `one_of?` by defining it as `false`, and when `one_of` is used,
redefining the method as `true`.

In profiles, I noticed that `oneOf?` was taking _some_ time. Looping
through the directive list each time feels wasteful.

This approach borrows from Rails' `Class#class_attribute` and redefines
the `one_of?` when calling the `one_of` class method. This respects the
fact that directives are inherited from parent classes; so is the
`one_of?` class method. That said, this is not 100% backward compatible
because users could be adding the OneOf directive without using the
`one_of` DSL, but... it still feels pretty safe?
@@ -6,6 +6,18 @@ class OneOf < GraphQL::Schema::Directive
description "Requires that exactly one field must be supplied and that field must not be `null`."
locations(GraphQL::Schema::Directive::INPUT_OBJECT)
default_directive true

def initialize(...)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The gem now supports Ruby 2.7+, so this should work. Feel free to change it to something else if you prefer.

Copy link
Owner

Choose a reason for hiding this comment

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

🙀 Beautiful, I bet there are many other places in this library that would benefit from this. What a time to be alive!

@rmosolgo
Copy link
Owner

Thanks for this improvement!

@rmosolgo rmosolgo merged commit 5a15efd into rmosolgo:master Oct 24, 2023
12 checks passed
@rmosolgo rmosolgo added this to the 2.1.4 milestone Oct 24, 2023
@jbourassa jbourassa deleted the faster-one-of-query branch March 11, 2024 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants