Skip to content

Instantly share code, notes, and snippets.

@casperisfine
Last active July 23, 2025 13:06
Show Gist options
  • Save casperisfine/ca47694a68c65eefb9866e260b2333f8 to your computer and use it in GitHub Desktop.
Save casperisfine/ca47694a68c65eefb9866e260b2333f8 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails"
# If you want to test against edge Rails replace the previous line with this:
# gem "rails", github: "rails/rails", branch: "main"
gem "sqlite3"
gem "benchmark-ips"
gem "vernier"
end
require "active_record"
require "benchmark/ips"
require "logger"
# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.define do
create_table :products, force: true do |t|
end
create_table :product_variants, force: true do |t|
t.integer :product_id
15.times do |i|
t.string :"string_#{i}", null: false, default: ""
end
10.times do |i|
t.integer :"integer_#{i}"
end
10.times do |i|
t.boolean :"bool_#{i}"
end
t.boolean :is_not_deleted, default: true, null: false
t.timestamps
end
end
class Product < ActiveRecord::Base
has_many :product_variants
end
class ProductVariant < ActiveRecord::Base
belongs_to :product
scope :deleted, -> { ignoring_soft_deletion.where(is_not_deleted: nil) }
scope :not_deleted, -> { where(is_not_deleted: true) }
scope :ignoring_soft_deletion, -> { unscope(where: :is_not_deleted) }
default_scope { not_deleted }
end
class ProductVariantPORO
attr_accessor :product_id
15.times do |i|
attr_accessor :"string_#{i}"
end
10.times do |i|
attr_accessor :"integer_#{i}"
end
10.times do |i|
attr_accessor :"bool_#{i}"
end
init = [
"# frozen_string_literal: true",
"def initialize(attributes)",
]
init << "@product_id = attributes[:product_id]"
15.times do |i|
init << "@string_#{i} = attributes[:string_#{i}] || ''"
end
10.times do |i|
init << "@integer_#{i} = attributes[:integer_#{i}] || ''"
end
10.times do |i|
init << "@bool_#{i} = attributes[:bool_#{i}] || ''"
end
init << "end"
class_eval(init.join("\n"))
end
attributes = {
product_id: 42,
}
15.times do |i|
attributes[:"string_#{i}"] = "#{i}" * 20
end
10.times do |i|
attributes[:"integer_#{i}"] = i
end
10.times do |i|
attributes[:"integer_#{i}"] = false
end
Benchmark.ips do |x|
x.report("PORO") do
2_000.times { ProductVariantPORO.new(attributes) }
end
x.report("ActiveRecord") do
2_000.times { ProductVariant.new(attributes) }
end
x.compare!(order: :baseline)
end
# require "vernier"
# Vernier.profile(out: "ar_new_profile.json") do
# 200_000.times { ProductVariant.new(attributes) }
# end
#
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails"
# If you want to test against edge Rails replace the previous line with this:
# gem "rails", github: "rails/rails", branch: "main"
gem "sqlite3"
gem "benchmark-ips"
gem "vernier"
end
require "active_record"
require "benchmark/ips"
require "logger"
# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.define do
create_table :products, force: true do |t|
end
create_table :product_variants, force: true do |t|
t.integer :product_id
15.times do |i|
t.string :"string_#{i}", null: false, default: ""
end
10.times do |i|
t.integer :"integer_#{i}"
end
10.times do |i|
t.boolean :"bool_#{i}"
end
t.boolean :is_not_deleted, default: true, null: false
t.timestamps
end
end
class Product < ActiveRecord::Base
has_many :product_variants
end
class ProductVariant < ActiveRecord::Base
belongs_to :product
scope :deleted, -> { ignoring_soft_deletion.where(is_not_deleted: nil) }
scope :not_deleted, -> { where(is_not_deleted: true) }
scope :ignoring_soft_deletion, -> { unscope(where: :is_not_deleted) }
default_scope { not_deleted }
end
class ProductVariantPORO
attr_accessor :product_id
15.times do |i|
attr_accessor :"string_#{i}"
end
10.times do |i|
attr_accessor :"integer_#{i}"
end
10.times do |i|
attr_accessor :"bool_#{i}"
end
init = [
"# frozen_string_literal: true",
"def initialize(attributes)",
]
init << "@product_id = attributes[:product_id]"
15.times do |i|
init << "@string_#{i} = attributes[:string_#{i}] || ''"
end
10.times do |i|
init << "@integer_#{i} = attributes[:integer_#{i}] || ''"
end
10.times do |i|
init << "@bool_#{i} = attributes[:bool_#{i}] || ''"
end
init << "end"
class_eval(init.join("\n"))
end
attributes = {
product_id: 42,
}
15.times do |i|
attributes[:"string_#{i}"] = "#{i}" * 20
end
10.times do |i|
attributes[:"integer_#{i}"] = i
end
10.times do |i|
attributes[:"integer_#{i}"] = false
end
# Benchmark.ips do |x|
# x.report("PORO") do
# 2_000.times { ProductVariantPORO.new(attributes) }
# end
#
# x.report("ActiveRecord") do
# 2_000.times { ProductVariant.new(attributes) }
# end
#
# x.compare!(order: :baseline)
# end
template = ProductVariant.new
require "vernier"
Vernier.profile(out: "ar_new_profile.json") do
200_000.times { template.dup.assign_attributes(attributes) }
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment