diary

I like Hatena Star with a text selection.

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?)が指定されているため。

https://github.com/rmosolgo/graphql-ruby/blob/558a43fb1ce444d673267d241ea41b612f5b57c5/lib/graphql/language/document_from_schema_definition.rb#L26-L37

これでうーん分からんなとなって諦めた。パッチのライセンスはCC-0とするので、チャレンジしたい人がいたらどうぞ。


github.com

この高速化を見ていたら、GraphQL::Schema::Wardenにドキュメントのミスを見つけたので直した。


適当にベンチマークをとると、GraphQL::StaticValidation::Validator#validateに結構な時間がかかっているのでこれをキャッシュしたら速くなりそう。

Rails appでcurrentUserのfullNameを取得するだけのクエリを対象にベンチマークをとっても、wall click time で10%ぐらいはこれに食われている。

内部APIとして使っていれば必然的にクエリの種類は限られてくるので、同じクエリが何度もリクエストされる。なので毎回同じクエリをvalidationするのは無駄。

ただ、いい感じにキャッシュを差し込めるインターフェースがなさそうなので、提案するしかないかなあ。


github.com

GraphQL::Query#fingerprintというメソッドがあるのだけど、digest/sha2ライブラリがロードされていない環境で動かなかったので明示的にrequireを足した。 まあRailsなりなんなりが実際はrequireしているだろうし、実際に踏むことは少なさそう。

色々眺めていてfingerprintメソッドを呼んでみたらエラーになったので気がついた。


github.com

GraphQL Rubyで、Frozen String Literalのコメントがrequireの後に置かれていて効いていなかったので直した。