bench.rb
# frozen_string_literal: true
# rubocop:disable Metrics/AbcSize,Metrics/MethodLength
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'activerecord', require: 'active_record'
gem 'activemodel'
gem 'sqlite3'
gem 'light_serializer'
gem 'active_model_serializers'
gem 'blueprinter'
gem 'surrealist'
gem 'benchmark-ips', require: 'benchmark/ips'
end
puts 'Gems installed and loaded!'
ActiveRecord::Base.establish_connection(
adapter: 'sqlite3',
database: ':memory:'
)
ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define do
create_table :users do |table|
table.column :name, :string
table.column :email, :string
end
create_table :authors do |table|
table.column :name, :string
table.column :last_name, :string
table.column :age, :int
end
create_table :books do |table|
table.column :title, :string
table.column :year, :string
table.belongs_to :author, foreign_key: true
end
end
ActiveModelSerializers.config.adapter = :json
def random_name
('a'..'z').to_a.shuffle.join('').first(10).capitalize
end
class User < ActiveRecord::Base
include Surrealist
json_schema { { name: String, email: String } }
end
class UserLightSerializer < LightSerializer::Serializer
attributes(:name, :email)
end
class UserSerializer < ActiveModel::Serializer
attributes :name, :email
end
class UserSurrealistSerializer < Surrealist::Serializer
json_schema { { name: String, email: String } }
end
class UserAMSSerializer < ActiveModel::Serializer
attributes :name, :email
end
class UserBlueprint < Blueprinter::Base
fields :name, :email
end
### Associations ###
class BookLightSerializer < LightSerializer::Serializer
attributes(:title, :year)
end
class AuthorLightSerializer < LightSerializer::Serializer
attributes(
:name,
:last_name,
:full_name,
:age,
books: BookLightSerializer
)
end
class AuthorSurrealistSerializer < Surrealist::Serializer
json_schema do
{ name: String, last_name: String, full_name: String, age: Integer, books: Array }
end
def books
object.books.to_a
end
def full_name
"#{object.name} #{object.last_name}"
end
end
class BookSurrealistSerializer < Surrealist::Serializer
json_schema { { title: String, year: String } }
end
class BookAMSSerializer < ActiveModel::Serializer
attributes :title, :year
end
class BookBlueprint < Blueprinter::Base
fields :title, :year
end
class AuthorAMSSerializer < ActiveModel::Serializer
attributes :name, :last_name, :full_name, :age
has_many :books, serializer: BookAMSSerializer
end
class AuthorBlueprint < Blueprinter::Base
fields :name, :last_name, :age
field :full_name do |author|
"#{author.name} #{author.last_name}"
end
association :books, blueprint: BookBlueprint
end
class Author < ActiveRecord::Base
include Surrealist
surrealize_with AuthorSurrealistSerializer
has_many :books
def full_name
"#{name} #{last_name}"
end
end
class Book < ActiveRecord::Base
include Surrealist
surrealize_with BookSurrealistSerializer
belongs_to :author, required: true
end
N = 3000
N.times { User.create!(name: random_name, email: "#{random_name}@test.com") }
(N / 2).times { Author.create!(name: random_name, last_name: random_name, age: rand(80)) }
N.times { Book.create!(title: random_name, year: "19#{rand(10..99)}", author_id: rand(1..N / 2)) }
def sort(obj)
case obj
when Array then obj.map { |el| sort(el) }
when Hash then obj.transform_values { |v| sort(v) }
else obj
end
end
def check_correctness(serializers)
results = serializers.map(&:call).map { |r| sort(JSON.parse(r)) }
raise 'Results are not the same' if results.uniq.size > 1
end
def benchmark(names, serializers)
check_correctness(serializers)
Benchmark.ips do |x|
x.config(time: 5, warmup: 2)
names.zip(serializers).each { |name, proc| x.report(name, &proc) }
x.compare!
end
end
def benchmark_instance(ams_arg: '', oj_arg: '')
user = User.find(rand(1..N))
names = ["AMS#{[ams_arg, oj_arg].join(' ')}: instance",
'Surrealist: instance through .surrealize',
'Light: instance through .to_json',
'Surrealist: instance through Surrealist::Serializer',
"ActiveModel::Serializers::JSON#{oj_arg} instance",
"Blueprinter#{oj_arg}"]
serializers = [-> { UserAMSSerializer.new(user).to_json },
-> { user.surrealize },
-> { UserLightSerializer.new(user).to_json },
-> { UserSurrealistSerializer.new(user).surrealize },
-> { user.to_json(only: %i[name email]) },
-> { UserBlueprint.render(user) }]
benchmark(names, serializers)
end
def benchmark_collection(ams_arg: '', oj_arg: '')
users = User.all
names = ["AMS#{[ams_arg, oj_arg].join(' ')}: collection",
'Surrealist: collection through Surrealist.surrealize_collection()',
'Light: collection through .to_json',
'Surrealist: collection through Surrealist::Serializer',
"ActiveModel::Serializers::JSON#{oj_arg} collection",
"Blueprinter collection#{oj_arg}"]
serializers = [lambda do
ActiveModel::Serializer::CollectionSerializer.new(
users, root: nil, serializer: UserAMSSerializer
).to_json
end,
-> { Surrealist.surrealize_collection(users) },
-> { LightSerializer::SerializeCollection.new(users, serializer: UserLightSerializer).to_json },
-> { UserSurrealistSerializer.new(users).surrealize },
-> { users.to_json(only: %i[name email]) },
-> { UserBlueprint.render(users) }]
benchmark(names, serializers)
end
def benchmark_associations_instance
instance = Author.find(rand((1..(N / 2))))
names = ['AMS (associations): instance',
'Surrealist (associations): instance through .surrealize',
'Light (associations): instance through .to_json',
'Surrealist (associations): instance through Surrealist::Serializer',
'ActiveModel::Serializers::JSON (associations)',
'Blueprinter (associations)']
serializers = [-> { AuthorAMSSerializer.new(instance).to_json },
-> { instance.surrealize },
-> { AuthorLightSerializer.new(instance).to_json },
-> { AuthorSurrealistSerializer.new(instance).surrealize },
lambda do
instance.to_json(only: %i[name last_name age], methods: %i[full_name],
include: { books: { only: %i[title year] } })
end,
-> { AuthorBlueprint.render(instance) }]
benchmark(names, serializers)
end
def benchmark_associations_collection
collection = Author.all
names = ['AMS (associations): collection',
'Surrealist (associations): collection through Surrealist.surrealize_collection()',
'Light (associations): collection through .to_json',
'Surrealist (associations): collection through Surrealist::Serializer',
'ActiveModel::Serializers::JSON (associations): collection',
'Blueprinter (associations): collection']
serializers = [lambda do
ActiveModel::Serializer::CollectionSerializer.new(
collection, root: nil, serializer: AuthorAMSSerializer
).to_json
end,
-> { Surrealist.surrealize_collection(collection) },
-> { LightSerializer::SerializeCollection.new(collection, serializer: AuthorLightSerializer).to_json },
-> { AuthorSurrealistSerializer.new(collection).surrealize },
lambda do
collection.to_json(only: %i[name last_name age], methods: %i[full_name],
include: { books: { only: %i[title year] } })
end,
-> { AuthorBlueprint.render(collection) }]
benchmark(names, serializers)
end
# Default configuration
benchmark_instance
benchmark_collection
# With AMS logger turned off
puts "\n------- Turning off AMS logger -------\n"
ActiveModelSerializers.logger.level = Logger::Severity::UNKNOWN
benchmark_instance(ams_arg: '(without logging)')
benchmark_collection(ams_arg: '(without logging)')
# Associations
benchmark_associations_instance
benchmark_associations_collection
puts "\n------- Enabling Oj.optimize_rails() & Blueprinter config.generator = Oj -------\n"
Oj.optimize_rails
Blueprinter.configure do |config|
config.generator = Oj
end
benchmark_instance(ams_arg: '(without logging)', oj_arg: '(with Oj)')
benchmark_collection(ams_arg: '(without logging)', oj_arg: '(with Oj)')
# Associations
benchmark_associations_instance
benchmark_associations_collection
# rubocop:enable Metrics/AbcSize,Metrics/MethodLength