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
152 changes: 130 additions & 22 deletions lib/graphql/schema/visibility.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "graphql/schema/visibility/profile"
require "graphql/schema/visibility/migration"
require "graphql/schema/visibility/visit"

module GraphQL
class Schema
Expand All @@ -13,6 +14,9 @@ class Visibility
# @param migration_errors [Boolean] if `true`, raise an error when `Visibility` and `Warden` return different results
def self.use(schema, dynamic: false, profiles: EmptyObjects::EMPTY_HASH, preload: (defined?(Rails) ? Rails.env.production? : nil), migration_errors: false)
schema.visibility = self.new(schema, dynamic: dynamic, preload: preload, profiles: profiles, migration_errors: migration_errors)
if preload
schema.visibility.preload
end
end

def initialize(schema, dynamic:, preload:, profiles:, migration_errors:)
Expand All @@ -26,27 +30,54 @@ def initialize(schema, dynamic:, preload:, profiles:, migration_errors:)
@cached_profiles = {}
@dynamic = dynamic
@migration_errors = migration_errors
if preload
# Traverse the schema now (and in the *_configured hooks below)
# To make sure things are loaded during boot
@preloaded_types = Set.new
types_to_visit = [
@schema.query,
@schema.mutation,
@schema.subscription,
*@schema.introspection_system.types.values,
*@schema.introspection_system.entry_points.map { |ep| ep.type.unwrap },
*@schema.orphan_types,
]
# Root types may have been nil:
types_to_visit.compact!
ensure_all_loaded(types_to_visit)

profiles.each do |profile_name, example_ctx|
example_ctx[:visibility_profile] = profile_name
prof = profile_for(example_ctx, profile_name)
prof.all_types # force loading
end
# Top-level type caches:
@loaded_all = false
@interface_type_memberships = nil
@directives = nil
@types = nil
@references = nil
end

def all_directives
load_all
@directives
end

def all_interface_type_memberships
load_all
@interface_type_memberships
end

def all_references
load_all
@references
end

def get_type(type_name)
load_all
@types[type_name]
end

def preload
# Traverse the schema now (and in the *_configured hooks below)
# To make sure things are loaded during boot
@preloaded_types = Set.new
types_to_visit = [
@schema.query,
@schema.mutation,
@schema.subscription,
*@schema.introspection_system.types.values,
*@schema.introspection_system.entry_points.map { |ep| ep.type.unwrap },
*@schema.orphan_types,
]
# Root types may have been nil:
types_to_visit.compact!
ensure_all_loaded(types_to_visit)

@profiles.each do |profile_name, example_ctx|
example_ctx[:visibility_profile] = profile_name
prof = profile_for(example_ctx, profile_name)
prof.all_types # force loading
end
end

Expand Down Expand Up @@ -132,7 +163,10 @@ def profile_for(context, visibility_profile)
end
end

private
attr_reader :top_level

# @api private
attr_reader :unfiltered_interface_type_memberships

def top_level_profile(refresh: false)
if refresh
Expand All @@ -141,6 +175,8 @@ def top_level_profile(refresh: false)
@top_level_profile ||= @schema.visibility_profile_class.new(context: Query::NullContext.instance, schema: @schema)
end

private

def ensure_all_loaded(types_to_visit)
while (type = types_to_visit.shift)
if type.kind.fields? && @preloaded_types.add?(type)
Expand All @@ -151,8 +187,80 @@ def ensure_all_loaded(types_to_visit)
end
end
top_level_profile(refresh: true)
load_all(refresh: true)
nil
end

def load_all(refresh: false)
if refresh
@loaded_all = nil
end
@loaded_all ||= begin
@interface_type_memberships = Hash.new { |h, interface_type| h[interface_type] = [] }.compare_by_identity
@directives = []
@types = {} # String => Module
@references = Hash.new { |h, member| h[member] = [] }.compare_by_identity
visit = Visibility::Visit.new(@schema)

@schema.root_types.each do |t|
@references[t] << true
end

@schema.introspection_system.types.each_value do |t|
@references[t] << true
end

unions_for_references = []

visit.visit_each do |member|
if member.is_a?(Module)
type_name = member.graphql_name
if (prev_t = @types[type_name])
if prev_t.is_a?(Array)
prev_t << member
else
@types[type_name] = [member, prev_t]
end
else
@types[member.graphql_name] = member
end
if member < GraphQL::Schema::Directive
@directives << member
elsif member.respond_to?(:interface_type_memberships)
member.interface_type_memberships.each do |itm|
@references[itm.abstract_type] << member
@interface_type_memberships[itm.abstract_type] << itm
end
elsif member < GraphQL::Schema::Union
unions_for_references << member
end
elsif member.is_a?(GraphQL::Schema::Argument)
member.validate_default_value
@references[member.type.unwrap] << member
elsif member.is_a?(GraphQL::Schema::Field)
@references[member.type.unwrap] << member
end
true
end
@interface_type_memberships.each do |int_type, type_memberships|
referers = @references[int_type].select { |r| r.is_a?(GraphQL::Schema::Field) }
if referers.any?
type_memberships.each do |type_membership|
implementor_type = type_membership.object_type
@references[implementor_type].concat(referers)
end
end
end

unions_for_references.each do |union_type|
refs = @references[union_type]
union_type.all_possible_types.each do |object_type|
@references[object_type].concat(refs)
end
end
true
end
end
end
end
end
Loading
Loading