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
```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
```
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:
So, there are still some wrinkles to smooth out, but it's heading the right direction!
TODO
type => possible_types
and use itVisibility::Profile
type => references
and use itinterface => interface_implementors
〃Profile
? removedef references?
Schema::Visibility::Visit
inProfile
instead of hand-rolled visit codeTopLevel
interacts withpreload: true
andpreload: false
TopLevel
(and rename?) to make sure lazy-loading works as expectedrefresh: true
top_level_profile
?Fixes #5151