#!/usr/bin/env ruby # frozen_string_literal: true # Run with: # asdf exec ruby benchmark/dag_cbor_decode.rb # asdf exec ruby --yjit benchmark/dag_cbor_decode.rb # # A CAR path can be passed as the first argument. ITERATIONS, SAMPLES, and # WARMUP can be set in the environment for longer or shorter runs. require 'bundler/setup' require 'benchmark' require 'oxygene' def equivalent_decodings?(left, right) if left.is_a?(CBOR::Tagged) && right.is_a?(Oxygene::DAGCBOR::Tagged) left.tag == right.tag && equivalent_decodings?(left.value, right.value) elsif left.is_a?(Hash) && right.is_a?(Hash) left.length == right.length && left.all? { |key, value| right.key?(key) && equivalent_decodings?(value, right[key]) } elsif left.is_a?(Array) && right.is_a?(Array) left.length == right.length && left.each_index.all? { |index| equivalent_decodings?(left[index], right[index]) } elsif left.is_a?(String) && right.is_a?(String) left == right && left.encoding == right.encoding else left == right end end file = ARGV[0] || File.expand_path('../spec/fixtures/bsky_repo.car', __dir__) iterations = Integer(ENV.fetch('ITERATIONS', 25)) samples = Integer(ENV.fetch('SAMPLES', 7)) warmup = Integer(ENV.fetch('WARMUP', 20)) archive = Oxygene::CARArchive.new(File.binread(file)) blocks = archive.sections.map(&:data) total_bytes = blocks.sum(&:bytesize) blocks.each_with_index do |data, index| c_value = CBOR.decode(data) ruby_value = Oxygene::DAGCBOR.decode(data) unless equivalent_decodings?(c_value, ruby_value) raise "Decoder results differ for block #{index}" end end jobs = { 'cbor C extension' => -> { blocks.each { |data| CBOR.decode(data) } }, 'pure Ruby' => -> { blocks.each { |data| Oxygene::DAGCBOR.decode(data) } } } jobs.each_value do |job| warmup.times { job.call } end timings = jobs.to_h { |name, _job| [name, []] } samples.times do |sample| order = sample.even? ? jobs.to_a : jobs.to_a.reverse order.each do |name, job| GC.start timings[name] << Benchmark.realtime { iterations.times { job.call } } end end puts RUBY_DESCRIPTION puts "YJIT: #{RubyVM::YJIT.enabled?}" puts "Input: #{File.expand_path(file)}" puts "Corpus: #{blocks.length} blocks, #{total_bytes} encoded bytes" puts "Each result is the median of #{samples} samples x #{iterations} corpus passes" puts medians = timings.to_h { |name, values| [name, values.sort[values.length / 2]] } medians.each do |name, seconds| block_rate = blocks.length * iterations / seconds byte_rate = total_bytes * iterations / seconds / 1024 / 1024 puts format('%-18s %10.0f blocks/s %7.1f MiB/s (%0.4fs)', name, block_rate, byte_rate, seconds) end c_time = medians.fetch('cbor C extension') ruby_time = medians.fetch('pure Ruby') if ruby_time < c_time puts format('pure Ruby is %.2fx faster', c_time / ruby_time) else puts format('pure Ruby is %.2fx slower', ruby_time / c_time) end