From 8e4a9b1eadc50c6e23c6db717f7d34388ee1fe48 Mon Sep 17 00:00:00 2001 From: Dmytro Shteflyuk Date: Fri, 27 Mar 2026 12:39:51 -0400 Subject: [PATCH] Corrected and expanded Ruby protocol benchmarks --- lib/rb/lib/thrift/thrift_native.rb | 2 +- test/rb/benchmarks/README.md | 107 +++++ test/rb/benchmarks/protocol_benchmark.rb | 483 ++++++++++++++++------- test/rb/fixtures/structs.rb | 153 ++++--- 4 files changed, 546 insertions(+), 199 deletions(-) create mode 100644 test/rb/benchmarks/README.md diff --git a/lib/rb/lib/thrift/thrift_native.rb b/lib/rb/lib/thrift/thrift_native.rb index 372f3a5e053..d6dc4c24ac2 100644 --- a/lib/rb/lib/thrift/thrift_native.rb +++ b/lib/rb/lib/thrift/thrift_native.rb @@ -20,5 +20,5 @@ begin require "thrift_native" rescue LoadError - puts "Unable to load thrift_native extension. Defaulting to pure Ruby libraries." + warn "Unable to load thrift_native extension. Defaulting to pure Ruby libraries." end diff --git a/test/rb/benchmarks/README.md b/test/rb/benchmarks/README.md new file mode 100644 index 00000000000..b1eff1c8eb2 --- /dev/null +++ b/test/rb/benchmarks/README.md @@ -0,0 +1,107 @@ +# Ruby Protocol Benchmarks + +This directory holds a small harness for quick Ruby protocol benchmarks. +Use it to spot read and write regressions in tree. + +## Quick Start +Run the script with plain `ruby` from the repo root. + +```sh +ruby test/rb/benchmarks/protocol_benchmark.rb +``` + +This runs the full benchmark set: + +- Ruby binary +- Ruby compact +- Ruby JSON +- Header binary +- Header compact +- Header zlib +- C binary, if `thrift_native.so` loads + +## Options +Use flags or env vars to tune a run. + +```sh +ruby test/rb/benchmarks/protocol_benchmark.rb --large-runs 1 --small-runs 10000 +``` + +- `--json` +- `THRIFT_BENCHMARK_LARGE_RUNS` +- `THRIFT_BENCHMARK_SMALL_RUNS` +- `THRIFT_BENCHMARK_SKIP_NATIVE=1` +- `THRIFT_BENCHMARK_SCENARIOS` + +```sh +ruby test/rb/benchmarks/protocol_benchmark.rb --json > benchmark.json +``` + +> [!NOTE] +> `--json` keeps the warm-up pass, but only prints measured results. + +> [!TIP] +> Set `THRIFT_BENCHMARK_SKIP_NATIVE=1` to force a pure-Ruby run. + +## Scenario IDs +Use `--scenarios` or `THRIFT_BENCHMARK_SCENARIOS` to run only part of the matrix. + +```sh +ruby test/rb/benchmarks/protocol_benchmark.rb --scenarios rb-bin-write-large,rb-json-read-large,hdr-zlib-read-small +``` + +Each ID has four parts: + +- family: `rb`, `c`, or `hdr` +- protocol: `bin`, `cmp`, `json`, or `zlib` +- operation: `write` or `read` +- size: `large` or `small` + +### Full Table + +| ID | Family | Protocol | Operation | Size | +| --- | --- | --- | --- | --- | +| `rb-bin-write-large` | Ruby | binary | write | large | +| `rb-bin-read-large` | Ruby | binary | read | large | +| `c-bin-write-large` | C native | binary | write | large | +| `c-bin-read-large` | C native | binary | read | large | +| `rb-cmp-write-large` | Ruby | compact | write | large | +| `rb-cmp-read-large` | Ruby | compact | read | large | +| `rb-json-write-large` | Ruby | JSON | write | large | +| `rb-json-read-large` | Ruby | JSON | read | large | +| `rb-bin-write-small` | Ruby | binary | write | small | +| `rb-bin-read-small` | Ruby | binary | read | small | +| `c-bin-write-small` | C native | binary | write | small | +| `c-bin-read-small` | C native | binary | read | small | +| `rb-cmp-write-small` | Ruby | compact | write | small | +| `rb-cmp-read-small` | Ruby | compact | read | small | +| `rb-json-write-small` | Ruby | JSON | write | small | +| `rb-json-read-small` | Ruby | JSON | read | small | +| `hdr-bin-write-small` | Header | binary | write | small | +| `hdr-bin-read-small` | Header | binary | read | small | +| `hdr-cmp-write-small` | Header | compact | write | small | +| `hdr-cmp-read-small` | Header | compact | read | small | +| `hdr-zlib-write-small` | Header | zlib | write | small | +| `hdr-zlib-read-small` | Header | zlib | read | small | + +> [!NOTE] +> Native-only IDs fail if `thrift_native.so` is not available. + +## Reference + +### What It Measures + +- Large jobs serialize and deserialize one nested `Nested4` payload by default. +- Small jobs serialize and deserialize many `OneOfEach` payloads. +- Read jobs use payloads built before timing so they measure read cost, not payload construction. +- Header jobs flush after each struct so reads benchmark framed messages, not a buffered write that was never emitted. + +### Files + +- `protocol_benchmark.rb`: benchmark harness and scenario definitions +- `../fixtures/structs.rb`: sample structs used by the benchmark + +### Notes +This harness is for quick in-tree checks. +Use `--json` if you want structured output for scripts, result diffs, or branch comparisons. +Run it more than once if you want a wider sample. diff --git a/test/rb/benchmarks/protocol_benchmark.rb b/test/rb/benchmarks/protocol_benchmark.rb index 897743fe055..aa39e251f7a 100644 --- a/test/rb/benchmarks/protocol_benchmark.rb +++ b/test/rb/benchmarks/protocol_benchmark.rb @@ -17,202 +17,379 @@ # under the License. # -$LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. .. .. lib rb lib]) -$LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. .. .. lib rb ext]) +THRIFT_BENCHMARK_SKIP_NATIVE = ENV.fetch('THRIFT_BENCHMARK_SKIP_NATIVE', '').match?(/\A(?:1|true|yes|on)\z/i) -require 'thrift' +lib_path = File.expand_path('../../../lib/rb/lib', __dir__) +ext_path = File.expand_path('../../../lib/rb/ext', __dir__) + +$LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path) + +if THRIFT_BENCHMARK_SKIP_NATIVE + $LOAD_PATH.delete(ext_path) +else + $LOAD_PATH.unshift ext_path unless $LOAD_PATH.include?(ext_path) +end + +if THRIFT_BENCHMARK_SKIP_NATIVE + File.open(File::NULL, 'w') do |null_stdout| + original_stdout = $stdout + $stdout = null_stdout + begin + require 'thrift' + ensure + $stdout = original_stdout + end + end +else + require 'thrift' +end require 'benchmark' -require 'rubygems' -require 'set' -require 'pp' +require 'json' +require 'optparse' # require 'ruby-debug' # require 'ruby-prof' -require File.join(File.dirname(__FILE__), '../fixtures/structs') - -transport1 = Thrift::MemoryBuffer.new -ruby_binary_protocol = Thrift::BinaryProtocol.new(transport1) - -transport2 = Thrift::MemoryBuffer.new -c_fast_binary_protocol = Thrift::BinaryProtocolAccelerated.new(transport2) - -transport3 = Thrift::MemoryBuffer.new -header_binary_protocol = Thrift::HeaderProtocol.new(transport3) - -transport4 = Thrift::MemoryBuffer.new -header_compact_protocol = Thrift::HeaderProtocol.new(transport4, nil, Thrift::HeaderSubprotocolID::COMPACT) - -transport5 = Thrift::MemoryBuffer.new -header_zlib_protocol = Thrift::HeaderProtocol.new(transport5) -header_zlib_protocol.add_transform(Thrift::HeaderTransformID::ZLIB) - -ooe = Fixtures::Structs::OneOfEach.new -ooe.im_true = true -ooe.im_false = false -ooe.a_bite = -42 -ooe.integer16 = 27000 -ooe.integer32 = 1<<24 -ooe.integer64 = 6000 * 1000 * 1000 -ooe.double_precision = Math::PI -ooe.some_characters = "Debug THIS!" -ooe.zomg_unicode = "\xd7\n\a\t" - -n1 = Fixtures::Structs::Nested1.new -n1.a_list = [] -n1.a_list << ooe << ooe << ooe << ooe -n1.i32_map = {} -n1.i32_map[1234] = ooe -n1.i32_map[46345] = ooe -n1.i32_map[-34264] = ooe -n1.i64_map = {} -n1.i64_map[43534986783945] = ooe -n1.i64_map[-32434639875122] = ooe -n1.dbl_map = {} -n1.dbl_map[324.65469834] = ooe -n1.dbl_map[-9458672340.4986798345112] = ooe -n1.str_map = {} -n1.str_map['sdoperuix'] = ooe -n1.str_map['pwoerxclmn'] = ooe - -n2 = Fixtures::Structs::Nested2.new -n2.a_list = [] -n2.a_list << n1 << n1 << n1 << n1 << n1 -n2.i32_map = {} -n2.i32_map[398345] = n1 -n2.i32_map[-2345] = n1 -n2.i32_map[12312] = n1 -n2.i64_map = {} -n2.i64_map[2349843765934] = n1 -n2.i64_map[-123234985495] = n1 -n2.i64_map[0] = n1 -n2.dbl_map = {} -n2.dbl_map[23345345.38927834] = n1 -n2.dbl_map[-1232349.5489345] = n1 -n2.dbl_map[-234984574.23498725] = n1 -n2.str_map = {} -n2.str_map[''] = n1 -n2.str_map['sdflkertpioux'] = n1 -n2.str_map['sdfwepwdcjpoi'] = n1 - -n3 = Fixtures::Structs::Nested3.new -n3.a_list = [] -n3.a_list << n2 << n2 << n2 << n2 << n2 -n3.i32_map = {} -n3.i32_map[398345] = n2 -n3.i32_map[-2345] = n2 -n3.i32_map[12312] = n2 -n3.i64_map = {} -n3.i64_map[2349843765934] = n2 -n3.i64_map[-123234985495] = n2 -n3.i64_map[0] = n2 -n3.dbl_map = {} -n3.dbl_map[23345345.38927834] = n2 -n3.dbl_map[-1232349.5489345] = n2 -n3.dbl_map[-234984574.23498725] = n2 -n3.str_map = {} -n3.str_map[''] = n2 -n3.str_map['sdflkertpioux'] = n2 -n3.str_map['sdfwepwdcjpoi'] = n2 - -n4 = Fixtures::Structs::Nested4.new -n4.a_list = [] -n4.a_list << n3 -n4.i32_map = {} -n4.i32_map[-2345] = n3 -n4.i64_map = {} -n4.i64_map[2349843765934] = n3 -n4.dbl_map = {} -n4.dbl_map[-1232349.5489345] = n3 -n4.str_map = {} -n4.str_map[''] = n3 - -# prof = RubyProf.profile do -# n4.write(c_fast_binary_protocol) -# Fixtures::Structs::Nested4.new.read(c_fast_binary_protocol) -# end -# -# printer = RubyProf::GraphHtmlPrinter.new(prof) -# printer.print(STDOUT, :min_percent=>0) +require File.expand_path('../fixtures/structs', __dir__) + +module ProtocolBenchmark + DEFAULT_LARGE_RUNS = 1 + DEFAULT_SMALL_RUNS = 10_000 + ALL_SCENARIO_IDS = %w[ + rb-bin-write-large rb-bin-read-large c-bin-write-large c-bin-read-large + rb-cmp-write-large rb-cmp-read-large rb-json-write-large rb-json-read-large + rb-bin-write-small rb-bin-read-small c-bin-write-small c-bin-read-small + rb-cmp-write-small rb-cmp-read-small rb-json-write-small rb-json-read-small + hdr-bin-write-small hdr-bin-read-small hdr-cmp-write-small hdr-cmp-read-small + hdr-zlib-write-small hdr-zlib-read-small + ].freeze + NATIVE_SCENARIO_IDS = %w[ + c-bin-write-large c-bin-read-large c-bin-write-small c-bin-read-small + ].freeze -Benchmark.bmbm do |x| - x.report("ruby write large (1MB) structure once") do - n4.write(ruby_binary_protocol) + module_function + + def parse_run_options(argv = ARGV, env: ENV) + options = { + large_runs: env.fetch('THRIFT_BENCHMARK_LARGE_RUNS', DEFAULT_LARGE_RUNS), + small_runs: env.fetch('THRIFT_BENCHMARK_SMALL_RUNS', DEFAULT_SMALL_RUNS), + scenarios: env['THRIFT_BENCHMARK_SCENARIOS'], + json: false + } + + OptionParser.new do |parser| + parser.on('--large-runs N', Integer) { |value| options[:large_runs] = value } + parser.on('--small-runs N', Integer) { |value| options[:small_runs] = value } + parser.on('--scenarios IDS', String) { |value| options[:scenarios] = value } + parser.on('--json') { options[:json] = true } + end.parse!(argv.dup) + + { + large_runs: normalize_run_count(options[:large_runs], 'large runs'), + small_runs: normalize_run_count(options[:small_runs], 'small runs'), + scenarios: normalize_scenarios(options[:scenarios]), + json: options[:json] + } end - x.report("ruby read large (1MB) structure once") do - Fixtures::Structs::Nested4.new.read(ruby_binary_protocol) + def normalize_run_count(value, name) + count = value.is_a?(String) ? Integer(value, 10) : Integer(value) + raise ArgumentError, "#{name} must be >= 1" if count < 1 + + count end - x.report("c write large (1MB) structure once") do - n4.write(c_fast_binary_protocol) + def large_run_label(count) + count == 1 ? 'once' : "#{count} times" end - x.report("c read large (1MB) structure once") do - Fixtures::Structs::Nested4.new.read(c_fast_binary_protocol) + def normalize_scenarios(value) + return nil if value.nil? + + scenario_ids = value.split(/[\s,]+/).filter_map do |scenario_id| + normalized = scenario_id.strip + normalized unless normalized.empty? + end + + scenario_ids.empty? ? nil : scenario_ids.uniq end - x.report("ruby write 10_000 small structures") do - 10_000.times do - ooe.write(ruby_binary_protocol) + def binary_protocol_builder(accelerated: false) + protocol_class = + if accelerated && native_available? + Thrift::BinaryProtocolAccelerated + else + Thrift::BinaryProtocol + end + + lambda do |buffer = nil| + transport = Thrift::MemoryBufferTransport.new(buffer) + [transport, protocol_class.new(transport)] end end - x.report("ruby read 10_000 small structures") do - 10_000.times do - Fixtures::Structs::OneOfEach.new.read(ruby_binary_protocol) + def compact_protocol_builder + lambda do |buffer = nil| + transport = Thrift::MemoryBufferTransport.new(buffer) + [transport, Thrift::CompactProtocol.new(transport)] end end - x.report("c write 10_000 small structures") do - 10_000.times do - ooe.write(c_fast_binary_protocol) + def json_protocol_builder + lambda do |buffer = nil| + transport = Thrift::MemoryBufferTransport.new(buffer) + [transport, Thrift::JsonProtocol.new(transport)] end end - x.report("c read 10_000 small structures") do - 10_000.times do - Fixtures::Structs::OneOfEach.new.read(c_fast_binary_protocol) + def header_protocol_builder(default_protocol:, zlib: false) + lambda do |buffer = nil| + transport = Thrift::MemoryBufferTransport.new(buffer) + protocol = Thrift::HeaderProtocol.new(transport, nil, default_protocol) + protocol.add_transform(Thrift::HeaderTransformID::ZLIB) if zlib + [transport, protocol] end end - x.report("header (binary) write 10_000 small structures") do - 10_000.times do - ooe.write(header_binary_protocol) - header_binary_protocol.trans.flush + def serialize(builder, value, count: 1) + transport, protocol = builder.call + + count.times do + value.write(protocol) + flush(protocol) end + + transport.read(transport.available) end - x.report("header (binary) read 10_000 small structures") do - 10_000.times do - Fixtures::Structs::OneOfEach.new.read(header_binary_protocol) + def deserialize(builder, struct_class, payload, count: 1) + _transport, protocol = builder.call(payload.dup) + value = nil + + count.times do + value = struct_class.new + value.read(protocol) end + + value end - x.report("header (compact) write 10_000 small structures") do - 10_000.times do - ooe.write(header_compact_protocol) - header_compact_protocol.trans.flush + def write(builder, value, count: 1) + _transport, protocol = builder.call + + count.times do + value.write(protocol) + flush(protocol) end end - x.report("header (compact) read 10_000 small structures") do - 10_000.times do - Fixtures::Structs::OneOfEach.new.read(header_compact_protocol) + def flush(protocol) + protocol.trans.flush if protocol.trans.is_a?(Thrift::HeaderTransport) + end + + def build_sample_structs + ooe = Fixtures::Structs::OneOfEach.new + ooe.im_true = true + ooe.im_false = false + ooe.a_bite = -42 + ooe.integer16 = 27_000 + ooe.integer32 = 1 << 24 + ooe.integer64 = 6000 * 1000 * 1000 + ooe.double_precision = Math::PI + ooe.some_characters = 'Debug THIS!' + ooe.zomg_unicode = "\u00D7\n\a\t" + + n1 = Fixtures::Structs::Nested1.new + n1.a_list = [ooe, ooe, ooe, ooe] + n1.i32_map = {1234 => ooe, 46_345 => ooe, -34_264 => ooe} + n1.i64_map = {43_534_986_783_945 => ooe, -32_434_639_875_122 => ooe} + n1.dbl_map = {324.65469834 => ooe, -9_458_672_340.49868 => ooe} + n1.str_map = {'sdoperuix' => ooe, 'pwoerxclmn' => ooe} + + n2 = Fixtures::Structs::Nested2.new + n2.a_list = [n1, n1, n1, n1, n1] + n2.i32_map = {398_345 => n1, -2345 => n1, 12_312 => n1} + n2.i64_map = {2_349_843_765_934 => n1, -123_234_985_495 => n1, 0 => n1} + n2.dbl_map = {23_345_345.38927834 => n1, -1_232_349.5489345 => n1, -234_984_574.23498726 => n1} + n2.str_map = {'' => n1, 'sdflkertpioux' => n1, 'sdfwepwdcjpoi' => n1} + + n3 = Fixtures::Structs::Nested3.new + n3.a_list = [n2, n2, n2, n2, n2] + n3.i32_map = {398_345 => n2, -2345 => n2, 12_312 => n2} + n3.i64_map = {2_349_843_765_934 => n2, -123_234_985_495 => n2, 0 => n2} + n3.dbl_map = {23_345_345.38927834 => n2, -1_232_349.5489345 => n2, -234_984_574.23498726 => n2} + n3.str_map = {'' => n2, 'sdflkertpioux' => n2, 'sdfwepwdcjpoi' => n2} + + n4 = Fixtures::Structs::Nested4.new + n4.a_list = [n3] + n4.i32_map = {-2345 => n3} + n4.i64_map = {2_349_843_765_934 => n3} + n4.dbl_map = {-1_232_349.5489345 => n3} + n4.str_map = {'' => n3} + + [ooe, n4] + end + + def scenario(id, label, &job) + {id: id, label: label, job: job} + end + + def native_available? + Thrift.const_defined?(:BinaryProtocolAccelerated, false) + end + + def with_scenario_selected(requested_ids, *ids) + selected = requested_ids.nil? || ids.any? { |id| requested_ids.include?(id) } + return false unless selected + return true unless block_given? + + yield + end + + def select_scenarios(scenarios, requested_ids, native_available:) + return scenarios if requested_ids.nil? + + unknown_ids = requested_ids - ALL_SCENARIO_IDS + raise ArgumentError, "unknown scenarios: #{unknown_ids.join(', ')}" if unknown_ids.any? + + unavailable_native_ids = requested_ids & NATIVE_SCENARIO_IDS unless native_available + if unavailable_native_ids&.any? + raise ArgumentError, "native-only scenarios unavailable without thrift_native: #{unavailable_native_ids.join(', ')}" + end + + scenarios.select { |entry| requested_ids.include?(entry[:id]) } + end + + def build_scenarios(large_runs:, small_runs:, scenario_ids: nil) + unknown_ids = scenario_ids ? scenario_ids - ALL_SCENARIO_IDS : [] + raise ArgumentError, "unknown scenarios: #{unknown_ids.join(', ')}" if unknown_ids.any? + + one_of_each, nested4 = build_sample_structs + + ruby_binary = binary_protocol_builder + ruby_compact = compact_protocol_builder + ruby_json = json_protocol_builder + accelerated_binary = binary_protocol_builder(accelerated: true) + header_binary = header_protocol_builder(default_protocol: Thrift::HeaderSubprotocolID::BINARY) + header_compact = header_protocol_builder(default_protocol: Thrift::HeaderSubprotocolID::COMPACT) + header_zlib = header_protocol_builder(default_protocol: Thrift::HeaderSubprotocolID::BINARY, zlib: true) + + native_available = native_available? + unavailable_native_ids = native_available ? [] : (scenario_ids || []) & NATIVE_SCENARIO_IDS + if unavailable_native_ids.any? + raise ArgumentError, "native-only scenarios unavailable without thrift_native: #{unavailable_native_ids.join(', ')}" + end + + native_scenarios = [] + + ruby_large_payload = with_scenario_selected(scenario_ids, 'rb-bin-read-large') { serialize(ruby_binary, nested4, count: large_runs) } + ruby_small_payload = with_scenario_selected(scenario_ids, 'rb-bin-read-small') { serialize(ruby_binary, one_of_each, count: small_runs) } + compact_large_payload = with_scenario_selected(scenario_ids, 'rb-cmp-read-large') { serialize(ruby_compact, nested4, count: large_runs) } + compact_small_payload = with_scenario_selected(scenario_ids, 'rb-cmp-read-small') { serialize(ruby_compact, one_of_each, count: small_runs) } + json_large_payload = with_scenario_selected(scenario_ids, 'rb-json-read-large') { serialize(ruby_json, nested4, count: large_runs) } + json_small_payload = with_scenario_selected(scenario_ids, 'rb-json-read-small') { serialize(ruby_json, one_of_each, count: small_runs) } + header_binary_payload = with_scenario_selected(scenario_ids, 'hdr-bin-read-small') { serialize(header_binary, one_of_each, count: small_runs) } + header_compact_payload = with_scenario_selected(scenario_ids, 'hdr-cmp-read-small') { serialize(header_compact, one_of_each, count: small_runs) } + header_zlib_payload = with_scenario_selected(scenario_ids, 'hdr-zlib-read-small') { serialize(header_zlib, one_of_each, count: small_runs) } + + if native_available + accelerated_large_payload = with_scenario_selected(scenario_ids, 'c-bin-read-large') { serialize(accelerated_binary, nested4, count: large_runs) } + accelerated_small_payload = with_scenario_selected(scenario_ids, 'c-bin-read-small') { serialize(accelerated_binary, one_of_each, count: small_runs) } + + native_scenarios = [ + scenario('c-bin-write-large', "c binary write large (1MB) structure #{large_run_label(large_runs)}") { write(accelerated_binary, nested4, count: large_runs) }, + scenario('c-bin-read-large', "c binary read large (1MB) structure #{large_run_label(large_runs)}") { deserialize(accelerated_binary, Fixtures::Structs::Nested4, accelerated_large_payload, count: large_runs) }, + scenario('c-bin-write-small', "c binary write #{small_runs} small structures") { write(accelerated_binary, one_of_each, count: small_runs) }, + scenario('c-bin-read-small', "c binary read #{small_runs} small structures") { deserialize(accelerated_binary, Fixtures::Structs::OneOfEach, accelerated_small_payload, count: small_runs) } + ] + elsif !THRIFT_BENCHMARK_SKIP_NATIVE && with_scenario_selected(scenario_ids, *NATIVE_SCENARIO_IDS) + warn 'Skipping accelerated binary protocol benchmarks: thrift_native extension is unavailable.' end + + scenario_list = [ + scenario('rb-bin-write-large', "ruby binary write large (1MB) structure #{large_run_label(large_runs)}") { write(ruby_binary, nested4, count: large_runs) }, + scenario('rb-bin-read-large', "ruby binary read large (1MB) structure #{large_run_label(large_runs)}") { deserialize(ruby_binary, Fixtures::Structs::Nested4, ruby_large_payload, count: large_runs) }, + *native_scenarios.first(2), + scenario('rb-cmp-write-large', "ruby compact write large (1MB) structure #{large_run_label(large_runs)}") { write(ruby_compact, nested4, count: large_runs) }, + scenario('rb-cmp-read-large', "ruby compact read large (1MB) structure #{large_run_label(large_runs)}") { deserialize(ruby_compact, Fixtures::Structs::Nested4, compact_large_payload, count: large_runs) }, + scenario('rb-json-write-large', "ruby json write large (1MB) structure #{large_run_label(large_runs)}") { write(ruby_json, nested4, count: large_runs) }, + scenario('rb-json-read-large', "ruby json read large (1MB) structure #{large_run_label(large_runs)}") { deserialize(ruby_json, Fixtures::Structs::Nested4, json_large_payload, count: large_runs) }, + scenario('rb-bin-write-small', "ruby binary write #{small_runs} small structures") { write(ruby_binary, one_of_each, count: small_runs) }, + scenario('rb-bin-read-small', "ruby binary read #{small_runs} small structures") { deserialize(ruby_binary, Fixtures::Structs::OneOfEach, ruby_small_payload, count: small_runs) }, + *native_scenarios.drop(2), + scenario('rb-cmp-write-small', "ruby compact write #{small_runs} small structures") { write(ruby_compact, one_of_each, count: small_runs) }, + scenario('rb-cmp-read-small', "ruby compact read #{small_runs} small structures") { deserialize(ruby_compact, Fixtures::Structs::OneOfEach, compact_small_payload, count: small_runs) }, + scenario('rb-json-write-small', "ruby json write #{small_runs} small structures") { write(ruby_json, one_of_each, count: small_runs) }, + scenario('rb-json-read-small', "ruby json read #{small_runs} small structures") { deserialize(ruby_json, Fixtures::Structs::OneOfEach, json_small_payload, count: small_runs) }, + scenario('hdr-bin-write-small', "header binary write #{small_runs} small structures") { write(header_binary, one_of_each, count: small_runs) }, + scenario('hdr-bin-read-small', "header binary read #{small_runs} small structures") { deserialize(header_binary, Fixtures::Structs::OneOfEach, header_binary_payload, count: small_runs) }, + scenario('hdr-cmp-write-small', "header compact write #{small_runs} small structures") { write(header_compact, one_of_each, count: small_runs) }, + scenario('hdr-cmp-read-small', "header compact read #{small_runs} small structures") { deserialize(header_compact, Fixtures::Structs::OneOfEach, header_compact_payload, count: small_runs) }, + scenario('hdr-zlib-write-small', "header zlib write #{small_runs} small structures") { write(header_zlib, one_of_each, count: small_runs) }, + scenario('hdr-zlib-read-small', "header zlib read #{small_runs} small structures") { deserialize(header_zlib, Fixtures::Structs::OneOfEach, header_zlib_payload, count: small_runs) } + ] + + select_scenarios(scenario_list, scenario_ids, native_available: native_available) + end + + def measure_job(job, label: '') + result = Benchmark.measure(label, &job) + { + user: result.utime, + system: result.stime, + total: result.total, + real: result.real + } + end + + def warm_up_scenarios(scenarios) + scenarios.each { |entry| measure_job(entry[:job]) } end - x.report("header (zlib) write 10_000 small structures") do - 10_000.times do - ooe.write(header_zlib_protocol) - header_zlib_protocol.trans.flush + def benchmark_scenarios(scenarios) + scenarios.map do |entry| + GC.start + { + id: entry[:id], + label: entry[:label], + benchmark: measure_job(entry[:job], label: entry[:label]) + } end end - x.report("header (zlib) read 10_000 small structures") do - 10_000.times do - Fixtures::Structs::OneOfEach.new.read(header_zlib_protocol) + def run(large_runs: DEFAULT_LARGE_RUNS, small_runs: DEFAULT_SMALL_RUNS, scenarios: nil, json: false) + scenario_list = build_scenarios(large_runs: large_runs, small_runs: small_runs, scenario_ids: scenarios) + + if json + warm_up_scenarios(scenario_list) + + puts JSON.generate( + config: { + large_runs: large_runs, + small_runs: small_runs, + scenarios: scenario_list.map { |entry| entry[:id] }, + skip_native: THRIFT_BENCHMARK_SKIP_NATIVE, + native_available: native_available? + }, + results: benchmark_scenarios(scenario_list) + ) + return + end + + Benchmark.bmbm do |x| + scenario_list.each do |entry| + x.report(entry[:label], &entry[:job]) + end end end end + +if $PROGRAM_NAME == __FILE__ + begin + ProtocolBenchmark.run(**ProtocolBenchmark.parse_run_options) + rescue OptionParser::ParseError, ArgumentError => e + warn e.message + exit 1 + end +end diff --git a/test/rb/fixtures/structs.rb b/test/rb/fixtures/structs.rb index 79f2997200c..d24e5d1453d 100644 --- a/test/rb/fixtures/structs.rb +++ b/test/rb/fixtures/structs.rb @@ -22,146 +22,198 @@ module Fixtures module Structs class OneBool - include Thrift::Struct - attr_accessor :bool + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::BOOL, :name => 'bool'} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class OneByte - include Thrift::Struct - attr_accessor :byte + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::BYTE, :name => 'byte'} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class OneI16 - include Thrift::Struct - attr_accessor :i16 + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::I16, :name => 'i16'} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class OneI32 - include Thrift::Struct - attr_accessor :i32 + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::I32, :name => 'i32'} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class OneI64 - include Thrift::Struct - attr_accessor :i64 + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::I64, :name => 'i64'} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class OneDouble - include Thrift::Struct - attr_accessor :double + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::DOUBLE, :name => 'double'} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class OneString - include Thrift::Struct - attr_accessor :string + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::STRING, :name => 'string'} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class OneMap - include Thrift::Struct - attr_accessor :map + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::MAP, :name => 'map', :key => {:type => Thrift::Types::STRING}, :value => {:type => Thrift::Types::STRING}} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class NestedMap - include Thrift::Struct - attr_accessor :map + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 0 => {:type => Thrift::Types::MAP, :name => 'map', :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::MAP, :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::I32}}} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class OneList - include Thrift::Struct - attr_accessor :list + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::LIST, :name => 'list', :element => {:type => Thrift::Types::STRING}} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class NestedList - include Thrift::Struct - attr_accessor :list + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 0 => {:type => Thrift::Types::LIST, :name => 'list', :element => {:type => Thrift::Types::LIST, :element => { :type => Thrift::Types::I32 } } } } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class OneSet - include Thrift::Struct - attr_accessor :set + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::SET, :name => 'set', :element => {:type => Thrift::Types::STRING}} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end class NestedSet - include Thrift::Struct - attr_accessor :set + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::SET, :name => 'set', :element => {:type => Thrift::Types::SET, :element => { :type => Thrift::Types::STRING } }} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end # struct OneOfEach { @@ -178,8 +230,8 @@ def validate # 11: binary base64, # } class OneOfEach - include Thrift::Struct - attr_accessor :im_true, :im_false, :a_bite, :integer16, :integer32, :integer64, :double_precision, :some_characters, :zomg_unicode, :what_who, :base64 + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::BOOL, :name => 'im_true'}, 2 => {:type => Thrift::Types::BOOL, :name => 'im_false'}, @@ -191,20 +243,15 @@ class OneOfEach 8 => {:type => Thrift::Types::STRING, :name => 'some_characters'}, 9 => {:type => Thrift::Types::STRING, :name => 'zomg_unicode'}, 10 => {:type => Thrift::Types::BOOL, :name => 'what_who'}, - 11 => {:type => Thrift::Types::STRING, :name => 'base64'} + 11 => {:type => Thrift::Types::STRING, :name => 'base64', :binary => true} } - # Added for assert_equal - def ==(other) - [:im_true, :im_false, :a_bite, :integer16, :integer32, :integer64, :double_precision, :some_characters, :zomg_unicode, :what_who, :base64].each do |f| - var = "@#{f}" - return false if instance_variable_get(var) != other.instance_variable_get(var) - end - true - end + def struct_fields; FIELDS; end def validate end + + Thrift::Struct.generate_accessors self end # struct Nested1 { @@ -215,8 +262,8 @@ def validate # 5: map str_map # } class Nested1 - include Thrift::Struct - attr_accessor :a_list, :i32_map, :i64_map, :dbl_map, :str_map + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::LIST, :name => 'a_list', :element => {:type => Thrift::Types::STRUCT, :class => OneOfEach}}, 2 => {:type => Thrift::Types::MAP, :name => 'i32_map', :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::STRUCT, :class => OneOfEach}}, @@ -225,8 +272,12 @@ class Nested1 5 => {:type => Thrift::Types::MAP, :name => 'str_map', :key => {:type => Thrift::Types::STRING}, :value => {:type => Thrift::Types::STRUCT, :class => OneOfEach}} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end # struct Nested2 { @@ -237,8 +288,8 @@ def validate # 5: map str_map # } class Nested2 - include Thrift::Struct - attr_accessor :a_list, :i32_map, :i64_map, :dbl_map, :str_map + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::LIST, :name => 'a_list', :element => {:type => Thrift::Types::STRUCT, :class => Nested1}}, 2 => {:type => Thrift::Types::MAP, :name => 'i32_map', :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::STRUCT, :class => Nested1}}, @@ -247,8 +298,12 @@ class Nested2 5 => {:type => Thrift::Types::MAP, :name => 'str_map', :key => {:type => Thrift::Types::STRING}, :value => {:type => Thrift::Types::STRUCT, :class => Nested1}} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end # struct Nested3 { @@ -259,8 +314,8 @@ def validate # 5: map str_map # } class Nested3 - include Thrift::Struct - attr_accessor :a_list, :i32_map, :i64_map, :dbl_map, :str_map + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::LIST, :name => 'a_list', :element => {:type => Thrift::Types::STRUCT, :class => Nested2}}, 2 => {:type => Thrift::Types::MAP, :name => 'i32_map', :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::STRUCT, :class => Nested2}}, @@ -269,8 +324,12 @@ class Nested3 5 => {:type => Thrift::Types::MAP, :name => 'str_map', :key => {:type => Thrift::Types::STRING}, :value => {:type => Thrift::Types::STRUCT, :class => Nested2}} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end # struct Nested4 { @@ -281,8 +340,8 @@ def validate # 5: map str_map # } class Nested4 - include Thrift::Struct - attr_accessor :a_list, :i32_map, :i64_map, :dbl_map, :str_map + include Thrift::Struct, Thrift::Struct_Union + FIELDS = { 1 => {:type => Thrift::Types::LIST, :name => 'a_list', :element => {:type => Thrift::Types::STRUCT, :class => Nested3}}, 2 => {:type => Thrift::Types::MAP, :name => 'i32_map', :key => {:type => Thrift::Types::I32}, :value => {:type => Thrift::Types::STRUCT, :class => Nested3}}, @@ -291,8 +350,12 @@ class Nested4 5 => {:type => Thrift::Types::MAP, :name => 'str_map', :key => {:type => Thrift::Types::STRING}, :value => {:type => Thrift::Types::STRUCT, :class => Nested3}} } + def struct_fields; FIELDS; end + def validate end + + Thrift::Struct.generate_accessors self end end end