Skip to content

Instantly share code, notes, and snippets.

@jlecour
Last active January 2, 2016 18:19
Show Gist options
  • Save jlecour/9dd8022c542c9d353ed7 to your computer and use it in GitHub Desktop.
Save jlecour/9dd8022c542c9d353ed7 to your computer and use it in GitHub Desktop.
With Virtus (1.0.1) I get very poor performance when using coercion.

Here is a real-world example with various classes :

  • CoercedWithVirtus : it has attributes with various coercions
  • CustomCoercedWithVirtus : minimum coercions
  • RawWithVirtus : it has the same attributes but no coercions
  • CoercedWithoutVirtus : same attributes but manually mapped and coerced
  • RawWithoutVirtus : same attributes, manually mapped, but no coercions
  • IndifferentCoercedWithoutVirtus : same as RawWithoutVirtus but with indifferent access to the attribute hash
require "virtus"
require "benchmark"
require 'active_support/core_ext/hash/indifferent_access'
require "rake"
require "pp"
class GeoLocation
include Virtus.value_object
attribute :lat, BigDecimal
attribute :lng, BigDecimal
end
class RawGeoLocation
attr_reader :lat, :lng
def initialize(lat: lat, lng: lng)
@lat = BigDecimal(lat, 12)
@lng = BigDecimal(lng, 12)
end
end
class CoercedWithVirtus
include Virtus.model
attribute :hotel_id, String
attribute :name, String
attribute :address_line1, String
attribute :address_line2, String
attribute :zip, String
attribute :city, String
attribute :country_code, String
attribute :lat_lng, GeoLocation
attribute :destinations, Array[Integer]
attribute :amenities, String
attribute :badges, Array
attribute :price_min, Float
attribute :review_count, Integer
attribute :review_score, Float
attribute :rating, String
attribute :cover_picture, String
attribute :pictures_count, Integer
attribute :nb_pictures, Integer
attribute :selling_partners, Array
attribute :chains, Array
attribute :kinds, Array
attribute :hh_score, Float
attribute :fb_likes, Integer
attribute :distance, Float
end
class CustomCoercedWithVirtus
include Virtus.model
attribute :hotel_id
attribute :name
attribute :address_line1
attribute :address_line2
attribute :zip
attribute :city
attribute :country_code
attribute :lat_lng, RawGeoLocation
attribute :destinations, Array[Integer]
attribute :amenities
attribute :badges
attribute :price_min, Float
attribute :review_count, Integer
attribute :review_score, Float
attribute :rating
attribute :cover_picture
attribute :pictures_count, Integer
attribute :nb_pictures, Integer
attribute :selling_partner
attribute :chains
attribute :kinds
attribute :hh_score, Float
attribute :fb_likes, Integer
attribute :distance, Float
end
class RawWithVirtus
include Virtus.model(:coerce => false)
attribute :hotel_id
attribute :name
attribute :address_line1
attribute :address_line2
attribute :zip
attribute :city
attribute :country_code
attribute :lat_lng
attribute :destinations
attribute :amenities
attribute :badges
attribute :price_min
attribute :review_count
attribute :review_score
attribute :rating
attribute :cover_picture
attribute :pictures_count
attribute :nb_pictures
attribute :selling_partners
attribute :chains
attribute :kinds
attribute :hh_score
attribute :fb_likes
attribute :distance
end
class CoercedWithoutVirtus
attr_accessor :hotel_id, :name, :address_line1, :address_line2,
:zip, :city, :country_code, :lat_lng, :destinations,
:amenities, :badges, :price_min, :review_count, :review_score,
:rating, :cover_picture, :pictures_count, :nb_pictures,
:selling_partners, :chains, :kinds, :hh_score, :fb_likes, :distance
def initialize(attr = {})
@hotel_id = attr[:hotel_id] # String
@name = attr[:name] # String
@address_line1 = attr[:address_line1] # String
@address_line2 = attr[:address_line2] # String
@zip = attr[:zip] # String
@city = attr[:city] # String
@country_code = attr[:country_code] # String
@lat_lng = GeoLocation.new(attr[:lat_lng]) # GeoLocation
@destinations = Array(attr[:destinations]).map{|d| Integer(d)} # Array[Integer]
@amenities = Array(attr[:amenities]) # String
@badges = Array(attr[:badges]) # Array
@price_min = Float(attr[:price_min]) if attr[:price_min] # Float
@review_count = Integer(attr[:review_count]) if attr[:review_count] # Integer
@review_score = Float(attr[:review_score]) if attr[:review_score] # Float
@rating = attr[:rating] # String
@cover_picture = attr[:cover_picture] # String
@pictures_count = Integer(attr[:pictures_count]) if attr[:pictures_count] # Integer
@nb_pictures = Integer(attr[:nb_pictures]) if attr[:nb_pictures] # Integer
@selling_partners = Array(attr[:selling_partners]) # Array
@chains = Array(attr[:chains]) # Array
@kinds = Array(attr[:kinds]) # Array
@hh_score = Float(attr[:hh_score]) if attr[:hh_score] # Float
@fb_likes = Integer(attr[:fb_likes]) if # Integer
@distance = Float(attr[:distance]) if attr[:distance] # Float
end
end
class IndifferentCoercedWithoutVirtus
attr_reader :attributes
def initialize(attr = {})
attributes = ActiveSupport::HashWithIndifferentAccess.new(attr)
@attributes = {}
@attributes[:hotel_id] = attributes[:hotel_id] # String
@attributes[:name] = attributes[:name] # String
@attributes[:address_line1] = attributes[:address_line1] # String
@attributes[:address_line2] = attributes[:address_line2] # String
@attributes[:zip] = attributes[:zip] # String
@attributes[:city] = attributes[:city] # String
@attributes[:country_code] = attributes[:country_code] # String
@attributes[:lat_lng] = RawGeoLocation.new(lat: attributes[:lat_lng][:lat], lng: attributes[:lat_lng][:lng]) # GeoLocation
@attributes[:destinations] = Array(attributes[:destinations]).map{|d| Integer(d)} # Array[Integer]
@attributes[:amenities] = Array(attributes[:amenities]) # String
@attributes[:badges] = Array(attributes[:badges]) # Array
@attributes[:price_min] = Float(attributes[:price_min]) if attributes[:price_min] # Float
@attributes[:review_count] = Integer(attributes[:review_count]) if attributes[:review_count] # Integer
@attributes[:review_score] = Float(attributes[:review_score]) if attributes[:review_score] # Float
@attributes[:rating] = attributes[:rating] # String
@attributes[:cover_picture] = attributes[:cover_picture] # String
@attributes[:pictures_count] = Integer(attributes[:pictures_count]) if attributes[:pictures_count] # Integer
@attributes[:nb_pictures] = Integer(attributes[:nb_pictures]) if attributes[:nb_pictures] # Integer
@attributes[:selling_partners] = Array(attributes[:selling_partners]) # Array
@attributes[:chains] = Array(attributes[:chains]) # Array
@attributes[:kinds] = Array(attributes[:kinds]) # Array
@attributes[:hh_score] = Float(attributes[:hh_score]) if attributes[:hh_score] # Float
@attributes[:fb_likes] = Integer(attributes[:fb_likes]) if attributes[:fb_likes] # Integer
@attributes[:distance] = Float(attributes[:distance]) if attributes[:distance] # Float
end
def method_missing(method, *args, &block)
attributes.fetch(method.to_sym) {
super
}
end
def respond_to_missing?(method, *)
attributes.has_key?(method.to_sym) || super
end
end
class CuratorStyle
attr_accessor :hotel_id, :name, :address_line1, :address_line2,
:zip, :city, :country_code, :lat_lng, :destinations,
:amenities, :badges, :price_min, :review_count, :review_score,
:rating, :cover_picture, :pictures_count, :nb_pictures,
:selling_partners, :chains, :kinds, :hh_score, :fb_likes, :distance
def initialize(args = {})
method_strings = methods.map(&:to_s)
args.each do |attribute, value|
send("#{attribute}=", value) if method_strings.include?("#{attribute}=")
# instance_variable_set("@#{attribute}", value) if method_strings.include?(attribute.to_s)
end
end
def lat_lng=(value)
@lat_lng = RawGeoLocation.new(lat: value["lat"], lng: value["lng"])
end
def lat_lng
@lat_lng
end
def destinations=(value)
@destinations = Array(value).map{|d| Integer(d)}
end
def amenities=(value)
@amenities = Array(value)
end
def badges=(value)
@badges = Array(value)
end
def price_min=(value)
@price_min = Float(value) if value
end
def review_count=(value)
@review_count = Integer(value) if value
end
def review_score=(value)
@review_score = Float(value) if value
end
def pictures_count=(value)
@pictures_count = Integer(value) if value
end
def nb_pictures=(value)
@nb_pictures = Integer(value) if value
end
def selling_partners=(value)
@selling_partners = Array(value)
end
def chains=(value)
@chains = Array(value)
end
def kinds=(value)
@kinds = Array(value)
end
def hh_score=(value)
@hh_score = Float(value) if value
end
def fb_likes=(value)
@fb_likes = Integer(value) if value
end
def distance=(value)
@distance = Float(value) if value
end
end
class RawWithoutVirtus
attr_accessor :hotel_id, :name, :address_line1, :address_line2,
:zip, :city, :country_code, :lat_lng, :destinations,
:amenities, :badges, :price_min, :review_count, :review_score,
:rating, :cover_picture, :pictures_count, :nb_pictures,
:selling_partners, :chains, :kinds, :hh_score, :fb_likes, :distance
def initialize(attr = {})
@hotel_id = attr[:hotel_id]
@name = attr[:name]
@address_line1 = attr[:address_line1]
@address_line2 = attr[:address_line2]
@zip = attr[:zip]
@city = attr[:city]
@country_code = attr[:country_code]
@lat_lng = attr[:lat_lng]
@destinations = attr[:destinations]
@amenities = attr[:amenities]
@badges = attr[:badges]
@price_min = attr[:price_min]
@review_count = attr[:review_count]
@review_score = attr[:review_score]
@rating = attr[:rating]
@cover_picture = attr[:cover_picture]
@pictures_count = attr[:pictures_count]
@nb_pictures = attr[:nb_pictures]
@selling_partners = attr[:selling_partners]
@chains = attr[:chains]
@kinds = attr[:kinds]
@hh_score = attr[:hh_score]
@fb_likes = attr[:fb_likes]
@distance = attr[:distance]
end
end
task :env do
puts Time.now, RUBY_DESCRIPTION
puts
end
task :init => :env do
@init_hash = {
:hotel_id => "123456",
:name => "name",
:address_line1 => "address_line1",
:address_line2 => "address_line2",
:zip => "zip",
:city => "city",
:country_code => "FR",
:lat_lng => {"lat" => "42.123456", "lng" => "5.123456"},
:destinations => %w(1 2 3 4 5 6),
:amenities => %w(toto tata titi),
:badges => %w(toto tata titi),
:price_min => "50",
:review_count => "195",
:review_score => "8.78",
:rating => "3",
:cover_picture => "cover_picture",
:pictures_count => "12",
:nb_pictures => "13",
:selling_partners => %w(foo bar baz),
:chains => ["Hilton", "Carlton"],
:kinds => %w(FOO BAR),
:hh_score => "115",
:fb_likes => "45678",
:distance => "1.456",
}
end
task :verify => :init do
pp object = CuratorStyle.new(@init_hash)
# object = IndifferentCoercedWithoutVirtus.new(@init_hash)
# pp begin
# object.method("example")
# rescue NameError => ex
# ex
# end
# pp object.method("hotel_id")
# pp object.hotel_id
end
task :benchmark_virtus => :init do
n = 1_000
Benchmark.bmbm do |x|
x.report("coerced_with_virtus") { n.times { CoercedWithVirtus.new(@init_hash) } }
x.report("custom_coerced_with_virtus") { n.times { CustomCoercedWithVirtus.new(@init_hash) } }
x.report("raw_with_virtus") { n.times { RawWithVirtus.new(@init_hash) } }
x.report("indifferent_coerced_without_virtus") { n.times { IndifferentCoercedWithoutVirtus.new(@init_hash) } }
x.report("coerced_without_virtus") { n.times { CoercedWithoutVirtus.new(@init_hash) } }
x.report("raw_without_virtus") { n.times { RawWithoutVirtus.new(@init_hash) } }
x.report("curator_style") { n.times { CuratorStyle.new(@init_hash) } }
end
end
# Rake::Task['benchmark_virtus'].invoke
Rake::Task['verify'].invoke
2014-02-03 12:20:24 +0100
ruby 2.1.0p0 (2013-12-25 revision 44422) [x86_64-darwin12.0]
Rehearsal ----------------------------------------------------------------------
coerced_with_virtus                  1.240000   0.010000   1.250000 (  1.248169)
custom_coerced_with_virtus           0.990000   0.000000   0.990000 (  0.994736)
raw_with_virtus                      0.540000   0.000000   0.540000 (  0.540986)
indifferent_coerced_without_virtus   0.110000   0.000000   0.110000 (  0.109522)
coerced_without_virtus               0.260000   0.000000   0.260000 (  0.269170)
raw_without_virtus                   0.010000   0.000000   0.010000 (  0.005352)
curator_style                        0.170000   0.000000   0.170000 (  0.174293)
------------------------------------------------------------- total: 3.330000sec
                                         user     system      total        real
coerced_with_virtus                  1.180000   0.010000   1.190000 (  1.185185)
custom_coerced_with_virtus           0.850000   0.000000   0.850000 (  0.850132)
raw_with_virtus                      0.450000   0.000000   0.450000 (  0.452137)
indifferent_coerced_without_virtus   0.090000   0.000000   0.090000 (  0.087469)
coerced_without_virtus               0.220000   0.000000   0.220000 (  0.215905)
raw_without_virtus                   0.000000   0.000000   0.000000 (  0.002892)
curator_style                        0.140000   0.000000   0.140000 (  0.139850)
require "virtus"
require "benchmark"
require 'active_support/core_ext/hash/indifferent_access'
require "rake"
require "pp"
class GeoLocation
include Virtus.value_object
attribute :lat, BigDecimal
attribute :lng, BigDecimal
end
class RawGeoLocation
attr_reader :lat, :lng
def initialize(lat: lat, lng: lng)
@lat = BigDecimal(lat, 12)
@lng = BigDecimal(lng, 12)
end
end
class CoercedWithVirtus
include Virtus.model
attribute :hotel_id, String
attribute :name, String
attribute :address_line1, String
attribute :address_line2, String
attribute :zip, String
attribute :city, String
attribute :country_code, String
attribute :lat_lng, GeoLocation
attribute :destinations, Array[Integer]
attribute :amenities, String
attribute :badges, Array
attribute :price_min, Float
attribute :review_count, Integer
attribute :review_score, Float
attribute :rating, String
attribute :cover_picture, String
attribute :pictures_count, Integer
attribute :nb_pictures, Integer
attribute :selling_partners, Array
attribute :chains, Array
attribute :kinds, Array
attribute :hh_score, Float
attribute :fb_likes, Integer
attribute :distance, Float
end
class CoercedWithoutVirtus
attr_accessor :hotel_id, :name, :address_line1, :address_line2,
:zip, :city, :country_code, :lat_lng, :destinations,
:amenities, :badges, :price_min, :review_count, :review_score,
:rating, :cover_picture, :pictures_count, :nb_pictures,
:selling_partners, :chains, :kinds, :hh_score, :fb_likes, :distance
def initialize(attr = {})
attr = ActiveSupport::HashWithIndifferentAccess.new(attr)
@hotel_id = attr[:hotel_id] # String
@name = attr[:name] # String
@address_line1 = attr[:address_line1] # String
@address_line2 = attr[:address_line2] # String
@zip = attr[:zip] # String
@city = attr[:city] # String
@country_code = attr[:country_code] # String
@lat_lng = RawGeoLocation.new(lat: attr[:lat_lng][:lat], lng: attr[:lat_lng][:lng]) # GeoLocation
@destinations = Array(attr[:destinations]).map{|d| Integer(d)} # Array[Integer]
@amenities = Array(attr[:amenities]) # String
@badges = Array(attr[:badges]) # Array
@price_min = Float(attr[:price_min]) if attr[:price_min] # Float
@review_count = Integer(attr[:review_count]) if attr[:review_count] # Integer
@review_score = Float(attr[:review_score]) if attr[:review_score] # Float
@rating = attr[:rating] # String
@cover_picture = attr[:cover_picture] # String
@pictures_count = Integer(attr[:pictures_count]) if attr[:pictures_count] # Integer
@nb_pictures = Integer(attr[:nb_pictures]) if attr[:nb_pictures] # Integer
@selling_partners = Array(attr[:selling_partners]) # Array
@chains = Array(attr[:chains]) # Array
@kinds = Array(attr[:kinds]) # Array
@hh_score = Float(attr[:hh_score]) if attr[:hh_score] # Float
@fb_likes = Integer(attr[:fb_likes]) if # Integer
@distance = Float(attr[:distance]) if attr[:distance] # Float
end
end
task :env do
puts Time.now, RUBY_DESCRIPTION
puts
end
task :init => :env do
@init_hash = {
:hotel_id => "123456",
:name => "name",
:address_line1 => "address_line1",
:address_line2 => "address_line2",
:zip => "zip",
:city => "city",
:country_code => "FR",
:lat_lng => {"lat" => "42.123456", "lng" => "5.123456"},
:destinations => %w(1 2 3 4 5 6),
:amenities => %w(toto tata titi),
:badges => %w(toto tata titi),
:price_min => "50",
:review_count => "195",
:review_score => "8.78",
:rating => "3",
:cover_picture => "cover_picture",
:pictures_count => "12",
:nb_pictures => "13",
:selling_partners => %w(foo bar baz),
:chains => ["Hilton", "Carlton"],
:kinds => %w(FOO BAR),
:hh_score => "115",
:fb_likes => "45678",
:distance => "1.456",
}
end
task :verify => :init do
with = CoercedWithVirtus.new(@init_hash)
without = CoercedWithoutVirtus.new(@init_hash)
@init_hash.each do |k, v|
if k == :lat_lng
raise "#{k} different : #{with.send(k).lat} <> #{without.send(k).lat}" unless with.send(k).lat == without.send(k).lat
else
raise "#{k} different : #{with.send(k)} <> #{without.send(k)}" unless with.send(k) == without.send(k)
end
end
end
task :benchmark_virtus => :init do
n = 1_000
Benchmark.bmbm do |x|
x.report("coerced_with_virtus") { n.times { CoercedWithVirtus.new(@init_hash) } }
x.report("coerced_without_virtus") { n.times { CoercedWithoutVirtus.new(@init_hash) } }
end
end
Rake::Task['verify'].invoke
Rake::Task['benchmark_virtus'].invoke
2014-02-03 15:03:06 +0100
ruby 2.1.0p0 (2013-12-25 revision 44422) [x86_64-darwin12.0]
Rehearsal ----------------------------------------------------------
coerced_with_virtus      1.150000   0.000000   1.150000 (  1.161851)
coerced_without_virtus   0.090000   0.000000   0.090000 (  0.086930)
------------------------------------------------- total: 1.240000sec
                             user     system      total        real
coerced_with_virtus      1.100000   0.010000   1.110000 (  1.109354)
coerced_without_virtus   0.070000   0.000000   0.070000 (  0.073982)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment