blob: aa39e251f7a355c01ff8d4cc3c034bbf572e7a0b [file] [log] [blame]
David Reissea2cba82009-03-30 21:35:00 +00001#
2# Licensed to the Apache Software Foundation (ASF) under one
3# or more contributor license agreements. See the NOTICE file
4# distributed with this work for additional information
5# regarding copyright ownership. The ASF licenses this file
6# to you under the Apache License, Version 2.0 (the
7# "License"); you may not use this file except in compliance
8# with the License. You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing,
13# software distributed under the License is distributed on an
14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15# KIND, either express or implied. See the License for the
16# specific language governing permissions and limitations
17# under the License.
18#
19
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -040020THRIFT_BENCHMARK_SKIP_NATIVE = ENV.fetch('THRIFT_BENCHMARK_SKIP_NATIVE', '').match?(/\A(?:1|true|yes|on)\z/i)
Kevin Clark4bd89162008-07-08 00:47:49 +000021
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -040022lib_path = File.expand_path('../../../lib/rb/lib', __dir__)
23ext_path = File.expand_path('../../../lib/rb/ext', __dir__)
24
25$LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
26
27if THRIFT_BENCHMARK_SKIP_NATIVE
28 $LOAD_PATH.delete(ext_path)
29else
30 $LOAD_PATH.unshift ext_path unless $LOAD_PATH.include?(ext_path)
31end
32
33if THRIFT_BENCHMARK_SKIP_NATIVE
34 File.open(File::NULL, 'w') do |null_stdout|
35 original_stdout = $stdout
36 $stdout = null_stdout
37 begin
38 require 'thrift'
39 ensure
40 $stdout = original_stdout
41 end
42 end
43else
44 require 'thrift'
45end
Kevin Clark4bd89162008-07-08 00:47:49 +000046
47require 'benchmark'
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -040048require 'json'
49require 'optparse'
Kevin Clark4bd89162008-07-08 00:47:49 +000050
51# require 'ruby-debug'
52# require 'ruby-prof'
53
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -040054require File.expand_path('../fixtures/structs', __dir__)
Kevin Clark4bd89162008-07-08 00:47:49 +000055
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -040056module ProtocolBenchmark
57 DEFAULT_LARGE_RUNS = 1
58 DEFAULT_SMALL_RUNS = 10_000
59 ALL_SCENARIO_IDS = %w[
60 rb-bin-write-large rb-bin-read-large c-bin-write-large c-bin-read-large
61 rb-cmp-write-large rb-cmp-read-large rb-json-write-large rb-json-read-large
62 rb-bin-write-small rb-bin-read-small c-bin-write-small c-bin-read-small
63 rb-cmp-write-small rb-cmp-read-small rb-json-write-small rb-json-read-small
64 hdr-bin-write-small hdr-bin-read-small hdr-cmp-write-small hdr-cmp-read-small
65 hdr-zlib-write-small hdr-zlib-read-small
66 ].freeze
67 NATIVE_SCENARIO_IDS = %w[
68 c-bin-write-large c-bin-read-large c-bin-write-small c-bin-read-small
69 ].freeze
Kevin Clark4bd89162008-07-08 00:47:49 +000070
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -040071 module_function
Kevin Clark4bd89162008-07-08 00:47:49 +000072
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -040073 def parse_run_options(argv = ARGV, env: ENV)
74 options = {
75 large_runs: env.fetch('THRIFT_BENCHMARK_LARGE_RUNS', DEFAULT_LARGE_RUNS),
76 small_runs: env.fetch('THRIFT_BENCHMARK_SMALL_RUNS', DEFAULT_SMALL_RUNS),
77 scenarios: env['THRIFT_BENCHMARK_SCENARIOS'],
78 json: false
79 }
Dmytro Shteflyuk67bfb292026-01-28 11:23:50 -050080
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -040081 OptionParser.new do |parser|
82 parser.on('--large-runs N', Integer) { |value| options[:large_runs] = value }
83 parser.on('--small-runs N', Integer) { |value| options[:small_runs] = value }
84 parser.on('--scenarios IDS', String) { |value| options[:scenarios] = value }
85 parser.on('--json') { options[:json] = true }
86 end.parse!(argv.dup)
Dmytro Shteflyuk67bfb292026-01-28 11:23:50 -050087
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -040088 {
89 large_runs: normalize_run_count(options[:large_runs], 'large runs'),
90 small_runs: normalize_run_count(options[:small_runs], 'small runs'),
91 scenarios: normalize_scenarios(options[:scenarios]),
92 json: options[:json]
93 }
Kevin Clark4bd89162008-07-08 00:47:49 +000094 end
Dmytro Shteflyukf5c80a42026-03-08 19:09:43 -040095
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -040096 def normalize_run_count(value, name)
97 count = value.is_a?(String) ? Integer(value, 10) : Integer(value)
98 raise ArgumentError, "#{name} must be >= 1" if count < 1
99
100 count
Kevin Clark4bd89162008-07-08 00:47:49 +0000101 end
Dmytro Shteflyukf5c80a42026-03-08 19:09:43 -0400102
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400103 def large_run_label(count)
104 count == 1 ? 'once' : "#{count} times"
Kevin Clark4bd89162008-07-08 00:47:49 +0000105 end
Dmytro Shteflyukf5c80a42026-03-08 19:09:43 -0400106
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400107 def normalize_scenarios(value)
108 return nil if value.nil?
109
110 scenario_ids = value.split(/[\s,]+/).filter_map do |scenario_id|
111 normalized = scenario_id.strip
112 normalized unless normalized.empty?
113 end
114
115 scenario_ids.empty? ? nil : scenario_ids.uniq
Kevin Clark4bd89162008-07-08 00:47:49 +0000116 end
Dmytro Shteflyukf5c80a42026-03-08 19:09:43 -0400117
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400118 def binary_protocol_builder(accelerated: false)
119 protocol_class =
120 if accelerated && native_available?
121 Thrift::BinaryProtocolAccelerated
122 else
123 Thrift::BinaryProtocol
124 end
125
126 lambda do |buffer = nil|
127 transport = Thrift::MemoryBufferTransport.new(buffer)
128 [transport, protocol_class.new(transport)]
Kevin Clark4bd89162008-07-08 00:47:49 +0000129 end
130 end
Dmytro Shteflyukf5c80a42026-03-08 19:09:43 -0400131
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400132 def compact_protocol_builder
133 lambda do |buffer = nil|
134 transport = Thrift::MemoryBufferTransport.new(buffer)
135 [transport, Thrift::CompactProtocol.new(transport)]
Kevin Clark4bd89162008-07-08 00:47:49 +0000136 end
137 end
Dmytro Shteflyukf5c80a42026-03-08 19:09:43 -0400138
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400139 def json_protocol_builder
140 lambda do |buffer = nil|
141 transport = Thrift::MemoryBufferTransport.new(buffer)
142 [transport, Thrift::JsonProtocol.new(transport)]
Kevin Clark4bd89162008-07-08 00:47:49 +0000143 end
144 end
Dmytro Shteflyukf5c80a42026-03-08 19:09:43 -0400145
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400146 def header_protocol_builder(default_protocol:, zlib: false)
147 lambda do |buffer = nil|
148 transport = Thrift::MemoryBufferTransport.new(buffer)
149 protocol = Thrift::HeaderProtocol.new(transport, nil, default_protocol)
150 protocol.add_transform(Thrift::HeaderTransformID::ZLIB) if zlib
151 [transport, protocol]
Kevin Clark4bd89162008-07-08 00:47:49 +0000152 end
153 end
Dmytro Shteflyuk67bfb292026-01-28 11:23:50 -0500154
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400155 def serialize(builder, value, count: 1)
156 transport, protocol = builder.call
157
158 count.times do
159 value.write(protocol)
160 flush(protocol)
161 end
162
163 transport.read(transport.available)
164 end
165
166 def deserialize(builder, struct_class, payload, count: 1)
167 _transport, protocol = builder.call(payload.dup)
168 value = nil
169
170 count.times do
171 value = struct_class.new
172 value.read(protocol)
173 end
174
175 value
176 end
177
178 def write(builder, value, count: 1)
179 _transport, protocol = builder.call
180
181 count.times do
182 value.write(protocol)
183 flush(protocol)
Dmytro Shteflyuk67bfb292026-01-28 11:23:50 -0500184 end
185 end
186
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400187 def flush(protocol)
188 protocol.trans.flush if protocol.trans.is_a?(Thrift::HeaderTransport)
189 end
190
191 def build_sample_structs
192 ooe = Fixtures::Structs::OneOfEach.new
193 ooe.im_true = true
194 ooe.im_false = false
195 ooe.a_bite = -42
196 ooe.integer16 = 27_000
197 ooe.integer32 = 1 << 24
198 ooe.integer64 = 6000 * 1000 * 1000
199 ooe.double_precision = Math::PI
200 ooe.some_characters = 'Debug THIS!'
201 ooe.zomg_unicode = "\u00D7\n\a\t"
202
203 n1 = Fixtures::Structs::Nested1.new
204 n1.a_list = [ooe, ooe, ooe, ooe]
205 n1.i32_map = {1234 => ooe, 46_345 => ooe, -34_264 => ooe}
206 n1.i64_map = {43_534_986_783_945 => ooe, -32_434_639_875_122 => ooe}
207 n1.dbl_map = {324.65469834 => ooe, -9_458_672_340.49868 => ooe}
208 n1.str_map = {'sdoperuix' => ooe, 'pwoerxclmn' => ooe}
209
210 n2 = Fixtures::Structs::Nested2.new
211 n2.a_list = [n1, n1, n1, n1, n1]
212 n2.i32_map = {398_345 => n1, -2345 => n1, 12_312 => n1}
213 n2.i64_map = {2_349_843_765_934 => n1, -123_234_985_495 => n1, 0 => n1}
214 n2.dbl_map = {23_345_345.38927834 => n1, -1_232_349.5489345 => n1, -234_984_574.23498726 => n1}
215 n2.str_map = {'' => n1, 'sdflkertpioux' => n1, 'sdfwepwdcjpoi' => n1}
216
217 n3 = Fixtures::Structs::Nested3.new
218 n3.a_list = [n2, n2, n2, n2, n2]
219 n3.i32_map = {398_345 => n2, -2345 => n2, 12_312 => n2}
220 n3.i64_map = {2_349_843_765_934 => n2, -123_234_985_495 => n2, 0 => n2}
221 n3.dbl_map = {23_345_345.38927834 => n2, -1_232_349.5489345 => n2, -234_984_574.23498726 => n2}
222 n3.str_map = {'' => n2, 'sdflkertpioux' => n2, 'sdfwepwdcjpoi' => n2}
223
224 n4 = Fixtures::Structs::Nested4.new
225 n4.a_list = [n3]
226 n4.i32_map = {-2345 => n3}
227 n4.i64_map = {2_349_843_765_934 => n3}
228 n4.dbl_map = {-1_232_349.5489345 => n3}
229 n4.str_map = {'' => n3}
230
231 [ooe, n4]
232 end
233
234 def scenario(id, label, &job)
235 {id: id, label: label, job: job}
236 end
237
238 def native_available?
239 Thrift.const_defined?(:BinaryProtocolAccelerated, false)
240 end
241
242 def with_scenario_selected(requested_ids, *ids)
243 selected = requested_ids.nil? || ids.any? { |id| requested_ids.include?(id) }
244 return false unless selected
245 return true unless block_given?
246
247 yield
248 end
249
250 def select_scenarios(scenarios, requested_ids, native_available:)
251 return scenarios if requested_ids.nil?
252
253 unknown_ids = requested_ids - ALL_SCENARIO_IDS
254 raise ArgumentError, "unknown scenarios: #{unknown_ids.join(', ')}" if unknown_ids.any?
255
256 unavailable_native_ids = requested_ids & NATIVE_SCENARIO_IDS unless native_available
257 if unavailable_native_ids&.any?
258 raise ArgumentError, "native-only scenarios unavailable without thrift_native: #{unavailable_native_ids.join(', ')}"
259 end
260
261 scenarios.select { |entry| requested_ids.include?(entry[:id]) }
262 end
263
264 def build_scenarios(large_runs:, small_runs:, scenario_ids: nil)
265 unknown_ids = scenario_ids ? scenario_ids - ALL_SCENARIO_IDS : []
266 raise ArgumentError, "unknown scenarios: #{unknown_ids.join(', ')}" if unknown_ids.any?
267
268 one_of_each, nested4 = build_sample_structs
269
270 ruby_binary = binary_protocol_builder
271 ruby_compact = compact_protocol_builder
272 ruby_json = json_protocol_builder
273 accelerated_binary = binary_protocol_builder(accelerated: true)
274 header_binary = header_protocol_builder(default_protocol: Thrift::HeaderSubprotocolID::BINARY)
275 header_compact = header_protocol_builder(default_protocol: Thrift::HeaderSubprotocolID::COMPACT)
276 header_zlib = header_protocol_builder(default_protocol: Thrift::HeaderSubprotocolID::BINARY, zlib: true)
277
278 native_available = native_available?
279 unavailable_native_ids = native_available ? [] : (scenario_ids || []) & NATIVE_SCENARIO_IDS
280 if unavailable_native_ids.any?
281 raise ArgumentError, "native-only scenarios unavailable without thrift_native: #{unavailable_native_ids.join(', ')}"
282 end
283
284 native_scenarios = []
285
286 ruby_large_payload = with_scenario_selected(scenario_ids, 'rb-bin-read-large') { serialize(ruby_binary, nested4, count: large_runs) }
287 ruby_small_payload = with_scenario_selected(scenario_ids, 'rb-bin-read-small') { serialize(ruby_binary, one_of_each, count: small_runs) }
288 compact_large_payload = with_scenario_selected(scenario_ids, 'rb-cmp-read-large') { serialize(ruby_compact, nested4, count: large_runs) }
289 compact_small_payload = with_scenario_selected(scenario_ids, 'rb-cmp-read-small') { serialize(ruby_compact, one_of_each, count: small_runs) }
290 json_large_payload = with_scenario_selected(scenario_ids, 'rb-json-read-large') { serialize(ruby_json, nested4, count: large_runs) }
291 json_small_payload = with_scenario_selected(scenario_ids, 'rb-json-read-small') { serialize(ruby_json, one_of_each, count: small_runs) }
292 header_binary_payload = with_scenario_selected(scenario_ids, 'hdr-bin-read-small') { serialize(header_binary, one_of_each, count: small_runs) }
293 header_compact_payload = with_scenario_selected(scenario_ids, 'hdr-cmp-read-small') { serialize(header_compact, one_of_each, count: small_runs) }
294 header_zlib_payload = with_scenario_selected(scenario_ids, 'hdr-zlib-read-small') { serialize(header_zlib, one_of_each, count: small_runs) }
295
296 if native_available
297 accelerated_large_payload = with_scenario_selected(scenario_ids, 'c-bin-read-large') { serialize(accelerated_binary, nested4, count: large_runs) }
298 accelerated_small_payload = with_scenario_selected(scenario_ids, 'c-bin-read-small') { serialize(accelerated_binary, one_of_each, count: small_runs) }
299
300 native_scenarios = [
301 scenario('c-bin-write-large', "c binary write large (1MB) structure #{large_run_label(large_runs)}") { write(accelerated_binary, nested4, count: large_runs) },
302 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) },
303 scenario('c-bin-write-small', "c binary write #{small_runs} small structures") { write(accelerated_binary, one_of_each, count: small_runs) },
304 scenario('c-bin-read-small', "c binary read #{small_runs} small structures") { deserialize(accelerated_binary, Fixtures::Structs::OneOfEach, accelerated_small_payload, count: small_runs) }
305 ]
306 elsif !THRIFT_BENCHMARK_SKIP_NATIVE && with_scenario_selected(scenario_ids, *NATIVE_SCENARIO_IDS)
307 warn 'Skipping accelerated binary protocol benchmarks: thrift_native extension is unavailable.'
308 end
309
310 scenario_list = [
311 scenario('rb-bin-write-large', "ruby binary write large (1MB) structure #{large_run_label(large_runs)}") { write(ruby_binary, nested4, count: large_runs) },
312 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) },
313 *native_scenarios.first(2),
314 scenario('rb-cmp-write-large', "ruby compact write large (1MB) structure #{large_run_label(large_runs)}") { write(ruby_compact, nested4, count: large_runs) },
315 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) },
316 scenario('rb-json-write-large', "ruby json write large (1MB) structure #{large_run_label(large_runs)}") { write(ruby_json, nested4, count: large_runs) },
317 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) },
318 scenario('rb-bin-write-small', "ruby binary write #{small_runs} small structures") { write(ruby_binary, one_of_each, count: small_runs) },
319 scenario('rb-bin-read-small', "ruby binary read #{small_runs} small structures") { deserialize(ruby_binary, Fixtures::Structs::OneOfEach, ruby_small_payload, count: small_runs) },
320 *native_scenarios.drop(2),
321 scenario('rb-cmp-write-small', "ruby compact write #{small_runs} small structures") { write(ruby_compact, one_of_each, count: small_runs) },
322 scenario('rb-cmp-read-small', "ruby compact read #{small_runs} small structures") { deserialize(ruby_compact, Fixtures::Structs::OneOfEach, compact_small_payload, count: small_runs) },
323 scenario('rb-json-write-small', "ruby json write #{small_runs} small structures") { write(ruby_json, one_of_each, count: small_runs) },
324 scenario('rb-json-read-small', "ruby json read #{small_runs} small structures") { deserialize(ruby_json, Fixtures::Structs::OneOfEach, json_small_payload, count: small_runs) },
325 scenario('hdr-bin-write-small', "header binary write #{small_runs} small structures") { write(header_binary, one_of_each, count: small_runs) },
326 scenario('hdr-bin-read-small', "header binary read #{small_runs} small structures") { deserialize(header_binary, Fixtures::Structs::OneOfEach, header_binary_payload, count: small_runs) },
327 scenario('hdr-cmp-write-small', "header compact write #{small_runs} small structures") { write(header_compact, one_of_each, count: small_runs) },
328 scenario('hdr-cmp-read-small', "header compact read #{small_runs} small structures") { deserialize(header_compact, Fixtures::Structs::OneOfEach, header_compact_payload, count: small_runs) },
329 scenario('hdr-zlib-write-small', "header zlib write #{small_runs} small structures") { write(header_zlib, one_of_each, count: small_runs) },
330 scenario('hdr-zlib-read-small', "header zlib read #{small_runs} small structures") { deserialize(header_zlib, Fixtures::Structs::OneOfEach, header_zlib_payload, count: small_runs) }
331 ]
332
333 select_scenarios(scenario_list, scenario_ids, native_available: native_available)
334 end
335
336 def measure_job(job, label: '')
337 result = Benchmark.measure(label, &job)
338 {
339 user: result.utime,
340 system: result.stime,
341 total: result.total,
342 real: result.real
343 }
344 end
345
346 def warm_up_scenarios(scenarios)
347 scenarios.each { |entry| measure_job(entry[:job]) }
348 end
349
350 def benchmark_scenarios(scenarios)
351 scenarios.map do |entry|
352 GC.start
353 {
354 id: entry[:id],
355 label: entry[:label],
356 benchmark: measure_job(entry[:job], label: entry[:label])
357 }
Dmytro Shteflyuk67bfb292026-01-28 11:23:50 -0500358 end
359 end
360
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400361 def run(large_runs: DEFAULT_LARGE_RUNS, small_runs: DEFAULT_SMALL_RUNS, scenarios: nil, json: false)
362 scenario_list = build_scenarios(large_runs: large_runs, small_runs: small_runs, scenario_ids: scenarios)
363
364 if json
365 warm_up_scenarios(scenario_list)
366
367 puts JSON.generate(
368 config: {
369 large_runs: large_runs,
370 small_runs: small_runs,
371 scenarios: scenario_list.map { |entry| entry[:id] },
372 skip_native: THRIFT_BENCHMARK_SKIP_NATIVE,
373 native_available: native_available?
374 },
375 results: benchmark_scenarios(scenario_list)
376 )
377 return
378 end
379
380 Benchmark.bmbm do |x|
381 scenario_list.each do |entry|
382 x.report(entry[:label], &entry[:job])
383 end
Dmytro Shteflyuk67bfb292026-01-28 11:23:50 -0500384 end
385 end
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400386end
Dmytro Shteflyuk67bfb292026-01-28 11:23:50 -0500387
Dmytro Shteflyuk82754d22026-03-27 12:39:51 -0400388if $PROGRAM_NAME == __FILE__
389 begin
390 ProtocolBenchmark.run(**ProtocolBenchmark.parse_run_options)
391 rescue OptionParser::ParseError, ArgumentError => e
392 warn e.message
393 exit 1
Dmytro Shteflyuk67bfb292026-01-28 11:23:50 -0500394 end
Kevin Clark4bd89162008-07-08 00:47:49 +0000395end