2020-08-22
GraphQL Rubyの高速化をしようとして、うまくいかなかった。
うまくいかなかったパッチは以下。
diff --git a/lib/graphql/filter.rb b/lib/graphql/filter.rb index 250a53ba7..0d1c791be 100644 --- a/lib/graphql/filter.rb +++ b/lib/graphql/filter.rb @@ -20,6 +20,10 @@ module GraphQL self.class.new(only: merged_only, except: merged_except) end + def do_nothing? + !(@only || @except) + end + private class MergedOnly diff --git a/lib/graphql/schema.rb b/lib/graphql/schema.rb index df3e83581..014d66890 100644 --- a/lib/graphql/schema.rb +++ b/lib/graphql/schema.rb @@ -286,7 +286,7 @@ module GraphQL @query_execution_strategy = self.class.default_execution_strategy @mutation_execution_strategy = self.class.default_execution_strategy @subscription_execution_strategy = self.class.default_execution_strategy - @default_mask = GraphQL::Schema::NullMask + @default_mask = nil @rebuilding_artifacts = false @context_class = GraphQL::Query::Context @introspection_namespace = nil @@ -918,7 +918,7 @@ module GraphQL if new_mask @own_default_mask = new_mask else - @own_default_mask || find_inherited_value(:default_mask, Schema::NullMask) + @own_default_mask || find_inherited_value(:default_mask, nil) end end diff --git a/lib/graphql/schema/warden.rb b/lib/graphql/schema/warden.rb index 17b5a8d15..aff4c77b3 100644 --- a/lib/graphql/schema/warden.rb +++ b/lib/graphql/schema/warden.rb @@ -37,10 +37,93 @@ module GraphQL # # @api private class Warden + # It has the same interface of Warden, but filter nothing. + class NullWarden + # @param filter [<#call(member)>] Objects are hidden when `.call(member, ctx)` returns true + # @param context [GraphQL::Query::Context] + # @param schema [GraphQL::Schema] + def initialize(filter, context:, schema:) + @schema = schema.interpreter? ? schema : schema.graphql_definition + @context = context + end + + # @return [Hash<String, GraphQL::BaseType>] Visible types in the schema + def types + @schema.types + end + + # @return [GraphQL::BaseType, nil] The type named `type_name`, if it exists (else `nil`) + def get_type(type_name) + @schema.get_type(type_name) + end + + # @return [Array<GraphQL::BaseType>] Visible and reachable types in the schema + def reachable_types + @reachable_types ||= @schema.types.values + end + + # @return Boolean True if the type is visible and reachable in the schema + def reachable_type?(type_name) + true + end + + # @return [GraphQL::Field, nil] The field named `field_name` on `parent_type`, if it exists + def get_field(parent_type, field_name) + @schema.get_field(parent_type, field_name) + end + + # @return [GraphQL::Argument, nil] The argument named `argument_name` on `parent_type`, if it exists and is visible + def get_argument(parent_type, argument_name) + parent_type.get_argument(argument_name) + end + + # @return [Array<GraphQL::BaseType>] The types which may be member of `type_defn` + def possible_types(type_defn) + @schema.possible_types(type_defn, @context) + end + + # @param type_defn [GraphQL::ObjectType, GraphQL::InterfaceType] + # @return [Array<GraphQL::Field>] Fields on `type_defn` + def fields(type_defn) + @schema.get_fields(type_defn).values + end + + # @param argument_owner [GraphQL::Field, GraphQL::InputObjectType] + # @return [Array<GraphQL::Argument>] Visible arguments on `argument_owner` + def arguments(argument_owner) + argument_owner.arguments.values + end + + # @return [Array<GraphQL::EnumType::EnumValue>] Visible members of `enum_defn` + def enum_values(enum_defn) + enum_defn.values.values + end + + # @return [Array<GraphQL::InterfaceType>] Visible interfaces implemented by `obj_type` + def interfaces(obj_type) + obj_type.interfaces(@context) + end + + def directives + @schema.directives.values + end + + def root_type_for_operation(op_name) + @schema.root_type_for_operation(op_name) + end + end + + def self.new(filter, context:, schema:) + if filter.do_nothing? + NullWarden.new(filter, context: context, schema: schema) + else + super + end + end + # @param filter [<#call(member)>] Objects are hidden when `.call(member, ctx)` returns true # @param context [GraphQL::Query::Context] # @param schema [GraphQL::Schema] - # @param deep_check [Boolean] def initialize(filter, context:, schema:) @schema = schema.interpreter? ? schema : schema.graphql_definition # Cache these to avoid repeated hits to the inheritance chain when one isn't present @@ -51,7 +134,7 @@ module GraphQL @visibility_cache = read_through { |m| filter.call(m, context) } end - # @return [Array<GraphQL::BaseType>] Visible types in the schema + # @return [Hash<String, GraphQL::BaseType>] Visible types in the schema def types @types ||= begin vis_types = {}
GraphQL Ruby では Warden というクラスで権限チェックができるのだけど、プロファイルをしてみるとそこそこ時間がかかっている。 なので特に権限チェックを設定していなければ何もしないようにすれば速くなるのでは、と思って実装してみた。
実装して1.25倍ぐらい速くなることまで確認はできたのだけど、切り替える条件がむずい。パッチではFilter#do_nothing?
を見ているけど、これだと不十分。
なぜならば Filter は常に only
に @schmea.method(:visible?)
が指定されているため。
これでうーん分からんなとなって諦めた。パッチのライセンスはCC-0とするので、チャレンジしたい人がいたらどうぞ。
この高速化を見ていたら、GraphQL::Schema::Warden
にドキュメントのミスを見つけたので直した。
適当にベンチマークをとると、GraphQL::StaticValidation::Validator#validate
に結構な時間がかかっているのでこれをキャッシュしたら速くなりそう。
Rails appでcurrentUserのfullNameを取得するだけのクエリを対象にベンチマークをとっても、wall click time で10%ぐらいはこれに食われている。
内部APIとして使っていれば必然的にクエリの種類は限られてくるので、同じクエリが何度もリクエストされる。なので毎回同じクエリをvalidationするのは無駄。
ただ、いい感じにキャッシュを差し込めるインターフェースがなさそうなので、提案するしかないかなあ。
GraphQL::Query#fingerprint
というメソッドがあるのだけど、digest/sha2
ライブラリがロードされていない環境で動かなかったので明示的にrequireを足した。
まあRailsなりなんなりが実際はrequireしているだろうし、実際に踏むことは少なさそう。
色々眺めていてfingerprint
メソッドを呼んでみたらエラーになったので気がついた。
GraphQL Rubyで、Frozen String Literalのコメントがrequire
の後に置かれていて効いていなかったので直した。