Skip to content

Commit e2f1a82

Browse files
committed
Setup benchmarking structure
- Setup dummy app files in `test/dummy` - Setup dummy test server `bin/serve_dummy - Note: Serializer caching can be completely disabled by passing in `CACHE_ON=off bin/serve_dummy start` since Serializer#_cache is only set at boot. - run with - ./bin/bench - `bin/bench` etc adapted from ruby-bench-suite - target files are `test/dummy/bm_*.rb`. Just add another to run it. - benchmark cache/no cache - remove rake dependency that loads unnecessary files - remove git gem dependency - Running over revisions to be added in subsequent PR
1 parent 03a0a54 commit e2f1a82

13 files changed

+700
-86
lines changed

.rubocop.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
inherit_from: .rubocop_todo.yml
22

33
AllCops:
4+
TargetRubyVersion: 2.2
45
Exclude:
56
- config/initializers/forbidden_yaml.rb
67
- !ruby/regexp /(vendor|bundle|bin|db|tmp)\/.*/

Gemfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ end
3636
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
3737
gem 'tzinfo-data', platforms: (@windows_platforms + [:jruby])
3838

39+
group :bench do
40+
# https://github.com/rails-api/active_model_serializers/commit/cb4459580a6f4f37f629bf3185a5224c8624ca76
41+
gem 'benchmark-ips', require: false, group: :development
42+
end
43+
3944
group :test do
4045
gem 'sqlite3', platform: (@windows_platforms + [:ruby])
4146
gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby
@@ -45,5 +50,4 @@ end
4550

4651
group :development, :test do
4752
gem 'rubocop', '~> 0.36', require: false
48-
gem 'git'
4953
end

Rakefile

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -74,38 +74,3 @@ end
7474

7575
desc 'CI test task'
7676
task :ci => [:default]
77-
78-
require 'git'
79-
require 'benchmark'
80-
Rake::TestTask.new :benchmark_tests do |t|
81-
t.libs << "test"
82-
t.test_files = FileList['test/**/*_benchmark.rb']
83-
t.ruby_opts = ['-r./test/test_helper.rb']
84-
t.verbose = true
85-
end
86-
87-
task :benchmark do
88-
@git = Git.init('.')
89-
ref = @git.current_branch
90-
91-
actual = run_benchmark_spec ref
92-
master = run_benchmark_spec 'master'
93-
94-
@git.checkout(ref)
95-
96-
puts "\n\nResults ============================\n"
97-
puts "------------------------------------~> (Branch) MASTER"
98-
puts master
99-
puts "------------------------------------\n\n"
100-
101-
puts "------------------------------------~> (Actual Branch) #{ref}"
102-
puts actual
103-
puts "------------------------------------"
104-
end
105-
106-
def run_benchmark_spec(ref)
107-
@git.checkout(ref)
108-
response = Benchmark.realtime { Rake::Task['benchmark_tests'].invoke }
109-
Rake::Task['benchmark_tests'].reenable
110-
response
111-
end

active_model_serializers.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
1717
spec.files = `git ls-files -z`.split("\x0")
1818
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
1919
spec.require_paths = ['lib']
20+
spec.executables = []
2021

2122
spec.required_ruby_version = '>= 2.0.0'
2223

bin/bench

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/usr/bin/env ruby
2+
# ActiveModelSerializers Benchmark driver
3+
# Adapted from
4+
# https://github.com/ruby-bench/ruby-bench-suite/blob/8ad567f7e43a044ae48c36833218423bb1e2bd9d/rails/benchmarks/driver.rb
5+
require 'bundler'
6+
Bundler.setup
7+
require 'json'
8+
require 'pathname'
9+
require 'optparse'
10+
require 'digest'
11+
require 'pathname'
12+
require 'shellwords'
13+
require 'logger'
14+
require 'English'
15+
16+
class BenchmarkDriver
17+
ROOT = Pathname File.expand_path(File.join('..', '..'), __FILE__)
18+
BASE = ENV.fetch('BASE') { ROOT.join('test', 'dummy') }
19+
ESCAPED_BASE = Shellwords.shellescape(BASE)
20+
21+
def self.benchmark(options)
22+
new(options).run
23+
end
24+
25+
def self.parse_argv_and_run(argv = ARGV, options = {})
26+
options = {
27+
repeat_count: 1,
28+
pattern: [],
29+
env: 'CACHE_ON=on'
30+
}.merge!(options)
31+
32+
OptionParser.new do |opts|
33+
opts.banner = 'Usage: bin/bench [options]'
34+
35+
opts.on('-r', '--repeat-count [NUM]', 'Run benchmarks [NUM] times taking the best result') do |value|
36+
options[:repeat_count] = value.to_i
37+
end
38+
39+
opts.on('-p', '--pattern <PATTERN1,PATTERN2,PATTERN3>', 'Benchmark name pattern') do |value|
40+
options[:pattern] = value.split(',')
41+
end
42+
43+
opts.on('-e', '--env <var1=val1,var2=val2,var3=vale>', 'ENV variables to pass in') do |value|
44+
options[:env] = value.split(',')
45+
end
46+
end.parse!(argv)
47+
48+
benchmark(options)
49+
end
50+
51+
attr_reader :commit_hash, :base
52+
53+
# Based on logfmt:
54+
# https://www.brandur.org/logfmt
55+
# For more complete implementation see:
56+
# see https://github.com/arachnid-cb/logfmtr/blob/master/lib/logfmtr/base.rb
57+
# For usage see:
58+
# https://blog.codeship.com/logfmt-a-log-format-thats-easy-to-read-and-write/
59+
# https://engineering.heroku.com/blogs/2014-09-05-hutils-explore-your-structured-data-logs/
60+
# For Ruby parser see:
61+
# https://github.com/cyberdelia/logfmt-ruby
62+
def self.summary_logger(device = 'output.txt')
63+
require 'time'
64+
logger = Logger.new(device)
65+
logger.level = Logger::INFO
66+
logger.formatter = proc { |severity, datetime, progname, msg|
67+
msg = "'#{msg}'"
68+
"level=#{severity} time=#{datetime.utc.iso8601(6)} pid=#{Process.pid} progname=#{progname} msg=#{msg}#{$INPUT_RECORD_SEPARATOR}"
69+
}
70+
logger
71+
end
72+
73+
def self.stdout_logger
74+
logger = Logger.new(STDOUT)
75+
logger.level = Logger::INFO
76+
logger.formatter = proc { |_, _, _, msg| "#{msg}#{$INPUT_RECORD_SEPARATOR}" }
77+
logger
78+
end
79+
80+
def initialize(options)
81+
@writer = ENV['SUMMARIZE'] ? self.class.summary_logger : self.class.stdout_logger
82+
@repeat_count = options[:repeat_count]
83+
@pattern = options[:pattern]
84+
@commit_hash = options.fetch(:commit_hash) { `git rev-parse --short HEAD`.chomp }
85+
@base = options.fetch(:base) { ESCAPED_BASE }
86+
@env = Array(options[:env]).join(' ')
87+
@rubyopt = options[:rubyopt] # TODO: rename
88+
end
89+
90+
def run
91+
files.each do |path|
92+
next if !@pattern.empty? && /#{@pattern.join('|')}/ !~ File.basename(path)
93+
run_single(Shellwords.shellescape(path))
94+
end
95+
end
96+
97+
private
98+
99+
def files
100+
Dir[File.join(base, 'bm_*')]
101+
end
102+
103+
def run_single(path)
104+
script = "RAILS_ENV=production #{@env} ruby #{@rubyopt} #{path}"
105+
environment = `ruby -v`.chomp.strip[/\d+\.\d+\.\d+\w+/]
106+
107+
runs_output = measure(script)
108+
if runs_output.empty?
109+
results = { error: :no_results }
110+
return
111+
end
112+
113+
results = {}
114+
results['commit_hash'] = commit_hash
115+
results['version'] = runs_output.first['version']
116+
results['benchmark_run[environment]'] = environment
117+
results['runs'] = []
118+
119+
runs_output.each do |output|
120+
results['runs'] << {
121+
'benchmark_type[category]' => output['label'],
122+
'benchmark_run[result][iterations_per_second]' => output['iterations_per_second'].round(3),
123+
'benchmark_run[result][total_allocated_objects_per_iteration]' => output['total_allocated_objects_per_iteration']
124+
}
125+
end
126+
ensure
127+
results && report(results)
128+
end
129+
130+
def report(results)
131+
@writer.info { 'Benchmark results:' }
132+
@writer.info { JSON.pretty_generate(results) }
133+
end
134+
135+
def summarize(result)
136+
puts "#{result['label']} #{result['iterations_per_second']}/ips; #{result['total_allocated_objects_per_iteration']} objects"
137+
end
138+
139+
# FIXME: ` provides the full output but it'll return failed output as well.
140+
def measure(script)
141+
results = Hash.new { |h, k| h[k] = [] }
142+
143+
@repeat_count.times do
144+
output = sh(script)
145+
output.each_line do |line|
146+
next if line.nil?
147+
begin
148+
result = JSON.parse(line)
149+
rescue JSON::ParserError
150+
result = { error: line } # rubocop:disable Lint/UselessAssignment
151+
else
152+
summarize(result)
153+
results[result['label']] << result
154+
end
155+
end
156+
end
157+
158+
results.map do |_, bm_runs|
159+
bm_runs.sort_by do |run|
160+
run['iterations_per_second']
161+
end.last
162+
end
163+
end
164+
165+
def sh(cmd)
166+
`#{cmd}`
167+
end
168+
end
169+
170+
BenchmarkDriver.parse_argv_and_run if $PROGRAM_NAME == __FILE__

bin/serve_dummy

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
case "$1" in
5+
6+
start)
7+
config="${CONFIG_RU:-test/dummy/config.ru}"
8+
bundle exec ruby -Ilib -S rackup "$config" --daemonize --pid tmp/dummy_app.pid --warn --server webrick
9+
until [ -f 'tmp/dummy_app.pid' ]; do
10+
sleep 0.1 # give it time to start.. I don't know a better way
11+
done
12+
cat tmp/dummy_app.pid
13+
true
14+
;;
15+
16+
stop)
17+
if [ -f 'tmp/dummy_app.pid' ]; then
18+
kill -TERM $(cat tmp/dummy_app.pid)
19+
else
20+
echo 'No pidfile'
21+
false
22+
fi
23+
;;
24+
25+
status)
26+
if [ -f 'tmp/dummy_app.pid' ]; then
27+
kill -0 $(cat tmp/dummy_app.pid)
28+
[ "$?" -eq 0 ]
29+
else
30+
echo 'No pidfile'
31+
false
32+
fi
33+
;;
34+
35+
*)
36+
echo "Usage: $0 [start|stop|status]"
37+
;;
38+
39+
esac

test/benchmark/serialization_benchmark.rb

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)