diff --git a/Rakefile b/Rakefile index 5e81be7..b2bd9f7 100644 --- a/Rakefile +++ b/Rakefile @@ -1,12 +1,42 @@ # frozen_string_literal: true -require 'rake/testtask' +require "bundler/gem_helper" +Bundler::GemHelper.install_tasks + +require "rake/testtask" Rake::TestTask.new(:test) do |t, args| puts args t.libs << "test" t.libs << "lib" - t.test_files = FileList['test/**/*_test.rb'] + t.test_files = FileList["test/**/*_test.rb"] +end + +# Load rake tasks from lib/tasks +Dir.glob("lib/tasks/*.rake").each { |r| load r } + +namespace :benchmark do + def prepare_benchmark + require_relative("./benchmark/run.rb") + end + + desc "Benchmark execution" + task :execution do + prepare_benchmark + GraphQLBenchmark.benchmark_execution + end + + desc "Benchmark lazy execution" + task :lazy_execution do + prepare_benchmark + GraphQLBenchmark.benchmark_lazy_execution + end + + desc "Memory profile" + task :memory do + prepare_benchmark + GraphQLBenchmark.memory_profile + end end -task :default => :test \ No newline at end of file +task default: :test diff --git a/benchmark/run.rb b/benchmark/run.rb new file mode 100644 index 0000000..e417f59 --- /dev/null +++ b/benchmark/run.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true +# +require "debug" +require "graphql" +require "graphql/batch" +require "graphql/cardinal" + +require "benchmark/ips" +require "memory_profiler" +require_relative '../test/fixtures' + +class GraphQLBenchmark + DOCUMENT = GraphQL.parse(BASIC_DOCUMENT) + CARDINAL_SCHEMA = SCHEMA + + class Schema < GraphQL::Schema + lazy_resolve(Proc, :call) + end + + class DataloaderSchema < GraphQL::Schema + use GraphQL::Dataloader + end + + class BatchLoaderSchema < GraphQL::Schema + use GraphQL::Batch + end + + GRAPHQL_GEM_SCHEMA = Schema.from_definition(SDL, default_resolve: GEM_RESOLVERS) + GRAPHQL_GEM_LAZY_SCHEMA = Schema.from_definition(SDL, default_resolve: GEM_LAZY_RESOLVERS) + GRAPHQL_GEM_DATALOADER_SCHEMA = DataloaderSchema.from_definition(SDL, default_resolve: GEM_DATALOADER_RESOLVERS) + GRAPHQL_GEM_DATALOADER_SCHEMA.use(GraphQL::Dataloader) + + GRAPHQL_GEM_BATCH_LOADER_SCHEMA = BatchLoaderSchema.from_definition(SDL, default_resolve: GEM_BATCH_LOADER_RESOLVERS) + GRAPHQL_GEM_BATCH_LOADER_SCHEMA.use(GraphQL::Batch) + + class << self + def benchmark_execution + default_data_sizes = "10, 100, 1000, 10000" + sizes = ENV.fetch("SIZES", default_data_sizes).split(",").map(&:to_i) + + with_data_sizes(sizes) do |data_source, num_objects| + Benchmark.ips do |x| + x.report("graphql-ruby: #{num_objects} resolvers") do + GRAPHQL_GEM_SCHEMA.execute(document: DOCUMENT, root_value: data_source) + end + + x.report("graphql-cardinal #{num_objects} resolvers") do + GraphQL::Cardinal::Executor.new( + SCHEMA, + BREADTH_RESOLVERS, + DOCUMENT, + data_source + ).perform + end + + x.compare! + end + end + end + + def benchmark_lazy_execution + default_data_sizes = "10, 100, 1000, 10000" + sizes = ENV.fetch("SIZES", default_data_sizes).split(",").map(&:to_i) + + with_data_sizes(sizes) do |data_source, num_objects| + Benchmark.ips do |x| + x.report("graphql-ruby lazy: #{num_objects} resolvers") do + GRAPHQL_GEM_LAZY_SCHEMA.execute(document: DOCUMENT, root_value: data_source) + end + + x.report("graphql-ruby dataloader: #{num_objects} resolvers") do + GRAPHQL_GEM_DATALOADER_SCHEMA.execute(document: DOCUMENT, root_value: data_source) + end + + x.report("graphql-ruby batch: #{num_objects} resolvers") do + GRAPHQL_GEM_BATCH_LOADER_SCHEMA.execute(document: DOCUMENT, root_value: data_source) + end + + x.report("graphql-cardinal: #{num_objects} lazy resolvers") do + GraphQL::Cardinal::Executor.new( + SCHEMA, + BREADTH_DEFERRED_RESOLVERS, + DOCUMENT, + data_source + ).perform + end + + x.compare! + end + end + end + + def memory_profile + default_data_sizes = "10, 1000" + sizes = ENV.fetch("SIZES", default_data_sizes).split(",").map(&:to_i) + + with_data_sizes(sizes) do |data_source, num_objects| + report = MemoryProfiler.report do + GRAPHQL_GEM_SCHEMA.execute(document: DOCUMENT, root_value: data_source) + end + + puts "\n\ngraphql-ruby memory profile: #{num_objects} resolvers" + puts "=" * 50 + report.pretty_print + end + + with_data_sizes(sizes) do |data_source, num_objects| + report = MemoryProfiler.report do + GraphQL::Cardinal::Executor.new( + SCHEMA, + BREADTH_RESOLVERS, + DOCUMENT, + data_source + ).perform + end + + puts "\n\ngraphql-cardinal memory profile: #{num_objects} resolvers" + puts "=" * 50 + report.pretty_print + end + end + + def with_data_sizes(sizes = [10]) + sizes.each do |size| + products = (1..size).map do |i| + { + "id" => i.to_s, + "title" => "Product #{i}", + "variants" => { + "nodes" => (1..5).map do |j| + { + "id" => "#{i}-#{j}", + "title" => "Variant #{j}" + } + end + } + } + end + + data = { + "products" => { + "nodes" => products + } + } + + num_objects = object_count(data) + + yield data, num_objects + end + end + + def object_count(obj) + case obj + when Hash + obj.size + obj.values.sum { |value| object_count(value) } + when Array + obj.sum { |item| object_count(item) } + else + 0 + end + end + end +end diff --git a/graphql-cardinal.gemspec b/graphql-cardinal.gemspec index db61662..04642d8 100644 --- a/graphql-cardinal.gemspec +++ b/graphql-cardinal.gemspec @@ -1,7 +1,6 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'graphql/cardinal/version' +# frozen_string_literal: true + +require_relative 'lib/graphql/cardinal/version' Gem::Specification.new do |spec| spec.name = 'graphql-cardinal' @@ -27,8 +26,13 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.add_runtime_dependency 'graphql', '>= 2.0' + spec.add_runtime_dependency 'ostruct' spec.add_development_dependency 'bundler', '~> 2.0' spec.add_development_dependency 'rake', '~> 12.0' spec.add_development_dependency 'minitest', '~> 5.12' + spec.add_development_dependency 'benchmark-ips', '~> 2.0' + spec.add_development_dependency 'memory_profiler' + spec.add_development_dependency 'debug' + spec.add_development_dependency 'graphql-batch' end diff --git a/test/fixtures.rb b/test/fixtures.rb index 9108ea6..eb8f338 100644 --- a/test/fixtures.rb +++ b/test/fixtures.rb @@ -1,4 +1,4 @@ -SCHEMA = GraphQL::Schema.from_definition(%| +SDL = <<~SCHEMA interface Node { id: ID! } @@ -47,7 +47,9 @@ type Mutation { writeValue(value: String!): WriteValuePayload } -|) +SCHEMA + +SCHEMA = GraphQL::Schema.from_definition(SDL) class WriteValueResolver < GraphQL::Cardinal::FieldResolver def resolve(objects, _args, _ctx, _scope) @@ -56,6 +58,42 @@ def resolve(objects, _args, _ctx, _scope) end end +class SimpleLoader < GraphQL::Cardinal::Loader + def perform(keys) + keys + end +end + +class DeferredHashResolver < GraphQL::Cardinal::FieldResolver + def initialize(key) + @key = key + end + + def resolve(objects, _args, _ctx, scope) + scope.defer(SimpleLoader, group: "a", keys: objects.map { _1[@key] }) + end +end + +class SimpleHashSource < GraphQL::Dataloader::Source + def initialize(hash) + @hash = hash + end + + def fetch(keys) + [@hash.fetch(keys.first)] + end +end + +class SimpleHashBatchLoader < GraphQL::Batch::Loader + def initialize(hash) + @hash = hash + end + + def perform(keys) + keys.each { |key| fulfill(key, @hash.fetch(key)) } + end +end + BREADTH_RESOLVERS = { "Node" => { "id" => GraphQL::Cardinal::HashKeyResolver.new("id"), @@ -100,6 +138,50 @@ def resolve(objects, _args, _ctx, _scope) }, }.freeze +BREADTH_DEFERRED_RESOLVERS = { + "Node" => { + "id" => DeferredHashResolver.new("id"), + "__type__" => ->(obj, ctx) { ctx[:query].get_type(obj["__typename__"]) }, + }, + "HasMetafields" => { + "metafield" => DeferredHashResolver.new("metafield"), + "__type__" => ->(obj, ctx) { ctx[:query].get_type(obj["__typename__"]) }, + }, + "Metafield" => { + "key" => DeferredHashResolver.new("key"), + "value" => DeferredHashResolver.new("value"), + }, + "Product" => { + "id" => DeferredHashResolver.new("id"), + "title" => DeferredHashResolver.new("title"), + "maybe" => DeferredHashResolver.new("maybe"), + "must" => DeferredHashResolver.new("must"), + "variants" => DeferredHashResolver.new("variants"), + "metafield" => DeferredHashResolver.new("metafield"), + }, + "ProductConnection" => { + "nodes" => DeferredHashResolver.new("nodes"), + }, + "Variant" => { + "id" => DeferredHashResolver.new("id"), + "title" => DeferredHashResolver.new("title"), + }, + "VariantConnection" => { + "nodes" => DeferredHashResolver.new("nodes"), + }, + "WriteValuePayload" => { + "value" => DeferredHashResolver.new("value"), + }, + "Query" => { + "products" => DeferredHashResolver.new("products"), + "nodes" => DeferredHashResolver.new("nodes"), + "node" => DeferredHashResolver.new("node"), + }, + "Mutation" => { + "writeValue" => WriteValueResolver.new, + }, +}.freeze + DEPTH_RESOLVERS = { "Product" => { "id" => ->(obj) { obj["id"] }, @@ -121,6 +203,90 @@ def resolve(objects, _args, _ctx, _scope) }, }.freeze +GEM_RESOLVERS = { + "Product" => { + "id" => ->(obj, args, ctx) { obj["id"] }, + "title" => ->(obj, args, ctx) { obj["title"] }, + "variants" => ->(obj, args, ctx) { obj["variants"] }, + }, + "ProductConnection" => { + "nodes" => ->(obj, args, ctx) { obj["nodes"] }, + }, + "Variant" => { + "id" => ->(obj, args, ctx) { obj["id"] }, + "title" => ->(obj, args, ctx) { obj["title"] }, + }, + "VariantConnection" => { + "nodes" => ->(obj, args, ctx) { obj["nodes"] }, + }, + "Query" => { + "products" => ->(obj, args, ctx) { obj["products"] }, + }, +}.freeze + +GEM_LAZY_RESOLVERS = { + "Product" => { + "id" => ->(obj, args, ctx) { -> { obj["id"] } }, + "title" => ->(obj, args, ctx) { -> { obj["title"] } }, + "variants" => ->(obj, args, ctx) { -> { obj["variants"] } }, + }, + "ProductConnection" => { + "nodes" => ->(obj, args, ctx) { -> { obj["nodes"] } }, + }, + "Variant" => { + "id" => ->(obj, args, ctx) { -> { obj["id"] } }, + "title" => ->(obj, args, ctx) { -> { obj["title"] } }, + }, + "VariantConnection" => { + "nodes" => ->(obj, args, ctx) { -> { obj["nodes"] } }, + }, + "Query" => { + "products" => ->(obj, args, ctx) { -> { obj["products"] } }, + }, +}.freeze + +GEM_DATALOADER_RESOLVERS = { + "Product" => { + "id" => ->(obj, args, ctx) { ctx.dataloader.with(SimpleHashSource, obj).load("id") }, + "title" => ->(obj, args, ctx) { ctx.dataloader.with(SimpleHashSource, obj).load("title") }, + "variants" => ->(obj, args, ctx) { ctx.dataloader.with(SimpleHashSource, obj).load("variants") }, + }, + "ProductConnection" => { + "nodes" => ->(obj, args, ctx) { ctx.dataloader.with(SimpleHashSource, obj).load("nodes") }, + }, + "Variant" => { + "id" => ->(obj, args, ctx) { ctx.dataloader.with(SimpleHashSource, obj).load("id") }, + "title" => ->(obj, args, ctx) { ctx.dataloader.with(SimpleHashSource, obj).load("title") }, + }, + "VariantConnection" => { + "nodes" => ->(obj, args, ctx) { ctx.dataloader.with(SimpleHashSource, obj).load("nodes") }, + }, + "Query" => { + "products" => ->(obj, args, ctx) { ctx.dataloader.with(SimpleHashSource, obj).load("products") }, + }, +}.freeze + +GEM_BATCH_LOADER_RESOLVERS = { + "Product" => { + "id" => ->(obj, args, ctx) { SimpleHashBatchLoader.for(obj).load("id") }, + "title" => ->(obj, args, ctx) { SimpleHashBatchLoader.for(obj).load("title") }, + "variants" => ->(obj, args, ctx) { SimpleHashBatchLoader.for(obj).load("variants") }, + }, + "ProductConnection" => { + "nodes" => ->(obj, args, ctx) { SimpleHashBatchLoader.for(obj).load("nodes") }, + }, + "Variant" => { + "id" => ->(obj, args, ctx) { SimpleHashBatchLoader.for(obj).load("id") }, + "title" => ->(obj, args, ctx) { SimpleHashBatchLoader.for(obj).load("title") }, + }, + "VariantConnection" => { + "nodes" => ->(obj, args, ctx) { SimpleHashBatchLoader.for(obj).load("nodes") }, + }, + "Query" => { + "products" => ->(obj, args, ctx) { SimpleHashBatchLoader.for(obj).load("products") }, + }, +}.freeze + BASIC_DOCUMENT = %|{ products(first: 3) { nodes { @@ -159,3 +325,4 @@ def resolve(objects, _args, _ctx, _scope) }], }, } + diff --git a/test/test_helper.rb b/test/test_helper.rb index f8ce0c4..db0154e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) - require 'warning' Gem.path.each do |path| # ignore warnings from auto-generated GraphQL lib code. @@ -15,7 +13,9 @@ require 'minitest/pride' require 'minitest/autorun' require 'minitest/stub_const' + require 'graphql/cardinal' +require 'graphql/batch' require_relative './fixtures' def breadth_exec(query, source, variables: {}, context: {})