Skip to content

Commit 1191542

Browse files
authored
Merge pull request #31 from Sage/performance-1
Performance improvements to ValueHelper
2 parents 947f4ae + 4baeed1 commit 1191542

7 files changed

Lines changed: 276 additions & 69 deletions

File tree

.github/workflows/rspec.yml

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,3 @@ jobs:
1414

1515
- name: Run tests
1616
run: bundle exec rspec
17-
18-
- name: 'Upload Coverage Report'
19-
uses: actions/upload-artifact@v4
20-
with:
21-
name: coverage-report
22-
path: ./coverage
23-
24-
coverage:
25-
needs: [ test ]
26-
name: coverage
27-
runs-on: ubuntu-latest
28-
steps:
29-
- uses: actions/checkout@v4
30-
- name: Download Coverage Report
31-
uses: actions/download-artifact@v4
32-
with:
33-
name: coverage-report
34-
path: ./coverage
35-
- uses: paambaati/codeclimate-action@v9.0.0
36-
env:
37-
# Set CC_TEST_REPORTER_ID as secret of your repo
38-
CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}}
39-
with:
40-
debug: true

benchmark/Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
source 'http://rubygems.org'
22
gem 'class_kit'
3+
gem 'benchmark'

benchmark/Gemfile.lock

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
GEM
22
remote: http://rubygems.org/
33
specs:
4-
class_kit (0.6.0)
4+
benchmark (0.5.0)
5+
bigdecimal (4.0.1)
6+
class_kit (0.9.1)
7+
bigdecimal
58
hash_kit
69
json
7-
hash_kit (0.6.0)
8-
json (2.1.0)
10+
hash_kit (0.7.0)
11+
json (2.18.1)
912

1013
PLATFORMS
1114
ruby
15+
x86_64-darwin-25
1216

1317
DEPENDENCIES
18+
benchmark
1419
class_kit
1520

21+
CHECKSUMS
22+
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
23+
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
24+
class_kit (0.9.1) sha256=0b31f65130a2b99807883cbf211e8d22d5f10f0f5a7beb3ff8336a51a62db0b2
25+
hash_kit (0.7.0) sha256=9ff39a55fb4df2ebf524751862f2178d1778cbaab3812ef7bddfb26f55d0f90c
26+
json (2.18.1) sha256=fe112755501b8d0466b5ada6cf50c8c3f41e897fa128ac5d263ec09eedc9f986
27+
1628
BUNDLED WITH
17-
1.16.1
29+
4.0.4

benchmark/benchmark.rb

Lines changed: 223 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,40 @@
11
require_relative '../lib/class_kit'
22
require 'date'
33
require 'benchmark'
4+
require 'json'
45

5-
class Item
6+
# -------------------------------------------------------------------
7+
# Test entities
8+
# -------------------------------------------------------------------
9+
10+
class BenchContact
611
extend ClassKit
12+
attr_accessor_type :landline, type: String
13+
attr_accessor_type :mobile, type: String
14+
attr_accessor_type :email, type: String
15+
end
16+
17+
class BenchAddress
18+
extend ClassKit
19+
attr_accessor_type :line1, type: String
20+
attr_accessor_type :line2, type: String
21+
attr_accessor_type :postcode, type: String
22+
attr_accessor_type :country, type: String
23+
end
724

25+
class BenchEmployee
26+
extend ClassKit
27+
attr_accessor_type :name, type: String
28+
attr_accessor_type :age, type: Integer
29+
attr_accessor_type :salary, type: Float
30+
attr_accessor_type :dob, type: Date
31+
attr_accessor_type :active, type: :bool
32+
attr_accessor_type :address, type: BenchAddress
33+
attr_accessor_type :contacts, type: Array, collection_type: BenchContact
34+
end
35+
36+
class BenchFlatItem
37+
extend ClassKit
838
attr_accessor_type :text, type: String
939
attr_accessor_type :integer, type: Integer
1040
attr_accessor_type :float, type: Float
@@ -13,9 +43,19 @@ class Item
1343
attr_accessor_type :bool, type: :bool
1444
end
1545

16-
items = []
17-
10_000.times do
18-
items << Item.new.tap do |e|
46+
class BenchDeeplyNested
47+
extend ClassKit
48+
attr_accessor_type :name, type: String
49+
attr_accessor_type :child, type: BenchEmployee
50+
attr_accessor_type :employees, type: Array, collection_type: BenchEmployee
51+
end
52+
53+
# -------------------------------------------------------------------
54+
# Data builders
55+
# -------------------------------------------------------------------
56+
57+
def build_flat_item
58+
BenchFlatItem.new.tap do |e|
1959
e.text = 'foo bar'
2060
e.integer = 50
2161
e.float = 25.2
@@ -25,12 +65,186 @@ class Item
2565
end
2666
end
2767

68+
def build_address
69+
BenchAddress.new.tap do |a|
70+
a.line1 = '25 The Street'
71+
a.line2 = 'Home Town'
72+
a.postcode = 'NE3 5RT'
73+
a.country = 'United Kingdom'
74+
end
75+
end
76+
77+
def build_contact
78+
BenchContact.new.tap do |c|
79+
c.landline = '01234567890'
80+
c.mobile = '07891234567'
81+
c.email = 'test@example.com'
82+
end
83+
end
84+
85+
def build_employee
86+
BenchEmployee.new.tap do |e|
87+
e.name = 'Joe Bloggs'
88+
e.age = 42
89+
e.salary = 55_000.50
90+
e.dob = Date.parse('1980-06-03')
91+
e.active = true
92+
e.address = build_address
93+
e.contacts = [build_contact, build_contact]
94+
end
95+
end
96+
97+
def build_deeply_nested
98+
BenchDeeplyNested.new.tap do |d|
99+
d.name = 'Root'
100+
d.child = build_employee
101+
d.employees = 5.times.map { build_employee }
102+
end
103+
end
104+
105+
# -------------------------------------------------------------------
106+
# Benchmark runner
107+
# -------------------------------------------------------------------
108+
109+
ITERATIONS = 5
110+
SIZES = { small: 100, medium: 1_000, large: 10_000 }
111+
28112
helper = ClassKit::Helper.new
29113

30-
json = ''
114+
def separator
115+
puts '-' * 70
116+
end
117+
118+
def run_benchmark(label, iterations: ITERATIONS)
119+
times = iterations.times.map do
120+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
121+
yield
122+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
123+
end
124+
median = times.sort[times.size / 2]
125+
best = times.min
126+
printf " %-50s median: %8.4fs best: %8.4fs\n", label, median, best
127+
median
128+
end
129+
130+
puts '=' * 70
131+
puts 'ClassKit Benchmark Suite'
132+
puts "Ruby #{RUBY_VERSION} | #{RUBY_PLATFORM}"
133+
puts '=' * 70
134+
135+
results = {}
136+
137+
# -------------------------------------------------------------------
138+
# 1. Flat serialization (to_json / from_json)
139+
# -------------------------------------------------------------------
140+
puts "\n### 1. Flat items (6 typed attributes, no nesting)"
141+
separator
31142

32-
puts '***serialize items***'
33-
puts Benchmark.measure { json = helper.to_json(items) }
143+
SIZES.each do |size_name, count|
144+
items = count.times.map { build_flat_item }
145+
json = helper.to_json(items)
34146

35-
puts '***deserialize items***'
36-
puts Benchmark.measure { helper.from_json(json: json, klass: Item) }
147+
key_ser = "flat_#{size_name}_serialize"
148+
key_de = "flat_#{size_name}_deserialize"
149+
150+
results[key_ser] = run_benchmark("to_json #{count} flat items") { helper.to_json(items) }
151+
results[key_de] = run_benchmark("from_json #{count} flat items") do
152+
helper.from_json(json: json, klass: BenchFlatItem)
153+
end
154+
end
155+
156+
# -------------------------------------------------------------------
157+
# 2. Nested serialization (Employee with Address + Contacts)
158+
# -------------------------------------------------------------------
159+
puts "\n### 2. Nested items (Employee -> Address + 2x Contact)"
160+
separator
161+
162+
SIZES.each do |size_name, count|
163+
items = count.times.map { build_employee }
164+
json = helper.to_json(items)
165+
166+
key_ser = "nested_#{size_name}_serialize"
167+
key_de = "nested_#{size_name}_deserialize"
168+
169+
results[key_ser] = run_benchmark("to_json #{count} nested items") { helper.to_json(items) }
170+
results[key_de] = run_benchmark("from_json #{count} nested items") do
171+
helper.from_json(json: json, klass: BenchEmployee)
172+
end
173+
end
174+
175+
# -------------------------------------------------------------------
176+
# 3. Deeply nested (3 levels deep with arrays)
177+
# -------------------------------------------------------------------
178+
puts "\n### 3. Deeply nested items (3 levels, arrays of nested)"
179+
separator
180+
181+
[10, 100, 500].each do |count|
182+
items = count.times.map { build_deeply_nested }
183+
json = helper.to_json(items)
184+
185+
key_ser = "deep_#{count}_serialize"
186+
key_de = "deep_#{count}_deserialize"
187+
188+
results[key_ser] = run_benchmark("to_json #{count} deep items") { helper.to_json(items) }
189+
results[key_de] = run_benchmark("from_json #{count} deep items") do
190+
helper.from_json(json: json, klass: BenchDeeplyNested)
191+
end
192+
end
193+
194+
# -------------------------------------------------------------------
195+
# 4. Hash round-trip (no JSON overhead)
196+
# -------------------------------------------------------------------
197+
puts "\n### 4. Hash round-trip (to_hash / from_hash, 1000 nested)"
198+
separator
199+
200+
items = 1_000.times.map { build_employee }
201+
hashes = items.map { |i| helper.to_hash(i) }
202+
203+
results['hash_serialize'] = run_benchmark('to_hash 1000 nested items') { items.each { |i| helper.to_hash(i) } }
204+
results['hash_deserialize'] = run_benchmark('from_hash 1000 nested items') do
205+
hashes.each do |h|
206+
helper.from_hash(hash: h, klass: BenchEmployee)
207+
end
208+
end
209+
210+
# -------------------------------------------------------------------
211+
# 5. Micro: single object round-trip
212+
# -------------------------------------------------------------------
213+
puts "\n### 5. Micro: single object (10_000 iterations)"
214+
separator
215+
216+
employee = build_employee
217+
employee_json = helper.to_json(employee)
218+
employee_hash = helper.to_hash(employee)
219+
220+
results['micro_to_json'] = run_benchmark('to_json single employee x10k') do
221+
10_000.times do
222+
helper.to_json(employee)
223+
end
224+
end
225+
results['micro_from_json'] = run_benchmark('from_json single employee x10k') do
226+
10_000.times do
227+
helper.from_json(json: employee_json, klass: BenchEmployee)
228+
end
229+
end
230+
results['micro_to_hash'] = run_benchmark('to_hash single employee x10k') do
231+
10_000.times do
232+
helper.to_hash(employee)
233+
end
234+
end
235+
results['micro_from_hash'] = run_benchmark('from_hash single employee x10k') do
236+
10_000.times do
237+
helper.from_hash(hash: employee_hash, klass: BenchEmployee)
238+
end
239+
end
240+
241+
# -------------------------------------------------------------------
242+
# Summary
243+
# -------------------------------------------------------------------
244+
puts "\n"
245+
puts '=' * 70
246+
puts 'Summary (median times in seconds)'
247+
puts '=' * 70
248+
results.sort_by { |_, v| -v }.each do |label, time|
249+
printf " %-45s %8.4fs\n", label, time
250+
end

lib/class_kit.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require_relative 'class_kit/constants'
12
require_relative 'class_kit/class_methods'
23
require_relative 'class_kit/exceptions'
34
require_relative 'class_kit/attribute_helper'
@@ -10,4 +11,3 @@
1011
require 'date'
1112
require 'bigdecimal'
1213
require 'time'
13-

lib/class_kit/constants.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
module ClassKit
4+
module Constants
5+
# Shared constants, to avoid re-creating them on each call to helper methods.
6+
BOOL_TRUE_RE = /\A(?:true|t|yes|y|1)\z/i
7+
BOOL_FALSE_RE = /\A(?:false|f|no|n|0)\z/i
8+
end
9+
end

0 commit comments

Comments
 (0)