THRIFT-5944: Corrected and expanded Ruby protocol benchmarks
diff --git a/lib/rb/lib/thrift/thrift_native.rb b/lib/rb/lib/thrift/thrift_native.rb
index 372f3a5..d6dc4c2 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 0000000..b1eff1c
--- /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 897743f..aa39e25 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')
+require File.expand_path('../fixtures/structs', __dir__)
 
-transport1 = Thrift::MemoryBuffer.new
-ruby_binary_protocol = Thrift::BinaryProtocol.new(transport1)
+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
 
-transport2 = Thrift::MemoryBuffer.new
-c_fast_binary_protocol = Thrift::BinaryProtocolAccelerated.new(transport2)
+  module_function
 
-transport3 = Thrift::MemoryBuffer.new
-header_binary_protocol = Thrift::HeaderProtocol.new(transport3)
+  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
+    }
 
-transport4 = Thrift::MemoryBuffer.new
-header_compact_protocol = Thrift::HeaderProtocol.new(transport4, nil, Thrift::HeaderSubprotocolID::COMPACT)
+    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)
 
-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)
-
-Benchmark.bmbm do |x|
-  x.report("ruby write large (1MB) structure once") do
-    n4.write(ruby_binary_protocol)
+    {
+      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
+
+  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
+
+  def write(builder, value, count: 1)
+    _transport, protocol = builder.call
+
+    count.times do
+      value.write(protocol)
+      flush(protocol)
     end
   end
 
-  x.report("header (binary) read 10_000 small structures") do
-    10_000.times do
-      Fixtures::Structs::OneOfEach.new.read(header_binary_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
+
+  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 (compact) write 10_000 small structures") do
-    10_000.times do
-      ooe.write(header_compact_protocol)
-      header_compact_protocol.trans.flush
+  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
 
-  x.report("header (compact) read 10_000 small structures") do
-    10_000.times do
-      Fixtures::Structs::OneOfEach.new.read(header_compact_protocol)
-    end
-  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
-    end
-  end
-
-  x.report("header (zlib) read 10_000 small structures") do
-    10_000.times do
-      Fixtures::Structs::OneOfEach.new.read(header_zlib_protocol)
-    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 79f2997..d24e5d1 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 @@
     #   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 @@
         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 @@
     #   5: map<string, OneOfEach> 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 @@
         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 @@
     #   5: map<string, Nested1> 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 @@
         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 @@
     #   5: map<string, Nested2> 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 @@
         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 @@
     #   5: map<string, Nested3> 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 @@
         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