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

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

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

rmosolgo
Copy link
Owner

@rmosolgo rmosolgo commented Nov 12, 2024

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:

  • Continue supporting lazy-loading
  • Support thread-safe preloading in production
  • Be as fast as Warden when validating lots of queries

I wrote a benchmark based on #5151:

Validation benchmark, Warden vs Schema::Visibility

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

  • Add schema-level cache for type => possible_types and use it Visibility::Profile
  • Add schema-level cache for type => references and use it
  • interface => interface_implementors
  • What caches can be removed from Profile? remove def references?
  • Reuse Schema::Visibility::Visit in Profile instead of hand-rolled visit code
  • Consider how TopLevel interacts with preload: true and preload: false
  • Unit-test TopLevel (and rename?) to make sure lazy-loading works as expected
  • Make top-level caches incrementally addable instead of refresh: true
  • Does this remove the need for top_level_profile?

Fixes #5151

@rmosolgo rmosolgo added this to the 2.4.4 milestone Nov 12, 2024
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.

Process hang during graphql-ruby-client sync with GraphQL::Schema::Visibility
1 participant