mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-22 22:06:50 +00:00
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
This commit is contained in:
parent
952ab0438f
commit
4cc454d49b
@ -1,6 +1,7 @@
|
|||||||
inherit_from: .rubocop_todo.yml
|
inherit_from: .rubocop_todo.yml
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
|
TargetRubyVersion: 2.2
|
||||||
Exclude:
|
Exclude:
|
||||||
- config/initializers/forbidden_yaml.rb
|
- config/initializers/forbidden_yaml.rb
|
||||||
- !ruby/regexp /(vendor|bundle|bin|db|tmp)\/.*/
|
- !ruby/regexp /(vendor|bundle|bin|db|tmp)\/.*/
|
||||||
|
|||||||
6
Gemfile
6
Gemfile
@ -36,6 +36,11 @@ end
|
|||||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||||
gem 'tzinfo-data', platforms: (@windows_platforms + [:jruby])
|
gem 'tzinfo-data', platforms: (@windows_platforms + [:jruby])
|
||||||
|
|
||||||
|
group :bench do
|
||||||
|
# https://github.com/rails-api/active_model_serializers/commit/cb4459580a6f4f37f629bf3185a5224c8624ca76
|
||||||
|
gem 'benchmark-ips', require: false, group: :development
|
||||||
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'sqlite3', platform: (@windows_platforms + [:ruby])
|
gem 'sqlite3', platform: (@windows_platforms + [:ruby])
|
||||||
gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby
|
gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby
|
||||||
@ -45,5 +50,4 @@ end
|
|||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'rubocop', '~> 0.36', require: false
|
gem 'rubocop', '~> 0.36', require: false
|
||||||
gem 'git'
|
|
||||||
end
|
end
|
||||||
|
|||||||
35
Rakefile
35
Rakefile
@ -74,38 +74,3 @@ end
|
|||||||
|
|
||||||
desc 'CI test task'
|
desc 'CI test task'
|
||||||
task :ci => [:default]
|
task :ci => [:default]
|
||||||
|
|
||||||
require 'git'
|
|
||||||
require 'benchmark'
|
|
||||||
Rake::TestTask.new :benchmark_tests do |t|
|
|
||||||
t.libs << "test"
|
|
||||||
t.test_files = FileList['test/**/*_benchmark.rb']
|
|
||||||
t.ruby_opts = ['-r./test/test_helper.rb']
|
|
||||||
t.verbose = true
|
|
||||||
end
|
|
||||||
|
|
||||||
task :benchmark do
|
|
||||||
@git = Git.init('.')
|
|
||||||
ref = @git.current_branch
|
|
||||||
|
|
||||||
actual = run_benchmark_spec ref
|
|
||||||
master = run_benchmark_spec 'master'
|
|
||||||
|
|
||||||
@git.checkout(ref)
|
|
||||||
|
|
||||||
puts "\n\nResults ============================\n"
|
|
||||||
puts "------------------------------------~> (Branch) MASTER"
|
|
||||||
puts master
|
|
||||||
puts "------------------------------------\n\n"
|
|
||||||
|
|
||||||
puts "------------------------------------~> (Actual Branch) #{ref}"
|
|
||||||
puts actual
|
|
||||||
puts "------------------------------------"
|
|
||||||
end
|
|
||||||
|
|
||||||
def run_benchmark_spec(ref)
|
|
||||||
@git.checkout(ref)
|
|
||||||
response = Benchmark.realtime { Rake::Task['benchmark_tests'].invoke }
|
|
||||||
Rake::Task['benchmark_tests'].reenable
|
|
||||||
response
|
|
||||||
end
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
|
|||||||
spec.files = `git ls-files -z`.split("\x0")
|
spec.files = `git ls-files -z`.split("\x0")
|
||||||
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
||||||
spec.require_paths = ['lib']
|
spec.require_paths = ['lib']
|
||||||
|
spec.executables = []
|
||||||
|
|
||||||
spec.required_ruby_version = '>= 2.0.0'
|
spec.required_ruby_version = '>= 2.0.0'
|
||||||
|
|
||||||
|
|||||||
170
bin/bench
Executable file
170
bin/bench
Executable file
@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# ActiveModelSerializers Benchmark driver
|
||||||
|
# Adapted from
|
||||||
|
# https://github.com/ruby-bench/ruby-bench-suite/blob/8ad567f7e43a044ae48c36833218423bb1e2bd9d/rails/benchmarks/driver.rb
|
||||||
|
require 'bundler'
|
||||||
|
Bundler.setup
|
||||||
|
require 'json'
|
||||||
|
require 'pathname'
|
||||||
|
require 'optparse'
|
||||||
|
require 'digest'
|
||||||
|
require 'pathname'
|
||||||
|
require 'shellwords'
|
||||||
|
require 'logger'
|
||||||
|
require 'English'
|
||||||
|
|
||||||
|
class BenchmarkDriver
|
||||||
|
ROOT = Pathname File.expand_path(File.join('..', '..'), __FILE__)
|
||||||
|
BASE = ENV.fetch('BASE') { ROOT.join('test', 'dummy') }
|
||||||
|
ESCAPED_BASE = Shellwords.shellescape(BASE)
|
||||||
|
|
||||||
|
def self.benchmark(options)
|
||||||
|
new(options).run
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.parse_argv_and_run(argv = ARGV, options = {})
|
||||||
|
options = {
|
||||||
|
repeat_count: 1,
|
||||||
|
pattern: [],
|
||||||
|
env: 'CACHE_ON=on'
|
||||||
|
}.merge!(options)
|
||||||
|
|
||||||
|
OptionParser.new do |opts|
|
||||||
|
opts.banner = 'Usage: bin/bench [options]'
|
||||||
|
|
||||||
|
opts.on('-r', '--repeat-count [NUM]', 'Run benchmarks [NUM] times taking the best result') do |value|
|
||||||
|
options[:repeat_count] = value.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
opts.on('-p', '--pattern <PATTERN1,PATTERN2,PATTERN3>', 'Benchmark name pattern') do |value|
|
||||||
|
options[:pattern] = value.split(',')
|
||||||
|
end
|
||||||
|
|
||||||
|
opts.on('-e', '--env <var1=val1,var2=val2,var3=vale>', 'ENV variables to pass in') do |value|
|
||||||
|
options[:env] = value.split(',')
|
||||||
|
end
|
||||||
|
end.parse!(argv)
|
||||||
|
|
||||||
|
benchmark(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :commit_hash, :base
|
||||||
|
|
||||||
|
# Based on logfmt:
|
||||||
|
# https://www.brandur.org/logfmt
|
||||||
|
# For more complete implementation see:
|
||||||
|
# see https://github.com/arachnid-cb/logfmtr/blob/master/lib/logfmtr/base.rb
|
||||||
|
# For usage see:
|
||||||
|
# https://blog.codeship.com/logfmt-a-log-format-thats-easy-to-read-and-write/
|
||||||
|
# https://engineering.heroku.com/blogs/2014-09-05-hutils-explore-your-structured-data-logs/
|
||||||
|
# For Ruby parser see:
|
||||||
|
# https://github.com/cyberdelia/logfmt-ruby
|
||||||
|
def self.summary_logger(device = 'output.txt')
|
||||||
|
require 'time'
|
||||||
|
logger = Logger.new(device)
|
||||||
|
logger.level = Logger::INFO
|
||||||
|
logger.formatter = proc { |severity, datetime, progname, msg|
|
||||||
|
msg = "'#{msg}'"
|
||||||
|
"level=#{severity} time=#{datetime.utc.iso8601(6)} pid=#{Process.pid} progname=#{progname} msg=#{msg}#{$INPUT_RECORD_SEPARATOR}"
|
||||||
|
}
|
||||||
|
logger
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.stdout_logger
|
||||||
|
logger = Logger.new(STDOUT)
|
||||||
|
logger.level = Logger::INFO
|
||||||
|
logger.formatter = proc { |_, _, _, msg| "#{msg}#{$INPUT_RECORD_SEPARATOR}" }
|
||||||
|
logger
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(options)
|
||||||
|
@writer = ENV['SUMMARIZE'] ? self.class.summary_logger : self.class.stdout_logger
|
||||||
|
@repeat_count = options[:repeat_count]
|
||||||
|
@pattern = options[:pattern]
|
||||||
|
@commit_hash = options.fetch(:commit_hash) { `git rev-parse --short HEAD`.chomp }
|
||||||
|
@base = options.fetch(:base) { ESCAPED_BASE }
|
||||||
|
@env = Array(options[:env]).join(' ')
|
||||||
|
@rubyopt = options[:rubyopt] # TODO: rename
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
files.each do |path|
|
||||||
|
next if !@pattern.empty? && /#{@pattern.join('|')}/ !~ File.basename(path)
|
||||||
|
run_single(Shellwords.shellescape(path))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def files
|
||||||
|
Dir[File.join(base, 'bm_*')]
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_single(path)
|
||||||
|
script = "RAILS_ENV=production #{@env} ruby #{@rubyopt} #{path}"
|
||||||
|
environment = `ruby -v`.chomp.strip[/\d+\.\d+\.\d+\w+/]
|
||||||
|
|
||||||
|
runs_output = measure(script)
|
||||||
|
if runs_output.empty?
|
||||||
|
results = { error: :no_results }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
results['commit_hash'] = commit_hash
|
||||||
|
results['version'] = runs_output.first['version']
|
||||||
|
results['benchmark_run[environment]'] = environment
|
||||||
|
results['runs'] = []
|
||||||
|
|
||||||
|
runs_output.each do |output|
|
||||||
|
results['runs'] << {
|
||||||
|
'benchmark_type[category]' => output['label'],
|
||||||
|
'benchmark_run[result][iterations_per_second]' => output['iterations_per_second'].round(3),
|
||||||
|
'benchmark_run[result][total_allocated_objects_per_iteration]' => output['total_allocated_objects_per_iteration']
|
||||||
|
}
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
results && report(results)
|
||||||
|
end
|
||||||
|
|
||||||
|
def report(results)
|
||||||
|
@writer.info { 'Benchmark results:' }
|
||||||
|
@writer.info { JSON.pretty_generate(results) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def summarize(result)
|
||||||
|
puts "#{result['label']} #{result['iterations_per_second']}/ips; #{result['total_allocated_objects_per_iteration']} objects"
|
||||||
|
end
|
||||||
|
|
||||||
|
# FIXME: ` provides the full output but it'll return failed output as well.
|
||||||
|
def measure(script)
|
||||||
|
results = Hash.new { |h, k| h[k] = [] }
|
||||||
|
|
||||||
|
@repeat_count.times do
|
||||||
|
output = sh(script)
|
||||||
|
output.each_line do |line|
|
||||||
|
next if line.nil?
|
||||||
|
begin
|
||||||
|
result = JSON.parse(line)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
result = { error: line } # rubocop:disable Lint/UselessAssignment
|
||||||
|
else
|
||||||
|
summarize(result)
|
||||||
|
results[result['label']] << result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
results.map do |_, bm_runs|
|
||||||
|
bm_runs.sort_by do |run|
|
||||||
|
run['iterations_per_second']
|
||||||
|
end.last
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sh(cmd)
|
||||||
|
`#{cmd}`
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
BenchmarkDriver.parse_argv_and_run if $PROGRAM_NAME == __FILE__
|
||||||
39
bin/serve_dummy
Executable file
39
bin/serve_dummy
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
|
||||||
|
start)
|
||||||
|
config="${CONFIG_RU:-test/dummy/config.ru}"
|
||||||
|
bundle exec ruby -Ilib -S rackup "$config" --daemonize --pid tmp/dummy_app.pid --warn --server webrick
|
||||||
|
until [ -f 'tmp/dummy_app.pid' ]; do
|
||||||
|
sleep 0.1 # give it time to start.. I don't know a better way
|
||||||
|
done
|
||||||
|
cat tmp/dummy_app.pid
|
||||||
|
true
|
||||||
|
;;
|
||||||
|
|
||||||
|
stop)
|
||||||
|
if [ -f 'tmp/dummy_app.pid' ]; then
|
||||||
|
kill -TERM $(cat tmp/dummy_app.pid)
|
||||||
|
else
|
||||||
|
echo 'No pidfile'
|
||||||
|
false
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
status)
|
||||||
|
if [ -f 'tmp/dummy_app.pid' ]; then
|
||||||
|
kill -0 $(cat tmp/dummy_app.pid)
|
||||||
|
[ "$?" -eq 0 ]
|
||||||
|
else
|
||||||
|
echo 'No pidfile'
|
||||||
|
false
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [start|stop|status]"
|
||||||
|
;;
|
||||||
|
|
||||||
|
esac
|
||||||
@ -1,50 +0,0 @@
|
|||||||
require 'test_helper'
|
|
||||||
|
|
||||||
module ActionController
|
|
||||||
module Serialization
|
|
||||||
class SerializerTest < ActionController::TestCase
|
|
||||||
class PostController < ActionController::Base
|
|
||||||
|
|
||||||
def render_with_cache_enable
|
|
||||||
comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
|
|
||||||
author = Author.new(id: 1, name: 'Joao Moura.')
|
|
||||||
post = Post.new({ id: 1, title: 'New Post', blog:nil, body: 'Body', comments: [comment], author: author })
|
|
||||||
|
|
||||||
render json: post
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
tests PostController
|
|
||||||
|
|
||||||
def test_render_with_cache_enable
|
|
||||||
ActionController::Base.cache_store.clear
|
|
||||||
get :render_with_cache_enable
|
|
||||||
|
|
||||||
expected = {
|
|
||||||
id: 1,
|
|
||||||
title: 'New Post',
|
|
||||||
body: 'Body',
|
|
||||||
comments: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
body: 'ZOMG A COMMENT' }
|
|
||||||
],
|
|
||||||
blog: {
|
|
||||||
id: 999,
|
|
||||||
name: 'Custom blog'
|
|
||||||
},
|
|
||||||
author: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Joao Moura.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_equal 'application/json', @response.content_type
|
|
||||||
assert_equal expected.to_json, @response.body
|
|
||||||
|
|
||||||
get :render_with_cache_enable
|
|
||||||
assert_equal expected.to_json, @response.body
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
65
test/dummy/app.rb
Normal file
65
test/dummy/app.rb
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# https://github.com/rails-api/active_model_serializers/pull/872
|
||||||
|
# approx ref 792fb8a9053f8db3c562dae4f40907a582dd1720 to test against
|
||||||
|
require 'bundler/setup'
|
||||||
|
|
||||||
|
require 'rails'
|
||||||
|
require 'active_model'
|
||||||
|
require 'active_support'
|
||||||
|
require 'active_support/json'
|
||||||
|
require 'action_controller'
|
||||||
|
require 'action_controller/test_case'
|
||||||
|
require 'action_controller/railtie'
|
||||||
|
abort "Rails application already defined: #{Rails.application.class}" if Rails.application
|
||||||
|
|
||||||
|
class NullLogger < Logger
|
||||||
|
def initialize(*_args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def add(*_args, &_block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
class DummyLogger < ActiveSupport::Logger
|
||||||
|
def initialize
|
||||||
|
@file = StringIO.new
|
||||||
|
super(@file)
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages
|
||||||
|
@file.rewind
|
||||||
|
@file.read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# ref: https://gist.github.com/bf4/8744473
|
||||||
|
class DummyApp < Rails::Application
|
||||||
|
# Set up production configuration
|
||||||
|
config.eager_load = true
|
||||||
|
config.cache_classes = true
|
||||||
|
# CONFIG: CACHE_ON={on,off}
|
||||||
|
config.action_controller.perform_caching = ENV['CACHE_ON'] != 'off'
|
||||||
|
config.action_controller.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
|
||||||
|
|
||||||
|
config.active_support.test_order = :random
|
||||||
|
config.secret_token = 'S' * 30
|
||||||
|
config.secret_key_base = 'abc123'
|
||||||
|
config.consider_all_requests_local = false
|
||||||
|
|
||||||
|
# otherwise deadlock occured
|
||||||
|
config.middleware.delete 'Rack::Lock'
|
||||||
|
|
||||||
|
# to disable log files
|
||||||
|
config.logger = NullLogger.new
|
||||||
|
config.active_support.deprecation = :log
|
||||||
|
config.log_level = :info
|
||||||
|
end
|
||||||
|
|
||||||
|
require 'active_model_serializers'
|
||||||
|
|
||||||
|
# Initialize app before any serializers are defined, for running across revisions.
|
||||||
|
# ref: https://github.com/rails-api/active_model_serializers/pull/1478
|
||||||
|
Rails.application.initialize!
|
||||||
|
# HACK: Serializer::cache depends on the ActionController-dependent configs being set.
|
||||||
|
ActiveSupport.on_load(:action_controller) do
|
||||||
|
require_relative 'fixtures'
|
||||||
|
end
|
||||||
|
|
||||||
|
require_relative 'controllers'
|
||||||
66
test/dummy/benchmarking_support.rb
Normal file
66
test/dummy/benchmarking_support.rb
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
require 'benchmark/ips'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
# Add benchmarking runner from ruby-bench-suite
|
||||||
|
# https://github.com/ruby-bench/ruby-bench-suite/blob/master/rails/benchmarks/support/benchmark_rails.rb
|
||||||
|
module Benchmark
|
||||||
|
module ActiveModelSerializers
|
||||||
|
module TestMethods
|
||||||
|
def request(method, path)
|
||||||
|
response = Rack::MockRequest.new(DummyApp).send(method, path)
|
||||||
|
if response.status.in?([404, 500])
|
||||||
|
fail "omg, #{method}, #{path}, '#{response.status}', '#{response.body}'"
|
||||||
|
end
|
||||||
|
response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# extend Benchmark with an `ams` method
|
||||||
|
def ams(label = nil, time:, disable_gc: true, warmup: 3, &block)
|
||||||
|
fail ArgumentError.new, 'block should be passed' unless block_given?
|
||||||
|
|
||||||
|
if disable_gc
|
||||||
|
GC.disable
|
||||||
|
else
|
||||||
|
GC.enable
|
||||||
|
end
|
||||||
|
|
||||||
|
report = Benchmark.ips(time, warmup, true) do |x|
|
||||||
|
x.report(label) { yield }
|
||||||
|
end
|
||||||
|
|
||||||
|
entry = report.entries.first
|
||||||
|
|
||||||
|
output = {
|
||||||
|
label: label,
|
||||||
|
version: ::ActiveModel::Serializer::VERSION.to_s,
|
||||||
|
iterations_per_second: entry.ips,
|
||||||
|
iterations_per_second_standard_deviation: entry.stddev_percentage,
|
||||||
|
total_allocated_objects_per_iteration: count_total_allocated_objects(&block)
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
puts output
|
||||||
|
output
|
||||||
|
end
|
||||||
|
|
||||||
|
def count_total_allocated_objects
|
||||||
|
if block_given?
|
||||||
|
key =
|
||||||
|
if RUBY_VERSION < '2.2'
|
||||||
|
:total_allocated_object
|
||||||
|
else
|
||||||
|
:total_allocated_objects
|
||||||
|
end
|
||||||
|
|
||||||
|
before = GC.stat[key]
|
||||||
|
yield
|
||||||
|
after = GC.stat[key]
|
||||||
|
after - before
|
||||||
|
else
|
||||||
|
-1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
extend Benchmark::ActiveModelSerializers
|
||||||
|
end
|
||||||
116
test/dummy/bm_caching.rb
Normal file
116
test/dummy/bm_caching.rb
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
require_relative './benchmarking_support'
|
||||||
|
require_relative './app'
|
||||||
|
|
||||||
|
# https://github.com/ruby-bench/ruby-bench-suite/blob/8ad567f7e43a044ae48c36833218423bb1e2bd9d/rails/benchmarks/actionpack_router.rb
|
||||||
|
class ApiAssertion
|
||||||
|
include Benchmark::ActiveModelSerializers::TestMethods
|
||||||
|
BadRevisionError = Class.new(StandardError)
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
caching = get_caching
|
||||||
|
caching[:body].delete('meta')
|
||||||
|
non_caching = get_non_caching
|
||||||
|
non_caching[:body].delete('meta')
|
||||||
|
assert_responses(caching, non_caching)
|
||||||
|
rescue BadRevisionError => e
|
||||||
|
msg = { error: e.message }
|
||||||
|
STDERR.puts msg
|
||||||
|
STDOUT.puts msg
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_status(on_off = 'on'.freeze)
|
||||||
|
get("/status/#{on_off}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear
|
||||||
|
get('/clear')
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_caching(on_off = 'on'.freeze)
|
||||||
|
get("/caching/#{on_off}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_non_caching(on_off = 'on'.freeze)
|
||||||
|
get("/non_caching/#{on_off}")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def assert_responses(caching, non_caching)
|
||||||
|
assert_equal(caching[:code], 200, "Caching response failed: #{caching}")
|
||||||
|
assert_equal(caching[:body], expected, "Caching response format failed: \n+ #{caching[:body]}\n- #{expected}")
|
||||||
|
assert_equal(caching[:content_type], 'application/json; charset=utf-8', "Caching response content type failed: \n+ #{caching[:content_type]}\n- application/json")
|
||||||
|
assert_equal(non_caching[:code], 200, "Non caching response failed: #{non_caching}")
|
||||||
|
assert_equal(non_caching[:body], expected, "Non Caching response format failed: \n+ #{non_caching[:body]}\n- #{expected}")
|
||||||
|
assert_equal(non_caching[:content_type], 'application/json; charset=utf-8', "Non caching response content type failed: \n+ #{non_caching[:content_type]}\n- application/json")
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(url)
|
||||||
|
response = request(:get, url)
|
||||||
|
{ code: response.status, body: JSON.load(response.body), content_type: response.content_type }
|
||||||
|
end
|
||||||
|
|
||||||
|
def expected
|
||||||
|
@expected ||=
|
||||||
|
{
|
||||||
|
'post' => {
|
||||||
|
'id' => 1337,
|
||||||
|
'title' => 'New Post',
|
||||||
|
'body' => 'Body',
|
||||||
|
'comments' => [
|
||||||
|
{
|
||||||
|
'id' => 1,
|
||||||
|
'body' => 'ZOMG A COMMENT'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'blog' => {
|
||||||
|
'id' => 999,
|
||||||
|
'name' => 'Custom blog'
|
||||||
|
},
|
||||||
|
'author' => {
|
||||||
|
'id' => 42,
|
||||||
|
'name' => 'Joao Moura.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_equal(expected, actual, message)
|
||||||
|
return true if expected == actual
|
||||||
|
if ENV['FAIL_ASSERTION'] =~ /\Atrue|on|0\z/i # rubocop:disable Style/GuardClause
|
||||||
|
fail BadRevisionError, message
|
||||||
|
else
|
||||||
|
STDERR.puts message unless ENV['SUMMARIZE']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def debug(msg = '')
|
||||||
|
if block_given? && ENV['DEBUG'] =~ /\Atrue|on|0\z/i
|
||||||
|
STDERR.puts yield
|
||||||
|
else
|
||||||
|
STDERR.puts msg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assertion = ApiAssertion.new
|
||||||
|
assertion.valid?
|
||||||
|
# STDERR.puts assertion.get_status
|
||||||
|
|
||||||
|
time = 10
|
||||||
|
{
|
||||||
|
'caching on: caching serializers: gc off' => { disable_gc: true, send: [:get_caching, 'on'] },
|
||||||
|
# 'caching on: caching serializers: gc on' => { disable_gc: false, send: [:get_caching, 'on'] },
|
||||||
|
'caching off: caching serializers: gc off' => { disable_gc: true, send: [:get_caching, 'off'] },
|
||||||
|
# 'caching off: caching serializers: gc on' => { disable_gc: false, send: [:get_caching, 'off'] },
|
||||||
|
'caching on: non-caching serializers: gc off' => { disable_gc: true, send: [:get_non_caching, 'on'] },
|
||||||
|
# 'caching on: non-caching serializers: gc on' => { disable_gc: false, send: [:get_non_caching, 'on'] },
|
||||||
|
'caching off: non-caching serializers: gc off' => { disable_gc: true, send: [:get_non_caching, 'off'] }
|
||||||
|
# 'caching off: non-caching serializers: gc on' => { disable_gc: false, send: [:get_non_caching, 'off'] }
|
||||||
|
}.each do |label, options|
|
||||||
|
assertion.clear
|
||||||
|
Benchmark.ams(label, time: time, disable_gc: options[:disable_gc]) do
|
||||||
|
assertion.send(*options[:send])
|
||||||
|
end
|
||||||
|
# STDERR.puts assertion.get_status(options[:send][-1])
|
||||||
|
end
|
||||||
3
test/dummy/config.ru
Normal file
3
test/dummy/config.ru
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
require File.expand_path(['..', 'app'].join(File::SEPARATOR), __FILE__)
|
||||||
|
|
||||||
|
run Rails.application
|
||||||
77
test/dummy/controllers.rb
Normal file
77
test/dummy/controllers.rb
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
class PostController < ActionController::Base
|
||||||
|
POST =
|
||||||
|
begin
|
||||||
|
if ENV['BENCH_STRESS']
|
||||||
|
comments = (0..50).map do |i|
|
||||||
|
Comment.new(id: i, body: 'ZOMG A COMMENT')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
comments = [Comment.new(id: 1, body: 'ZOMG A COMMENT')]
|
||||||
|
end
|
||||||
|
author = Author.new(id: 42, name: 'Joao Moura.')
|
||||||
|
Post.new(id: 1337, title: 'New Post', blog: nil, body: 'Body', comments: comments, author: author)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_with_caching_serializer
|
||||||
|
toggle_cache_status
|
||||||
|
render json: POST, serializer: CachingPostSerializer, adapter: :json, meta: { caching: perform_caching }
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_with_non_caching_serializer
|
||||||
|
toggle_cache_status
|
||||||
|
render json: POST, adapter: :json, meta: { caching: perform_caching }
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_cache_status
|
||||||
|
toggle_cache_status
|
||||||
|
# Uncomment to debug
|
||||||
|
# STDERR.puts cache_store.class
|
||||||
|
# STDERR.puts cache_dependencies
|
||||||
|
# ActiveSupport::Cache::Store.logger.debug [ActiveModelSerializers.config.cache_store, ActiveModelSerializers.config.perform_caching, CachingPostSerializer._cache, perform_caching, params].inspect
|
||||||
|
render json: { caching: perform_caching, meta: { cache_log: cache_messages, cache_status: cache_status } }.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear
|
||||||
|
ActionController::Base.cache_store.clear
|
||||||
|
# Test caching is on
|
||||||
|
# Uncomment to turn on logger; possible performance issue
|
||||||
|
# logger = DummyLogger.new
|
||||||
|
# ActiveSupport::Cache::Store.logger = logger # seems to be the best way
|
||||||
|
#
|
||||||
|
# the below is used in some rails tests but isn't available/working in all versions, so far as I can tell
|
||||||
|
# https://github.com/rails/rails/pull/15943
|
||||||
|
# ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args|
|
||||||
|
# logger.debug ActiveSupport::Notifications::Event.new(*args)
|
||||||
|
# end
|
||||||
|
render json: 'ok'.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def cache_status
|
||||||
|
{
|
||||||
|
controller: perform_caching,
|
||||||
|
app: Rails.configuration.action_controller.perform_caching,
|
||||||
|
serializers: Rails.configuration.serializers.each_with_object({}) { |serializer, data| data[serializer.name] = serializer._cache.present? }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_messages
|
||||||
|
ActiveSupport::Cache::Store.logger.is_a?(DummyLogger) && ActiveSupport::Cache::Store.logger.messages.split("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def toggle_cache_status
|
||||||
|
case params[:on]
|
||||||
|
when 'on'.freeze then self.perform_caching = true
|
||||||
|
when 'off'.freeze then self.perform_caching = false
|
||||||
|
else nil # no-op
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
get '/status(/:on)' => 'post#render_cache_status'
|
||||||
|
get '/clear' => 'post#clear'
|
||||||
|
get '/caching(/:on)' => 'post#render_with_caching_serializer'
|
||||||
|
get '/non_caching(/:on)' => 'post#render_with_non_caching_serializer'
|
||||||
|
end
|
||||||
157
test/dummy/fixtures.rb
Normal file
157
test/dummy/fixtures.rb
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
Rails.configuration.serializers = []
|
||||||
|
class AuthorSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :name
|
||||||
|
|
||||||
|
has_many :posts, embed: :ids
|
||||||
|
has_one :bio
|
||||||
|
end
|
||||||
|
Rails.configuration.serializers << AuthorSerializer
|
||||||
|
|
||||||
|
class BlogSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :name
|
||||||
|
end
|
||||||
|
Rails.configuration.serializers << BlogSerializer
|
||||||
|
|
||||||
|
class CommentSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :body
|
||||||
|
|
||||||
|
belongs_to :post
|
||||||
|
belongs_to :author
|
||||||
|
end
|
||||||
|
Rails.configuration.serializers << CommentSerializer
|
||||||
|
|
||||||
|
class PostSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :title, :body
|
||||||
|
|
||||||
|
has_many :comments, serializer: CommentSerializer
|
||||||
|
belongs_to :blog, serializer: BlogSerializer
|
||||||
|
belongs_to :author, serializer: AuthorSerializer
|
||||||
|
|
||||||
|
def blog
|
||||||
|
Blog.new(id: 999, name: 'Custom blog')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Rails.configuration.serializers << PostSerializer
|
||||||
|
|
||||||
|
class CachingAuthorSerializer < AuthorSerializer
|
||||||
|
cache key: 'writer', only: [:name], skip_digest: true
|
||||||
|
end
|
||||||
|
Rails.configuration.serializers << CachingAuthorSerializer
|
||||||
|
|
||||||
|
class CachingCommentSerializer < CommentSerializer
|
||||||
|
cache expires_in: 1.day, skip_digest: true
|
||||||
|
end
|
||||||
|
Rails.configuration.serializers << CachingCommentSerializer
|
||||||
|
|
||||||
|
class CachingPostSerializer < PostSerializer
|
||||||
|
cache key: 'post', expires_in: 0.1, skip_digest: true
|
||||||
|
belongs_to :blog, serializer: BlogSerializer
|
||||||
|
belongs_to :author, serializer: CachingAuthorSerializer
|
||||||
|
has_many :comments, serializer: CachingCommentSerializer
|
||||||
|
end
|
||||||
|
Rails.configuration.serializers << CachingPostSerializer
|
||||||
|
|
||||||
|
if ENV['ENABLE_ACTIVE_RECORD'] == 'true'
|
||||||
|
require 'active_record'
|
||||||
|
|
||||||
|
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
||||||
|
ActiveRecord::Schema.define do
|
||||||
|
self.verbose = false
|
||||||
|
|
||||||
|
create_table :blogs, force: true do |t|
|
||||||
|
t.string :name
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
create_table :authors, force: true do |t|
|
||||||
|
t.string :name
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
create_table :posts, force: true do |t|
|
||||||
|
t.string :title
|
||||||
|
t.text :body
|
||||||
|
t.references :author
|
||||||
|
t.references :blog
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
create_table :comments, force: true do |t|
|
||||||
|
t.text :body
|
||||||
|
t.references :author
|
||||||
|
t.references :post
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Comment < ActiveRecord::Base
|
||||||
|
belongs_to :author
|
||||||
|
belongs_to :post
|
||||||
|
end
|
||||||
|
|
||||||
|
class Author < ActiveRecord::Base
|
||||||
|
has_many :posts
|
||||||
|
has_many :comments
|
||||||
|
end
|
||||||
|
|
||||||
|
class Post < ActiveRecord::Base
|
||||||
|
has_many :comments
|
||||||
|
belongs_to :author
|
||||||
|
belongs_to :blog
|
||||||
|
end
|
||||||
|
|
||||||
|
class Blog < ActiveRecord::Base
|
||||||
|
has_many :posts
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# ActiveModelSerializers::Model is a convenient
|
||||||
|
# serializable class to inherit from when making
|
||||||
|
# serializable non-activerecord objects.
|
||||||
|
class DummyModel
|
||||||
|
include ActiveModel::Model
|
||||||
|
include ActiveModel::Serializers::JSON
|
||||||
|
|
||||||
|
attr_reader :attributes
|
||||||
|
|
||||||
|
def initialize(attributes = {})
|
||||||
|
@attributes = attributes
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
# Defaults to the downcased model name.
|
||||||
|
def id
|
||||||
|
attributes.fetch(:id) { self.class.name.downcase }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Defaults to the downcased model name and updated_at
|
||||||
|
def cache_key
|
||||||
|
attributes.fetch(:cache_key) { "#{self.class.name.downcase}/#{id}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Defaults to the time the serializer file was modified.
|
||||||
|
def updated_at
|
||||||
|
@updated_at ||= attributes.fetch(:updated_at) { File.mtime(__FILE__) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_attribute_for_serialization(key)
|
||||||
|
if key == :id || key == 'id'
|
||||||
|
attributes.fetch(key) { id }
|
||||||
|
else
|
||||||
|
attributes[key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Comment < DummyModel
|
||||||
|
attr_accessor :id, :body
|
||||||
|
end
|
||||||
|
|
||||||
|
class Author < DummyModel
|
||||||
|
attr_accessor :id, :name, :posts
|
||||||
|
end
|
||||||
|
|
||||||
|
class Post < DummyModel
|
||||||
|
attr_accessor :id, :title, :body, :comments, :blog, :author
|
||||||
|
end
|
||||||
|
|
||||||
|
class Blog < DummyModel
|
||||||
|
attr_accessor :id, :name
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user