benchmark/gem.rb (139 lines of code) (raw):

# frozen_string_literal: true require_relative '../result' module Benchmark # Abstract base class for benchmarking an SDK Gem. # Implementors must define the `gem_name`, `client_klass`, and the # `operation_benchmarks` methods. class Gem # Return all subclasses of this class. def self.descendants descendants = [] ObjectSpace.each_object(singleton_class) do |k| next if k.singleton_class? descendants.unshift k unless k == self end descendants end # The name of the gem (eg: aws-sdk-s3). def gem_name; end def service_name(gem_name) gem_name.split('-').last end # The name of the gem directory (eg: gems/aws-sdk-s3). def gem_dir; end # The module that contains the client (eg Aws::S3). def client_module_name; end # Return a hash with definitions for operation benchmarks to run. # The key should be the name of the test (reported as the metric name). # Values should be a hash with keys: # setup (proc), test (proc) and n (optional, integer) # # setup: Must be a proc that takes a client. Client will be pre initialized. # Setup may initialize stubs (eg `client.stub_responses(:operation, [...])`) # Setup MUST also return a hash with the request used in the test. # This avoids the cost of creating the argument in each run of the test. # # test: a proc that takes a client and request (generated from setup proc) def operation_benchmarks; end # Build the gem from its gemspec, then get the file size on disk. # Done within a temp directory to prevent accumulation of .gem artifacts. def benchmark_gem_size(legacy_report_data, report_data) Dir.mktmpdir('benchmark-gem-size') do |tmpdir| Dir.chdir(gem_dir) do `gem build #{gem_name}.gemspec -o #{tmpdir}/#{gem_name}.gem` legacy_report_data['gem_size_kb'] = File.size("#{tmpdir}/#{gem_name}.gem") / 1024.0 legacy_report_data['gem_version'] = File.read('VERSION').strip report_data << Result.new( "#{service_name(gem_name)}.gem.size", "The size of the #{gem_name} gem.", [File.size("#{tmpdir}/#{gem_name}.gem") / (1024.0 * 1024.0)] ).format end end end # Benchmark requiring a gem - runs in a forked process (when supported) # to ensure state of parent process is not modified by the require. # For accurate results, should be run before any SDK gems are required # in the parent process. # rubocop:disable Metrics/MethodLength def benchmark_require(legacy_report_data, report_data) return unless gem_name time = Benchmark.fork_run do |out| t1 = Benchmark.monotonic_milliseconds require gem_name out[:require_time] = (Benchmark.monotonic_milliseconds - t1) end memory = Benchmark.fork_run do |out| unless defined?(JRUBY_VERSION) r = ::MemoryProfiler.report { require gem_name } out[:require_mem_retained] = r.total_retained_memsize / (1024.0 * 1024.0) out[:require_mem_allocated] = r.total_allocated_memsize / (1024.0 * 1024.0) end end legacy_report_data.merge!(time) legacy_report_data.merge!(memory) report_data << Result.new( "#{service_name(gem_name)}.require.time", "The time it takes to require the #{gem_name} gem.", [time[:require_time]] ).format report_data << Result.new( "#{service_name(gem_name)}.require.retained.size", "The amount of memory retained when requiring the #{gem_name} gem.", [memory[:require_mem_retained]] ).format report_data << Result.new( "#{service_name(gem_name)}.require.allocated.size", "The amount of memory allocated when requiring the #{gem_name} gem.", [memory[:require_mem_allocated]] ).format end # rubocop:enable Metrics/MethodLength # Benchmark creating a client - runs in a forked process (when supported) # to ensure state of parent process is not modified by the require. # For accurate results, should be run before the client is initialized # in the parent process to ensure cache is clean. def benchmark_client(legacy_report_data, report_data) return unless client_module_name memory = Benchmark.fork_run do |out| require gem_name client_klass = Kernel.const_get(client_module_name).const_get(:Client) unless defined?(JRUBY_VERSION) r = ::MemoryProfiler.report { client_klass.new(stub_responses: true) } out[:client_mem_retained] = r.total_retained_memsize / (1024.0 * 1024.0) out[:client_mem_allocated] = r.total_allocated_memsize / (1024.0 * 1024.0) end end legacy_report_data.merge!(memory) report_data << Result.new( "#{service_name(gem_name)}.client.retained.size", "The amount of memory retained when creating the #{service_name(gem_name)} client.", [memory[:client_mem_retained]] ).format report_data << Result.new( "#{service_name(gem_name)}.client.allocated.size", "The amount of memory allocated when creating the #{service_name(gem_name)} client.", [memory[:client_mem_allocated]] ).format end # This runs in the main process and requires service gems. # It MUST be done after ALL testing of gem loads/client creates. # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity def benchmark_operations(legacy_report_data, report_data) require_relative 'test_data' return unless gem_name && client_module_name && operation_benchmarks require gem_name client_klass = Kernel.const_get(client_module_name).const_get(:Client) legacy_report_data[:client_init_ms] = Benchmark.measure_time(300) do client_klass.new(stub_responses: true) end report_data << Result.new( "#{service_name(gem_name)}.client.init.time", "The time it takes to initialize the #{service_name(gem_name)} client.", legacy_report_data[:client_init_ms] ).format values = legacy_report_data[:client_init_ms] ms = format('%.2f', (values.sum(0.0) / values.size)) puts "\t\t#{gem_name} client init avg: #{ms} ms" # rubocop:disable Metrics/BlockLength operation_benchmarks.each do |test_name, test_def| client = client_klass.new(stub_responses: true) req = test_def[:setup].call(client) op_name = test_name.to_s.split('_').join op_name_pascal = test_name.to_s.split('_').map(&:capitalize).join # warmup (run a few iterations without measurement) 2.times { test_def[:test].call(client, req) } mem_allocated = 0 unless defined?(JRUBY_VERSION) r = ::MemoryProfiler.report { test_def[:test].call(client, req) } mem_allocated = legacy_report_data["#{test_name}_allocated_kb"] = r.total_allocated_memsize / 1024.0 report_data << Result.new( "#{service_name(gem_name)}.#{op_name}.allocated.size", 'The amount of memory allocated to perform the ' \ "#{op_name_pascal} operation.", [mem_allocated / 1024.0] ).format end n = test_def[:n] || 300 values = Benchmark.measure_time(n) do test_def[:test].call(client, req) end legacy_report_data["#{test_name}_ms"] = values report_data << Result.new( "#{service_name(gem_name)}.#{op_name}.time", "The time it takes to perform the #{op_name_pascal} operation.", values ).format ms = format('%.2f', (values.sum(0.0) / values.size)) puts "\t\t#{test_name} avg: #{ms} ms\t" \ "mem_allocated: #{format('%.2f', mem_allocated)} kb" end # rubocop:enable Metrics/BlockLength end # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity end end