Skip to content

Commit 5f78e5e

Browse files
committed
Cleanup
Can now test various scenarios from bin/bench by controlling environment variables passed to the tests. (The tests still handle the number of iterations for efficiency of shelling out.) This lets us do something like ```ruby scenarios = { 'caching on: caching serializers' => 'CACHE_ON=true CACHING_SERIALIZER=true', 'caching off: caching serializers' => 'CACHE_ON=false CACHING_SERIALIZER=true', 'caching on: non-caching serializers' => 'CACHE_ON=true CACHING_SERIALIZER=false', 'caching off: non-caching serializers' => 'CACHE_ON=false CACHING_SERIALIZER=false' } Benchmark do |x| scenarios.each do |name, env_vars| x.report(name) { benchmark_tests(env_vars) } end end ``` (Also configurable via env: TIMES, DEBUG)
1 parent ce825f0 commit 5f78e5e

File tree

4 files changed

+270
-152
lines changed

4 files changed

+270
-152
lines changed

bin/bench

Lines changed: 115 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,138 @@ require 'fileutils'
33
require 'benchmark'
44
require 'pathname'
55
require 'shellwords'
6+
require 'English'
7+
8+
############################
9+
# USAGE
10+
#
11+
# bin/bench <ref1> <ref2>
12+
# <ref1> defaults to the current branch
13+
# <ref2> defaults to the master branch
14+
# bin/branch current will run on the current branch
15+
###########################
16+
17+
class Benchmarking
18+
ROOT = Pathname File.expand_path(['..', '..'].join(File::Separator), __FILE__)
19+
TMP_DIR = File.join(ROOT, 'tmp/bench')
20+
21+
attr_reader :prepend, :append
22+
23+
def initialize(prepend: '', append: '')
24+
@prepend = prepend
25+
@append = append
26+
refresh_temp_dir if temp_dir_empty?
27+
end
628

7-
ROOT = Pathname File.expand_path(['..', '..'].join(File::Separator), __FILE__)
8-
TMP_DIR = File.join(ROOT, 'tmp/bench')
29+
def temp_dir_empty?
30+
Dir[File.join(TMP_DIR, '*')].none?
31+
end
932

10-
def temp_dir_empty?
11-
Dir[File.join(TMP_DIR, '*')].none?
12-
end
33+
def empty_temp_dir
34+
FileUtils.mkdir_p(TMP_DIR)
35+
Dir[File.join(TMP_DIR, '*')].each do |file|
36+
FileUtils.rm(file)
37+
end
38+
end
1339

14-
def empty_temp_dir
15-
FileUtils.mkdir_p(TMP_DIR)
16-
Dir[File.join(TMP_DIR, '*')].each do |file|
17-
FileUtils.rm(file)
40+
def fill_temp_dir
41+
Dir[File.join(ROOT, 'test', 'benchmark', '*.rb')].each do |file|
42+
FileUtils.cp(file, File.join(TMP_DIR, File.basename(file)))
43+
end
44+
at_exit { empty_temp_dir }
1845
end
19-
end
2046

21-
def fill_temp_dir
22-
Dir[File.join(ROOT, 'test', 'benchmark', '*.rb')].each do |file|
23-
FileUtils.cp(file, File.join(TMP_DIR, File.basename(file)))
47+
def refresh_temp_dir
48+
empty_temp_dir
49+
fill_temp_dir
2450
end
25-
at_exit { empty_temp_dir }
26-
end
2751

28-
def refresh_temp_dir
29-
empty_temp_dir
30-
fill_temp_dir
31-
end
52+
def benchmark_tests
53+
tmp_dir = Shellwords.shellescape(TMP_DIR)
54+
system("#{prepend} bundle exec ruby -Ilib:test #{tmp_dir}/*_benchmark.rb #{append}")
55+
end
3256

33-
def benchmark_tests
34-
refresh_temp_dir if temp_dir_empty?
35-
system("bundle exec ruby -Ilib:test #{Shellwords.shellescape(TMP_DIR)}/*_benchmark.rb")
36-
end
57+
def current_branch
58+
@current_branch ||= `cat .git/HEAD | cut -d/ -f3,4,5`.chomp
59+
end
3760

38-
def current_branch
39-
@current_branch ||= `cat .git/HEAD | cut -d/ -f3,4,5`.chomp
40-
end
61+
def checkout_ref(ref)
62+
puts `git checkout #{ref}`.chomp
63+
abort "Checkout failed: #{ref}, #{$CHILD_STATUS.exitstatus}" unless $CHILD_STATUS.success?
64+
end
4165

42-
def checkout_ref(ref)
43-
puts `git checkout #{ref}`.chomp
44-
abort "Checkout failed: #{ref}, #{$?.exitstatus}" unless $?.success?
45-
end
66+
def benchmark_refs(ref1: nil, ref2: nil)
67+
ref0 = current_branch
68+
ref1 ||= current_branch
69+
ref2 ||= 'master'
4670

47-
def benchmark
48-
refresh_temp_dir
49-
ref = current_branch
71+
actual = run_benchmark_at_ref(ref1)
72+
master = run_benchmark_at_ref(ref2)
5073

51-
actual = run_benchmark_at_ref ref
52-
master = run_benchmark_at_ref 'master'
74+
checkout_ref(ref0)
5375

54-
checkout_ref(ref)
76+
<<-REPORT
5577
56-
puts "\n\nResults ============================\n"
57-
puts "------------------------------------~> (Branch) MASTER"
58-
puts master
59-
puts "------------------------------------\n\n"
6078
61-
puts "------------------------------------~> (Actual Branch) #{ref}"
62-
puts actual
63-
puts "------------------------------------"
64-
end
79+
Results ============================
80+
------------------------------------~> (Branch) #{ref2.upcase}
81+
#{master} (seconds)
82+
------------------------------------
6583
66-
def run_benchmark
67-
response = Benchmark.realtime {
68-
benchmark_tests
69-
}
70-
benchmark_tests
71-
response
72-
end
7384
74-
def run_benchmark_at_ref(ref)
75-
checkout_ref(ref)
76-
run_benchmark
85+
86+
------------------------------------~> (Actual Branch) #{ref1.upcase}
87+
#{actual} (seconds)
88+
------------------------------------
89+
REPORT
90+
rescue Exception # rubocop:disable Lint/RescueException
91+
checkout_ref(ref0)
92+
raise
93+
end
94+
95+
def run_benchmark
96+
bundle
97+
parse_measurement Benchmark.measure {
98+
benchmark_tests
99+
}
100+
end
101+
102+
def run_benchmark_at_ref(ref)
103+
checkout_ref(ref)
104+
run_benchmark
105+
end
106+
107+
def bundle
108+
system('rm -f Gemfile.lock; bundle check || bundle --local --quiet || bundle --quiet')
109+
end
110+
111+
def parse_measurement(measurement)
112+
user = measurement.utime
113+
system = measurement.stime
114+
total = measurement.total
115+
real = measurement.real
116+
{
117+
:real => real,
118+
:total => total,
119+
:user => user,
120+
:system => system
121+
}
122+
end
77123
end
78124

79-
if $0 == __FILE__
80-
benchmark
125+
if $PROGRAM_NAME == __FILE__
126+
# Example configuration using all options
127+
# benchmarking = Benchmarking.new(prepend: 'TIMES=1000 CACHE_ON=true CACHING_SERIALIZER=true DEBUG=true', append: '> /dev/null')
128+
# benchmarking = Benchmarking.new(prepend: 'CACHE_ON=false CACHING_SERIALIZER=false', append: '> /dev/null')
129+
benchmarking = Benchmarking.new(append: '> /dev/null')
130+
test_type = ARGV[0]
131+
case test_type
132+
when 'current'
133+
puts "Ran in #{benchmarking.run_benchmark} seconds."
134+
else
135+
# Default: Compare current_branch to master
136+
# Optionally: pass in two refs as args to `bin/bench`
137+
# TODO: Consider checking across more revisions, to automatically find problems.
138+
puts benchmarking.benchmark_refs(ref1: ARGV[0], ref2: ARGV[1])
139+
end
81140
end

test/benchmark/benchmark_helper.rb

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
1+
# https://github.com/rails-api/active_model_serializers/pull/872
2+
# approx ref 792fb8a9053f8db3c562dae4f40907a582dd1720 to test against
13
require 'bundler/setup'
24

35
require 'rails'
4-
abort "Rails application already defined: #{Rails.application.class}" if Rails.application
6+
require 'active_model'
7+
require 'active_support'
8+
require 'active_support/json'
59
require 'action_controller'
610
require 'action_controller/test_case'
711
require 'action_controller/railtie'
8-
require 'active_support/json'
12+
abort "Rails application already defined: #{Rails.application.class}" if Rails.application
913
require 'minitest/autorun'
1014
# Ensure backward compatibility with Minitest 4
1115
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
1216

17+
# ref: https://gist.github.com/bf4/8744473
1318
class BenchmarkApp < Rails::Application
14-
if Rails.version.to_s.start_with? '4'
15-
config.action_controller.perform_caching = true
16-
config.active_support.test_order = :random
17-
ActionController::Base.cache_store = :memory_store
18-
config.eager_load = false
19-
config.secret_key_base = 'abc123'
20-
end
19+
config.action_controller.perform_caching = ENV['CACHE_ON'] != 'off'
20+
ActionController::Base.cache_store = :memory_store
21+
22+
# Set up production configuration
23+
config.eager_load = true
24+
config.cache_classes = true
25+
26+
config.active_support.test_order = :random
27+
config.secret_token = '1234'
28+
config.secret_key_base = 'abc123'
29+
config.logger = Logger.new(IO::NULL)
2130
end
2231

2332
require 'active_model_serializers'
@@ -38,5 +47,11 @@ def setup
3847
end
3948
end
4049

50+
# Needs to initialize app before any serializes are defined, for sanity's sake.
51+
# Otherwise, you have to manually set perform caching.
52+
Rails.application.initialize!
53+
4154
require_relative 'fixtures'
42-
BenchmarkApp.initialize!
55+
56+
# Uncomment the below to test that cache is in use.
57+
# ActiveSupport::Cache::Store.logger = Logger.new(STDERR)

test/benchmark/fixtures.rb

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ class BlogSerializer < ActiveModel::Serializer
1212
class CommentSerializer < ActiveModel::Serializer
1313
attributes :id, :body
1414

15-
def custom_options
16-
options
17-
end
15+
belongs_to :post
16+
belongs_to :author
1817
end
1918

2019
class PostSerializer < ActiveModel::Serializer
@@ -27,67 +26,81 @@ class PostSerializer < ActiveModel::Serializer
2726
def blog
2827
Blog.new(id: 999, name: 'Custom blog')
2928
end
30-
31-
def custom_options
32-
options
33-
end
3429
end
3530

3631
class CachingAuthorSerializer < AuthorSerializer
37-
cache key: 'writer'
32+
cache key: 'writer', skip_digest: true
3833
end
3934

4035
class CachingCommentSerializer < CommentSerializer
41-
cache expires_in: 1.day
36+
cache expires_in: 1.day, skip_digest: true
4237
end
4338

4439
class CachingPostSerializer < PostSerializer
45-
cache key: 'post', expires_in: 0.1
40+
cache key: 'post', expires_in: 0.1, skip_digest: true
4641
belongs_to :blog, serializer: BlogSerializer
4742
belongs_to :author, serializer: CachingAuthorSerializer
4843
has_many :comments, serializer: CachingCommentSerializer
4944
end
5045

51-
class Model
52-
def initialize(hash = {})
53-
@attributes = hash
46+
# ActiveModelSerializers::Model is a convenient
47+
# serializable class to inherit from when making
48+
# serializable non-activerecord objects.
49+
class BenchmarkModel
50+
include ActiveModel::Model
51+
include ActiveModel::Serializers::JSON
52+
53+
attr_reader :attributes
54+
55+
def initialize(attributes = {})
56+
@attributes = attributes
57+
super
5458
end
5559

60+
# Defaults to the downcased model name.
61+
def id
62+
attributes.fetch(:id) { self.class.name.downcase }
63+
end
64+
65+
# Defaults to the downcased model name and updated_at
5666
def cache_key
57-
"#{self.class.name.downcase}/#{id}-#{updated_at}"
67+
attributes.fetch(:cache_key) { "#{self.class.name.downcase}/#{id}-#{updated_at.strftime("%Y%m%d%H%M%S%9N")}" }
5868
end
5969

70+
# Defaults to the time the serializer file was modified.
6071
def updated_at
61-
@attributes[:updated_at] ||= Time.current.to_i
72+
attributes.fetch(:updated_at) { File.mtime(__FILE__) }
6273
end
6374

64-
def read_attribute_for_serialization(name)
65-
if name == :id || name == 'id'
66-
id
75+
def read_attribute_for_serialization(key)
76+
if key == :id || key == 'id'
77+
attributes.fetch(key) { id }
6778
else
68-
@attributes[name]
79+
attributes[key]
6980
end
7081
end
82+
end
7183

72-
def id
73-
@attributes[:id] || @attributes['id'] || object_id
74-
end
84+
class Comment < BenchmarkModel
85+
attr_accessor :id, :body
7586

76-
def to_param
77-
id
87+
def cache_key
88+
"#{self.class.name.downcase}/#{id}"
7889
end
90+
end
7991

80-
def method_missing(meth, *args)
81-
if meth.to_s =~ /^(.*)=$/
82-
@attributes[Regexp.last_match(1).to_sym] = args[0]
83-
elsif @attributes.key?(meth)
84-
@attributes[meth]
85-
else
86-
super
87-
end
92+
class Author < BenchmarkModel
93+
attr_accessor :id, :name, :posts
94+
end
95+
96+
class Post < BenchmarkModel
97+
attr_accessor :id, :title, :body, :comments, :blog, :author
98+
99+
def cache_key
100+
'benchmarking::post/1-20151215212620000000000'
88101
end
89102
end
90-
class Comment < Model; end
91-
class Author < Model; end
92-
class Post < Model; end
93-
class Blog < Model; end
103+
104+
class Blog < BenchmarkModel
105+
attr_accessor :id, :name
106+
end

0 commit comments

Comments
 (0)