-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Use a recursive descent parser #4718
Conversation
So far, it's not as fast as the C parser (looking at the introspection query), but it does make fewer allocations (presumably due to the new lexer/token design).
Parser Benchmark
require "bundler/inline"
gemfile do
gem "graphql", path: "./"
gem "graphql-c_parser"
gem "benchmark-ips"
end
puts RUBY_DESCRIPTION
query_str = GraphQL::Introspection::INTROSPECTION_QUERY
Benchmark.ips do |x|
x.report("Old Parse") { GraphQL::Language::Parser.parse(query_str) }
x.report("New Parse") { GraphQL::Language::RecursiveDescentParser.parse(query_str) }
x.report("C Parse") { GraphQL::CParser.parse(query_str) }
x.compare!
end
def check_allocations(label)
prev = GC.stat(:total_allocated_objects)
yield
allocations = GC.stat(:total_allocated_objects) - prev
puts "Allocations: #{label}: #{allocations}"
end
check_allocations("Old Parse") { GraphQL::Language::Parser.parse(query_str) }
check_allocations("New Parse") { GraphQL::Language::RecursiveDescentParser.parse(query_str) }
check_allocations("C Parse") { GraphQL::CParser.parse(query_str) } |
1175e68
to
4f6e18b
Compare
Is this on Rosetta? If so, I'd try on ARM or an Intel. Also, I think I only tested with YJIT on Ruby 3.3 and we've made very good improvements there, so it might be worth testing. |
Also, one thing I'd suggest (though it might be outside the scope of this PR) is to refactor the constructors for nodes. It looks like they take a hash and then do a bunch of stuff. If these are changed to positional parameters that just set IVs (as they are in TinyGQL), then node allocation will be faster. Of course this will make constructing a nodes more cumbersome, but I'd posit that nobody actually manually allocates a node (it's all done via the parser). |
Agreed -- that's kind of what I meant by my last todo item, unintelligible as it was. I think the C Parser will also benefit from that. I saw how TinyGQL uses |
Please take any of it you find helpful. The code is all yours! 😄 |
…for keywords used as names; improve error messages
I tried the refactor on Node initialization, and maybe it's faster, but it didn't remove the allocation for the hash :S I think I'm learning that, for And I wrote a simple script to demonstrate the issue: require "memory_profiler"
class A; def initialize(a: nil); end; end
report = MemoryProfiler.report do
A.new(a: 1)
end
puts report.pretty_print
# allocated objects by class
# -----------------------------------
# 1 A
# 1 Hash
report = MemoryProfiler.report do
A.new
end
puts report.pretty_print
# allocated objects by class
# -----------------------------------
# 1 A So although simplifying that flow might make things faster, I'll have to get away from keywords in order to get rid of the hash allocations. |
|
Comparing vs. master now that I've replaced the old parser with the new one: ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [x86_64-darwin22]
Calculating -------------------------------------
- Ruby Parse 1.536k (± 2.3%) i/s - 7.803k in 5.083115s
+ Ruby Parse 8.558k (± 2.6%) i/s - 42.800k in 5.004758s
- C Parse 10.939k (± 1.5%) i/s - 54.800k in 5.010856s
+ C Parse 11.789k (± 6.3%) i/s - 59.731k in 5.090992s
- Allocations: Ruby Parse: 1150
+ Allocations: Ruby Parse: 261
Allocations: C Parse: 443
|
@@ -15,6 +15,7 @@ def initialize(multiplex: nil, query: nil, **_options) | |||
@query = query | |||
end | |||
|
|||
# The Ruby parser doesn't call this method (`graphql/c_parser` does.) |
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.
❤️ the new parser
Wish this behaviour was more widely communicated given the Ruby parser is the default (our CI started failing)
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.
Hey, sorry for the nasty surprise -- at least I added a changelog entry for this change in 7c3d899
@rmosolgo The OTel Instrumentation test suite started to fail starting with I see that this PR includes an environment variable that will run different versions of the parser. This may be something we have to include in our test suite as well. Would you mind giving me a hand with OTel Instrumentation and seeing what the best way forward is here? open-telemetry/opentelemetry-ruby-contrib#773 |
Yes. A client would have to In terms of tracing tests expecting def trace_lex_supported?
return @trace_lex_supported if defined?(@trace_lex_supported)
@trace_lex_supported = Gem::Requirement.new("< 2.2").satisfied_by?(Gem::Version.new(GraphQL::VERSION))
end |
Thanks for sharing what worked for you, @ravangen, LGTM 👍 |
I'm interested in incorporating optimizations from @tenderlove's work in:
It will be a bit of work because I want to maintain GraphQL-Ruby's
comment tracking (required in order to losslessly parse and dump schemas, for example) and retain(didn't happen -- just using strings now) compatibility for AST nodes. If that all works, then I will replaceGraphQL::Language::Parser
withGraphQL::Language::RecursiveDescentParser
before I merge this branch.Also, I think this will provide an opportunity to improve parse error messages.
Note
Fixes #4646