diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index c640102..67612ae 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -14,27 +14,3 @@ jobs: - name: Run tests run: bundle exec rspec - - - name: 'Upload Coverage Report' - uses: actions/upload-artifact@v4 - with: - name: coverage-report - path: ./coverage - - coverage: - needs: [ test ] - name: coverage - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Download Coverage Report - uses: actions/download-artifact@v4 - with: - name: coverage-report - path: ./coverage - - uses: paambaati/codeclimate-action@v9.0.0 - env: - # Set CC_TEST_REPORTER_ID as secret of your repo - CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} - with: - debug: true diff --git a/benchmark/Gemfile b/benchmark/Gemfile index 2606744..80a54ca 100644 --- a/benchmark/Gemfile +++ b/benchmark/Gemfile @@ -1,2 +1,3 @@ source 'http://rubygems.org' gem 'class_kit' +gem 'benchmark' diff --git a/benchmark/Gemfile.lock b/benchmark/Gemfile.lock index cd12e0d..1f0aa26 100644 --- a/benchmark/Gemfile.lock +++ b/benchmark/Gemfile.lock @@ -1,17 +1,29 @@ GEM remote: http://rubygems.org/ specs: - class_kit (0.6.0) + benchmark (0.5.0) + bigdecimal (4.0.1) + class_kit (0.9.1) + bigdecimal hash_kit json - hash_kit (0.6.0) - json (2.1.0) + hash_kit (0.7.0) + json (2.18.1) PLATFORMS ruby + x86_64-darwin-25 DEPENDENCIES + benchmark class_kit +CHECKSUMS + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c + bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7 + class_kit (0.9.1) sha256=0b31f65130a2b99807883cbf211e8d22d5f10f0f5a7beb3ff8336a51a62db0b2 + hash_kit (0.7.0) sha256=9ff39a55fb4df2ebf524751862f2178d1778cbaab3812ef7bddfb26f55d0f90c + json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986 + BUNDLED WITH - 1.16.1 + 4.0.4 diff --git a/benchmark/benchmark.rb b/benchmark/benchmark.rb index a097576..7833d7e 100644 --- a/benchmark/benchmark.rb +++ b/benchmark/benchmark.rb @@ -1,10 +1,40 @@ require_relative '../lib/class_kit' require 'date' require 'benchmark' +require 'json' -class Item +# ------------------------------------------------------------------- +# Test entities +# ------------------------------------------------------------------- + +class BenchContact extend ClassKit + attr_accessor_type :landline, type: String + attr_accessor_type :mobile, type: String + attr_accessor_type :email, type: String +end + +class BenchAddress + extend ClassKit + attr_accessor_type :line1, type: String + attr_accessor_type :line2, type: String + attr_accessor_type :postcode, type: String + attr_accessor_type :country, type: String +end +class BenchEmployee + extend ClassKit + attr_accessor_type :name, type: String + attr_accessor_type :age, type: Integer + attr_accessor_type :salary, type: Float + attr_accessor_type :dob, type: Date + attr_accessor_type :active, type: :bool + attr_accessor_type :address, type: BenchAddress + attr_accessor_type :contacts, type: Array, collection_type: BenchContact +end + +class BenchFlatItem + extend ClassKit attr_accessor_type :text, type: String attr_accessor_type :integer, type: Integer attr_accessor_type :float, type: Float @@ -13,9 +43,19 @@ class Item attr_accessor_type :bool, type: :bool end -items = [] -10_000.times do - items << Item.new.tap do |e| +class BenchDeeplyNested + extend ClassKit + attr_accessor_type :name, type: String + attr_accessor_type :child, type: BenchEmployee + attr_accessor_type :employees, type: Array, collection_type: BenchEmployee +end + +# ------------------------------------------------------------------- +# Data builders +# ------------------------------------------------------------------- + +def build_flat_item + BenchFlatItem.new.tap do |e| e.text = 'foo bar' e.integer = 50 e.float = 25.2 @@ -25,12 +65,186 @@ class Item end end +def build_address + BenchAddress.new.tap do |a| + a.line1 = '25 The Street' + a.line2 = 'Home Town' + a.postcode = 'NE3 5RT' + a.country = 'United Kingdom' + end +end + +def build_contact + BenchContact.new.tap do |c| + c.landline = '01234567890' + c.mobile = '07891234567' + c.email = 'test@example.com' + end +end + +def build_employee + BenchEmployee.new.tap do |e| + e.name = 'Joe Bloggs' + e.age = 42 + e.salary = 55_000.50 + e.dob = Date.parse('1980-06-03') + e.active = true + e.address = build_address + e.contacts = [build_contact, build_contact] + end +end + +def build_deeply_nested + BenchDeeplyNested.new.tap do |d| + d.name = 'Root' + d.child = build_employee + d.employees = 5.times.map { build_employee } + end +end + +# ------------------------------------------------------------------- +# Benchmark runner +# ------------------------------------------------------------------- + +ITERATIONS = 5 +SIZES = { small: 100, medium: 1_000, large: 10_000 } + helper = ClassKit::Helper.new -json = '' +def separator + puts '-' * 70 +end + +def run_benchmark(label, iterations: ITERATIONS) + times = iterations.times.map do + t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + yield + Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0 + end + median = times.sort[times.size / 2] + best = times.min + printf " %-50s median: %8.4fs best: %8.4fs\n", label, median, best + median +end + +puts '=' * 70 +puts 'ClassKit Benchmark Suite' +puts "Ruby #{RUBY_VERSION} | #{RUBY_PLATFORM}" +puts '=' * 70 + +results = {} + +# ------------------------------------------------------------------- +# 1. Flat serialization (to_json / from_json) +# ------------------------------------------------------------------- +puts "\n### 1. Flat items (6 typed attributes, no nesting)" +separator -puts '***serialize items***' -puts Benchmark.measure { json = helper.to_json(items) } +SIZES.each do |size_name, count| + items = count.times.map { build_flat_item } + json = helper.to_json(items) -puts '***deserialize items***' -puts Benchmark.measure { helper.from_json(json: json, klass: Item) } + key_ser = "flat_#{size_name}_serialize" + key_de = "flat_#{size_name}_deserialize" + + results[key_ser] = run_benchmark("to_json #{count} flat items") { helper.to_json(items) } + results[key_de] = run_benchmark("from_json #{count} flat items") do + helper.from_json(json: json, klass: BenchFlatItem) + end +end + +# ------------------------------------------------------------------- +# 2. Nested serialization (Employee with Address + Contacts) +# ------------------------------------------------------------------- +puts "\n### 2. Nested items (Employee -> Address + 2x Contact)" +separator + +SIZES.each do |size_name, count| + items = count.times.map { build_employee } + json = helper.to_json(items) + + key_ser = "nested_#{size_name}_serialize" + key_de = "nested_#{size_name}_deserialize" + + results[key_ser] = run_benchmark("to_json #{count} nested items") { helper.to_json(items) } + results[key_de] = run_benchmark("from_json #{count} nested items") do + helper.from_json(json: json, klass: BenchEmployee) + end +end + +# ------------------------------------------------------------------- +# 3. Deeply nested (3 levels deep with arrays) +# ------------------------------------------------------------------- +puts "\n### 3. Deeply nested items (3 levels, arrays of nested)" +separator + +[10, 100, 500].each do |count| + items = count.times.map { build_deeply_nested } + json = helper.to_json(items) + + key_ser = "deep_#{count}_serialize" + key_de = "deep_#{count}_deserialize" + + results[key_ser] = run_benchmark("to_json #{count} deep items") { helper.to_json(items) } + results[key_de] = run_benchmark("from_json #{count} deep items") do + helper.from_json(json: json, klass: BenchDeeplyNested) + end +end + +# ------------------------------------------------------------------- +# 4. Hash round-trip (no JSON overhead) +# ------------------------------------------------------------------- +puts "\n### 4. Hash round-trip (to_hash / from_hash, 1000 nested)" +separator + +items = 1_000.times.map { build_employee } +hashes = items.map { |i| helper.to_hash(i) } + +results['hash_serialize'] = run_benchmark('to_hash 1000 nested items') { items.each { |i| helper.to_hash(i) } } +results['hash_deserialize'] = run_benchmark('from_hash 1000 nested items') do + hashes.each do |h| + helper.from_hash(hash: h, klass: BenchEmployee) + end +end + +# ------------------------------------------------------------------- +# 5. Micro: single object round-trip +# ------------------------------------------------------------------- +puts "\n### 5. Micro: single object (10_000 iterations)" +separator + +employee = build_employee +employee_json = helper.to_json(employee) +employee_hash = helper.to_hash(employee) + +results['micro_to_json'] = run_benchmark('to_json single employee x10k') do + 10_000.times do + helper.to_json(employee) + end +end +results['micro_from_json'] = run_benchmark('from_json single employee x10k') do + 10_000.times do + helper.from_json(json: employee_json, klass: BenchEmployee) + end +end +results['micro_to_hash'] = run_benchmark('to_hash single employee x10k') do + 10_000.times do + helper.to_hash(employee) + end +end +results['micro_from_hash'] = run_benchmark('from_hash single employee x10k') do + 10_000.times do + helper.from_hash(hash: employee_hash, klass: BenchEmployee) + end +end + +# ------------------------------------------------------------------- +# Summary +# ------------------------------------------------------------------- +puts "\n" +puts '=' * 70 +puts 'Summary (median times in seconds)' +puts '=' * 70 +results.sort_by { |_, v| -v }.each do |label, time| + printf " %-45s %8.4fs\n", label, time +end