rmosolgo / graphql-ruby

Ruby implementation of GraphQL
http://graphql-ruby.org
MIT License
5.38k stars 1.39k forks source link

Add top-level schema caches to Schema::Visibility for better performance #5161

Closed rmosolgo closed 4 days ago

rmosolgo commented 1 week ago

In order to support lazy-loading in development, I had bypassed the existing top-level caches for types, possible types, and schema references. But, that turned out to be noticeably slow when validating lots of queries all at once. So in this PR, I'm trying to have it all:

I wrote a benchmark based on #5151:

Validation benchmark, Warden vs Schema::Visibility

```ruby require "bundler/inline" gemfile do gem "graphql", path: "~/code/graphql-ruby" # gem "graphql", "2.4.3" gem "graphql-pro", path: "./" # gem "graphql-pro", "1.29.2" gem "benchmark-ips" gem "stackprof" gem "memory_profiler" end puts "GraphQL-Ruby: #{GraphQL::VERSION} / GraphQL-Pro: #{GraphQL::Pro::VERSION}" WardenSchema = GraphQL::Schema.from_definition <<-GRAPHQL schema { query: Query mutation: Mutation } # The query type, represents all of the entry points into our object graph type Query { hero(episode: Episode): Character reviews(episode: Episode!): [Review] search(text: String): [SearchResult] character(id: ID!): Character droid(id: ID!): Droid human(id: ID!): Human starship(id: ID!): Starship } # The mutation type, represents all updates we can make to our data type Mutation { createReview(episode: Episode, review: ReviewInput!): Review } # The episodes in the Star Wars trilogy enum Episode { # Star Wars Episode IV: A New Hope, released in 1977. NEWHOPE # Star Wars Episode V: The Empire Strikes Back, released in 1980. EMPIRE # Star Wars Episode VI: Return of the Jedi, released in 1983. JEDI } # A character from the Star Wars universe interface Character { # The ID of the character id: ID! # The name of the character name: String! # The friends of the character, or an empty list if they have none friends: [Character] # The friends of the character exposed as a connection with edges friendsConnection(first: Int, after: ID): FriendsConnection! # The movies this character appears in appearsIn: [Episode]! } # Units of height enum LengthUnit { # The standard unit around the world METER # Primarily used in the United States FOOT # Ancient unit used during the Middle Ages CUBIT @deprecated(reason: "Test deprecated enum case") } # A humanoid creature from the Star Wars universe type Human implements Character { # The ID of the human id: ID! # What this human calls themselves name: String! # The home planet of the human, or null if unknown homePlanet: String # Height in the preferred unit, default is meters height(unit: LengthUnit = METER): Float # Mass in kilograms, or null if unknown mass: Float # This human's friends, or an empty list if they have none friends: [Character] # The friends of the human exposed as a connection with edges friendsConnection(first: Int, after: ID): FriendsConnection! # The movies this human appears in appearsIn: [Episode]! # A list of starships this person has piloted, or an empty list if none starships: [Starship] } # An autonomous mechanical character in the Star Wars universe type Droid implements Character { # The ID of the droid id: ID! # What others call this droid name: String! # This droid's friends, or an empty list if they have none friends: [Character] # The friends of the droid exposed as a connection with edges friendsConnection(first: Int, after: ID): FriendsConnection! # The movies this droid appears in appearsIn: [Episode]! # This droid's primary function primaryFunction: String } # A connection object for a character's friends type FriendsConnection { # The total number of friends totalCount: Int # The edges for each of the character's friends. edges: [FriendsEdge] # A list of the friends, as a convenience when edges are not needed. friends: [Character] # Information for paginating this connection pageInfo: PageInfo! } # An edge object for a character's friends type FriendsEdge { # A cursor used for pagination cursor: ID! # The character represented by this friendship edge node: Character } # Information for paginating this connection type PageInfo { startCursor: ID endCursor: ID hasNextPage: Boolean! } # Represents a review for a movie type Review { # The number of stars this review gave, 1-5 stars: Int! # Comment about the movie commentary: String } # The input object sent when someone is creating a new review input ReviewInput { # 0-5 stars stars: Int! # Comment about the movie, optional commentary: String # Favorite color, optional favorite_color: ColorInput } # The input object sent when passing in a color input ColorInput { red: Int! green: Int! blue: Int! } type Starship { # The ID of the starship id: ID! # The name of the starship name: String! # Length of the starship, along the longest axis length(unit: LengthUnit = METER): Float coordinates: [[Float!]!] } union SearchResult = Human | Droid | Starship GRAPHQL doc1 = GraphQL.parse <<~GRAPHQL query HeroAndFriendsNames($episode: Episode) { hero(episode: $episode) { name appearsIn friends { name } } } GRAPHQL VisibilitySchema = Class.new(WardenSchema) VisibilitySchema.use(GraphQL::Schema::Visibility) doc2 = GraphQL.parse(GraphQL::Introspection::INTROSPECTION_QUERY) # Warm-up: GraphQL::Pro::OperationStore::Validate.validate(VisibilitySchema, doc1, client_name: "foo") GraphQL::Pro::OperationStore::Validate.validate(WardenSchema, doc1, client_name: "foo") GraphQL::Pro::OperationStore::Validate.validate(VisibilitySchema, doc2, client_name: "foo") GraphQL::Pro::OperationStore::Validate.validate(WardenSchema, doc2, client_name: "foo") if ENV["IPS"] Benchmark.ips do |x| x.report("Visibility 1") { GraphQL::Pro::OperationStore::Validate.validate(VisibilitySchema, doc1, client_name: "foo") } x.report("Warden 1") { GraphQL::Pro::OperationStore::Validate.validate(WardenSchema, doc1, client_name: "foo") } x.report("Visibility 2") { GraphQL::Pro::OperationStore::Validate.validate(VisibilitySchema, doc2, client_name: "foo") } x.report("Warden 2") { GraphQL::Pro::OperationStore::Validate.validate(WardenSchema, doc2, client_name: "foo") } x.compare! end end if ENV["PROF"] GC.start prof_doc = doc1 StackProf.run(mode: :wall, interval: 1, out: 'tmp/warden-validate.dump', raw: true) do GraphQL::Pro::OperationStore::Validate.validate(WardenSchema, prof_doc, client_name: "foo") end StackProf.run(mode: :wall, interval: 1, out: 'tmp/validate.dump', raw: true) do GraphQL::Pro::OperationStore::Validate.validate(VisibilitySchema, prof_doc, client_name: "foo") end end if ENV["MEM"] GC.start report = MemoryProfiler.report do GraphQL::Pro::OperationStore::Validate.validate(VisibilitySchema, doc2, client_name: "foo") end puts report.pretty_print end ```

And I got big (10x +) performance gains so far:

  Calculating -------------------------------------
-        Visibility 1    205.565 (± 3.4%) i/s    (4.86 ms/i) -      1.040k in   5.065163s
+        Visibility 1      2.590k (± 6.3%) i/s  (386.13 μs/i) -     13.000k in   5.041105s
            Warden 1      2.554k (± 3.9%) i/s  (391.50 μs/i) -     12.852k in   5.039105s
-        Visibility 2     25.223 (± 4.0%) i/s   (39.65 ms/i) -    126.000 in   5.002731s
+        Visibility 2    827.604 (± 3.9%) i/s    (1.21 ms/i) -      4.212k in   5.097079s
            Warden 2    754.498 (± 3.3%) i/s    (1.33 ms/i) -      3.800k in   5.042172s

So, there are still some wrinkles to smooth out, but it's heading the right direction!

TODO

Fixes #5151