Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building a Better OpenStruct (RubyConf 2016)

7b5a451ee25044b9c869e3e98b79425d?s=47 Ariel Caplan
November 10, 2016

Building a Better OpenStruct (RubyConf 2016)

OpenStruct, part of Ruby's standard library, is prized for its beautiful API. It provides dynamic data objects with automatically generated getters and setters. Unfortunately, OpenStruct also carries a hefty performance penalty.

Luckily, Rubyists have recently improved OpenStruct performance and provided some alternatives. We'll study their approaches, learning to take advantage of the tools in our ecosystem while advancing the state our community.

Sometimes, we can have our cake and eat it too. But it takes creativity, hard work, and willingness to question why things are the way they are.

7b5a451ee25044b9c869e3e98b79425d?s=128

Ariel Caplan

November 10, 2016
Tweet

More Decks by Ariel Caplan

Other Decks in Technology

Transcript

  1. OPENSTRUCT BUILDING A BETTER ARIEL CAPLAN

  2. OPENSTRUCT LET’S TALK ABOUT

  3. OPENSTRUCT IS RUBY’S JAVASCRIPT OBJECT require 'ostruct' open_struct = OpenStruct.new(foo:

    :bar) open_struct.baz = 4 open_struct[:something] = 'whatever'
  4. OPENSTRUCT IS RUBY’S JAVASCRIPT OBJECT require 'ostruct' open_struct = OpenStruct.new(foo:

    :bar) open_struct.baz = 4 open_struct[:something] = 'whatever' open_struct[:foo] => :bar open_struct["baz"] => 4 open_struct[:quux] => nil open_struct.foo => :bar open_struct.baz => 4 open_struct.quux => nil
  5. WHY USE OPENSTRUCT? 3 common use cases for OpenStruct (Erik

    Michaels-Ober's list): • Consume an API • Configuration object • Simple test double
  6. CONSUME AN API hash = JSON.parse(APICall.execute) results = OpenStruct.new(hash) class

    APICall < OpenStruct def self.execute response = Net::HTTP.get(URI('domain.com/endpoint')) new(JSON.parse(response)) end end results = APICall.execute SIMPLE STRATEGY: PASS RESPONSE INTO AN OPENSTRUCT ADVANCED STRATEGY: SUBCLASS OPENSTRUCT
  7. CONSUME AN API { "thing": [1, 2, 3], "other_thing": "hello"

    } If the API response is… results.thing => [1, 2, 3] You can write this instead! Instead of… results['thing'] => [1, 2, 3]
  8. CONSUME AN API You can also define your own methods!

    class User < OpenStruct def name "#{first_name} #{last_name}" end end api_call = JSON.parse(API.get('/users/1')) # => {"first_name"=>"Ariel","last_name"=>"Caplan"} User.new(api_call).name # => "Ariel Caplan"
  9. CONFIGURATION OBJECT class MyGem def self.configure yield configuration end def

    self.configuration @configuration ||= OpenStruct.new end end MyGem.configure do |configuration| configuration.setting = true end
  10. TEST DOUBLE class Order def initialize(payment_gateway, products) @payment_gateway = payment_gateway

    @products = products end def pay payment_gateway.total = total_cost payment_gateway.charge end def total_cost # sum up the products end end
  11. TEST DOUBLE class Order def initialize(payment_gateway, products) @payment_gateway = payment_gateway

    @products = products end def pay payment_gateway.total = total_cost payment_gateway.charge end def total_cost # sum up the products end end Expensive to test!
  12. TEST DOUBLE # Stub out methods payment_gateway = OpenStruct.new(charge: :PAID)

    # Run the test payment_result = Order.new(payment_gateway, products).pay # Assert a value is returned assert_equal payment_result, :PAID # Assert a value is set on the OpenStruct assert_equal payment_gateway.total, total_cost(products)
  13. HOW DOES IT WORK?

  14. WARNING! CODE HAS BEEN BRUTALLY EDITED

  15. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class.
  16. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. foo = Object.new def foo.bar "hello from bar" end foo.bar => "hello from bar" foo.singleton_methods => [:bar]
  17. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. OpenStruct.new(foo: :bar)
  18. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end OpenStruct.new(foo: :bar)
  19. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end OpenStruct.new(foo: :bar) open_struct.baz = 4
  20. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end OpenStruct.new(foo: :bar) open_struct.baz = 4 open_struct.send(:baz=, 4)
  21. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end def method_missing(mid, *args) mname = mid.id2name if mname.chomp!('=') @table[new_ostruct_member(mname)] = args[0] else @table[mid] end end OpenStruct.new(foo: :bar) open_struct.baz = 4 open_struct.send(:baz=, 4)
  22. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end def method_missing(mid, *args) mname = mid.id2name if mname.chomp!('=') @table[new_ostruct_member(mname)] = args[0] else @table[mid] end end OpenStruct.new(foo: :bar) open_struct.baz = 4 open_struct.send(:baz=, 4)
  23. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v new_ostruct_member(k) end end end def method_missing(mid, *args) mname = mid.id2name if mname.chomp!('=') @table[new_ostruct_member(mname)] = args[0] else @table[mid] end end OpenStruct.new(foo: :bar) open_struct.baz = 4 open_struct.baz open_struct.send(:baz=, 4)
  24. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def new_ostruct_member(name) name = name.to_sym unless respond_to?(name) define_singleton_method(name) { @table[name] } define_singleton_method("#{name}=") { |x| @table[name] = x } end name end
  25. HOW DOES IT WORK? • Under the hood, OpenStruct defines

    attribute setter/getter methods on the object’s singleton class. def new_ostruct_member(name) name = name.to_sym unless respond_to?(name) define_singleton_method(name) { @table[name] } define_singleton_method("#{name}=") { |x| @table[name] = x } end name end This is the singleton class version of: attr_reader :foo attr_writer :foo but it uses the internal @table, not instance variables!
  26. • No 2 OpenStructs share a set of methods •

    The methods must be defined each time! • Under the hood, OpenStruct defines attribute setter/getter methods on the object’s singleton class. HOW DOES IT WORK?
  27. • Q: How slow is OpenStruct? • A: OPENSTRUCT IS

    SLOW !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! • Translation: 10-40x slower than an explicitly defined class • Q: Why is OpenStruct slow? • Defining methods is slow • method_missing is slow • In < 2.1, reset the global method cache, no longer a problem
  28. METHOD LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE

    CAT KERNEL
  29. METHOD LOOKUP IN RUBY cat = Cat.new cat.to_s DOG OBJECT

    ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL
  30. METHOD LOOKUP IN RUBY cat = Cat.new cat.to_s DOG OBJECT

    ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL #TO_S Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  31. class Cat def to_s "I'm a cat!" end end METHOD

    LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  32. class Cat def to_s "I'm a cat!" end end METHOD

    LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  33. METHOD LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE

    CAT KERNEL Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  34. METHOD LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE

    CAT KERNEL OpenStruct.new(foo: :bar) Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  35. METHOD LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE

    CAT KERNEL OpenStruct.new(foo: :bar) Method Location Cat#to_s Kernel GLOBAL METHOD CACHE
  36. METHOD LOOKUP IN RUBY DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE

    CAT KERNEL OpenStruct.new(foo: :bar) Method Location Cat#to_s Kernel GLOBAL METHOD CACHE Looking up methods can be really expensive!
  37. METHOD LOOKUP IN RUBY Cat.ancestors => [Cat (call 'Cat.connection' to

    establish a connection), Cat::GeneratedAssociationMethods, #<#<Class:0x007fd72c534de0>: 0x007fd72c534e58>, ApplicationRecord(abstract), ApplicationRecord::GeneratedAssoc
 iationMethods, #<#<Class:0x007fd72c6e5a18>: 0x007fd72c6e5ae0>, ActiveRecord::Base, GlobalID::Identification, ActiveRecord::Suppressor, ActiveRecord::SecureToken, ActiveRecord::Store, ActiveRecord::Serialization, ActiveModel::Serializers::JSON, ActiveModel::Serialization, ActiveRecord::Reflection, ActiveRecord::TouchLater, ActiveRecord::NoTouching, ActiveRecord::Transactions, ActiveRecord::Aggregations, ActiveRecord::NestedAttributes, ActiveRecord::AutosaveAssociation, ActiveModel::SecurePassword, ActiveRecord::Associations, ActiveRecord::Timestamp, ActiveModel::Validations::Callbacks, ActiveRecord::Callbacks, ActiveRecord::AttributeMethods::Serializ ation, ActiveRecord::AttributeMethods::Dirty, ActiveModel::Dirty, ActiveRecord::AttributeMethods::TimeZone Conversion, ActiveRecord::AttributeMethods::Primary
 Key, ActiveRecord::AttributeMethods::Query, ActiveRecord::AttributeMethods::Before
 TypeCast, ActiveRecord::AttributeMethods::Write, ActiveRecord::AttributeMethods::Read, ActiveRecord::Base::GeneratedAssociation Methods, #<#<Class:0x007fd72d296ee8>: 0x007fd72d296f60>, ActiveRecord::AttributeMethods, ActiveModel::AttributeMethods, ActiveRecord::Locking::Pessimistic, ActiveRecord::Locking::Optimistic, ActiveRecord::AttributeDecorators, ActiveRecord::Attributes, ActiveRecord::CounterCache, ActiveRecord::Validations, ActiveModel::Validations::HelperMethods, ActiveSupport::Callbacks, ActiveModel::Validations, ActiveRecord::Integration, ActiveModel::Conversion, ActiveRecord::AttributeAssignment, ActiveModel::AttributeAssignment, ActiveModel::ForbiddenAttributesPro
 tection, ActiveRecord::Sanitization, ActiveRecord::Scoping::Named, ActiveRecord::Scoping::Default, ActiveRecord::Scoping, ActiveRecord::Inheritance, ActiveRecord::ModelSchema, ActiveRecord::ReadonlyAttributes, ActiveRecord::Persistence, ActiveRecord::Core, ActiveSupport::ToJsonWithActiveSup
 portEncoder, Object, ActiveSupport::Dependencies::Load
 able, PP::ObjectMixin, JSON::Ext::Generator::Generator
 Methods::Object, ActiveSupport::Tryable, Kernel, BasicObject]
  38. METHOD LOOKUP IN RUBY • James Golick found that 10%

    of Rails CPU time was rebuilding the method cache… • So he implemented a better solution: A hierarchical method cache! • It was included in Ruby 2.1
  39. Method Location #to_s Kernel ANIMAL METHOD CACHE HIERARCHICAL METHOD CACHE

    DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL Method Location #to_s Kernel CAT METHOD CACHE
  40. Method Location #to_s Kernel ANIMAL METHOD CACHE HIERARCHICAL METHOD CACHE

    DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL Method Location #to_s Kernel CAT METHOD CACHE
  41. Method Location #to_s Kernel ANIMAL METHOD CACHE HIERARCHICAL METHOD CACHE

    DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT KERNEL Method Location #to_s Kernel CAT METHOD CACHE
  42. HIERARCHICAL METHOD CACHE DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT

    KERNEL Method Location #to_s Kernel CAT METHOD CACHE Method Location #to_s Kernel DOG METHOD CACHE
  43. HIERARCHICAL METHOD CACHE DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT

    KERNEL Method Location #to_s Kernel CAT METHOD CACHE Method Location #to_s Kernel DOG METHOD CACHE
  44. HIERARCHICAL METHOD CACHE DOG OBJECT ANIMAL BASIC
 OBJECT MEOWABLE CAT

    KERNEL Method Location #to_s Kernel CAT METHOD CACHE Method Location #to_s Kernel DOG METHOD CACHE "
  45. http://jamesgolick.com/2013/4/14/
 mris-method-caches.html

  46. IN RUBY >= 2.1, WE CAN CREATE OPENSTRUCTS WITHOUT SLOWING

    DOWN THE REST OF OUR APPLICATION!
  47. BUT OPENSTRUCT ITSELF IS STILL SLOW… #

  48. HI EVERYONE! MY NAME IS ARIEL CAPLAN. YOU CAN FIND

    ME ONLINE AT @AMCAPLAN AND AMCAPLAN.NINJA
  49. I WORK FOR WE PAY PEOPLE TO USE HIGH-QUALITY, LOW-COST

    HEALTHCARE.
  50. WE USE A SYSTEM OF MICROSERVICES AND EXTERNAL APIS COMMUNICATING

    VIA JSON PARSED INTO RUBY HASHES FED INTO OPENSTRUCTS
  51. PROFILING
 INDICATED THAT OPENSTRUCT WAS A MAJOR CULPRIT

  52. In our app: gem 'stackprof' StackProf.run(mode: :cpu, out: 'tmp/stackprof-cpu-myapp.dump') do

    # The block we wanted to test end Then, in the terminal: $ stackprof tmp/stackprof-cpu-myapp.dump --text
  53. ================================== Mode: cpu(1000) Samples: 2020 (3.72% miss rate) GC: 413

    (20.45%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 264 (13.1%) 264 (13.1%) OpenStruct#new_ostruct_member 158 (7.8%) 158 (7.8%) Nokogiri::XML::Document#decorate 62 (3.1%) 62 (3.1%) Nokogiri::XML::Node#content= 52 (2.6%) 51 (2.5%) Nokogiri::XML::Node#write_to 48 (2.4%) 48 (2.4%) Nokogiri::XML::Node#[]= 62 (3.1%) 48 (2.4%) RSolr::Xml::Document#add_field 47 (2.3%) 47 (2.3%) Time.zone_offset 45 (2.2%) 45 (2.2%) block in OpenStruct#new_ostruct_member 43 (2.1%) 43 (2.1%) block in Sunspot::Setup#get_inheritable_hash 41 (2.0%) 41 (2.0%) OpenStruct#method_missing
  54. ================================== Mode: cpu(1000) Samples: 2020 (3.72% miss rate) GC: 413

    (20.45%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 264 (13.1%) 264 (13.1%) OpenStruct#new_ostruct_member 158 (7.8%) 158 (7.8%) Nokogiri::XML::Document#decorate 62 (3.1%) 62 (3.1%) Nokogiri::XML::Node#content= 52 (2.6%) 51 (2.5%) Nokogiri::XML::Node#write_to 48 (2.4%) 48 (2.4%) Nokogiri::XML::Node#[]= 62 (3.1%) 48 (2.4%) RSolr::Xml::Document#add_field 47 (2.3%) 47 (2.3%) Time.zone_offset 45 (2.2%) 45 (2.2%) block in OpenStruct#new_ostruct_member 43 (2.1%) 43 (2.1%) block in Sunspot::Setup#get_inheritable_hash 41 (2.0%) 41 (2.0%) OpenStruct#method_missing
  55. CONGRATULATIONS, NOW YOU KNOW HOW TO PROFILE!

  56. WE WANTED TO KEEP OPENSTRUCT BECAUSE IT GAVE US FLEXIBILITY

  57. CAN WE BUILD A BETTER OPENSTRUCT?

  58. NO. #

  59. THANK YOU! ARIEL CAPLAN @AMCAPLAN

  60. None
  61. YES.

  62. 4 (MORE) STORIES

  63. ROUND 1: OpenFastStruct

  64. ” “ — Ruby Weekly, March 26, 2015

  65. ” “ — https://github.com/arturoherrero/ofstruct

  66. — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb OpenFastStruct.new(baz: 4)

  67. — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args)

    end OpenFastStruct.new(baz: 4)
  68. def update(args) args.each { |k, v| assign(k, v) } end

    — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end OpenFastStruct.new(baz: 4)
  69. def assign(key, value) @members[key.to_sym] = value end def update(args) args.each

    { |k, v| assign(k, v) } end — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end OpenFastStruct.new(baz: 4)
  70. def assign(key, value) @members[key.to_sym] = value end def update(args) args.each

    { |k, v| assign(k, v) } end — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end def method_missing(name, *args) @members.fetch(name) do if name[-1] == "=" assign(name[0..-2], args.first) else assign(key, self.class.new) end end end OpenFastStruct.new(baz: 4)
  71. def assign(key, value) @members[key.to_sym] = value end def update(args) args.each

    { |k, v| assign(k, v) } end — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end def method_missing(name, *args) @members.fetch(name) do if name[-1] == "=" assign(name[0..-2], args.first) else assign(key, self.class.new) end end end open_fast_struct.baz # baz exists OpenFastStruct.new(baz: 4)
  72. def assign(key, value) @members[key.to_sym] = value end def update(args) args.each

    { |k, v| assign(k, v) } end — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end def method_missing(name, *args) @members.fetch(name) do if name[-1] == "=" assign(name[0..-2], args.first) else assign(key, self.class.new) end end end open_fast_struct.baz = 4 open_fast_struct.baz # baz exists OpenFastStruct.new(baz: 4)
  73. def assign(key, value) @members[key.to_sym] = value end def update(args) args.each

    { |k, v| assign(k, v) } end — https://github.com/arturoherrero/ofstruct/blob/master/lib/ofstruct.rb def initialize(args = {}) @members = {} update(args) end def method_missing(name, *args) @members.fetch(name) do if name[-1] == "=" assign(name[0..-2], args.first) else assign(key, self.class.new) end end end open_fast_struct.baz = 4 open_fast_struct.baz # baz does not exist open_fast_struct.baz # baz exists OpenFastStruct.new(baz: 4)
  74. OPENFASTSTRUCT DOESN’T PAY THE UPFRONT COST OF DEFINING METHODS

  75. OPENFASTSTRUCT IS GOOD WHEN PROPERTIES ARE NOT ACCESSED REPEATEDLY

  76. OPENFASTSTRUCT DOESN’T WORK IN OUR APP DUE TO DIFFERENT BEHAVIOR

  77. ” “ — https://github.com/arturoherrero/ofstruct

  78. ” “ — https://github.com/arturoherrero/ofstruct

  79. OpenStruct.new.foo => nil OpenFastStruct.new.foo => #<OpenFastStruct>

  80. OpenStruct.new.foo => nil OpenFastStruct.new.foo => #<OpenFastStruct> if open_struct_response.foo “ falsey

    “ truthy if open_fast_struct_response.foo
  81. A DROP-IN REPLACEMENT SHOULD BE A DROP-IN REPLACEMENT

  82. CAN WE DO BETTER?

  83. ROUND 2: PersistentOpenStruct

  84. SAMPLE USAGE PersistentOpenStruct class Animal < PersistentOpenStruct def speak "The

    #{type} makes a #{sound} sound!" end end dog = Animal.new(type: 'dog', sound: 'woof') # => #<Animal type="dog", sound="woof"> dog.speak # => "The dog makes a woof sound!" dog.ears = 'droopy' dog[:nose] = ['cold', 'wet'] dog['tail'] = 'waggable' dog # => #<Animal type="dog", sound="woof", ears="droopy", nose=["cold", "wet"], tail="waggable"> Animal.instance_methods(false) # => [:speak, :type=, :type, :sound=, :sound, :ears=, :ears,
 :nose=, :nose, :tail=, :tail]
  85. HOW IT WORKS PersistentOpenStruct def new_ostruct_member(name) name = name.to_sym unless

    respond_to?(name) self.class.send(:define_method, name) { @table[name] } self.class.send(:define_method, "#{name}=") { |x| @table[name] = x } end name end def new_ostruct_member(name) name = name.to_sym unless respond_to?(name) define_singleton_method(name) { @table[name] } define_singleton_method("#{name}=") { |x| @table[name] = x } end name end OpenStruct PersistentOpenStruct def new_ostruct_member(name) name = name.to_sym unless respond_to?(name) define_singleton_method(name) { @table[name] } define_singleton_method("#{name}=") { |x| @table[name] = x } end name end def new_ostruct_member(name) name = name.to_sym unless respond_to?(name) self.class.send(:define_method, name) { @table[name] } self.class.send(:define_method, "#{name}=") { |x| @table[name] = x } end name end
  86. THIS IS A TERRIBLE HACK. IT MAY HAVE SECURITY IMPLICATIONS.

    AND IT MADE OUR APP 10% FASTER.
  87. Comparison: RegularClass: 3673779.6 i/s PersistentOpenStruct: 764359.4 i/s - 4.81x slower

    OpenFastStruct: 546808.8 i/s - 6.72x slower OpenStruct: 155130.1 i/s - 23.68x slower
  88. WHERE’S THE MATH?

  89. THE INFORMATION YOU CRAVE • Let n = number of

    methods to define • Let o = objects to create • Using PersistentOpenStruct: O(n) • Using OpenStruct: O(no) USING THE NOTATION YOU LOVE!
  90. MATH PROVIDES A USEFUL FRAMEWORK FOR THINKING ABOUT PROBLEMS. AS

    ENGINEERS, WE NEED ANSWERS GROUNDED IN REALITY.
  91. BENCHMARK YOUR LIBRARY BENCHMARK YOUR APP BENCHMARK ALL THE THINGS!

  92. HOW DO I BENCHMARK?

  93. $ gem install benchmark-ips Then, in a Ruby program: require

    ‘benchmark/ips' Benchmark.ips do |x| x.report('old code') do #run old code end x.report('new code') do # run new code end x.compare! end
  94. Warming up -------------------------------------- old code 48.250k i/100ms new code 37.676k

    i/100ms Calculating ------------------------------------- old code 561.458k (± 8.0%) i/s - 2.799M in 5.019311s new code 437.953k (± 4.8%) i/s - 2.185M in 5.001528s Comparison: old code: 561458.3 i/s new code: 437953.4 i/s - 1.28x slower
  95. CONGRATULATIONS, NOW YOU KNOW HOW TO BENCHMARK!

  96. PERSISTENTOPENSTRUCT IS GOOD WHEN REPEATEDLY CREATING DATA OBJECTS WITH THE

    SAME SHAPE
  97. ROUND 3: OpenStruct

  98. None
  99. None
  100. INTRODUCED IN RUBY 2.3!

  101. THIS IS A DRAMATIC IMPROVEMENT WHEN SOME KEYS ARE NEVER

    ACCESSED
  102. ROUND 4: DynamicClass

  103. SAMPLE USAGE DynamicClass Animal = DynamicClass.new do def speak "The

    #{type} makes a #{sound} sound!" end end dog = Animal.new(type: 'dog', sound: 'woof') # => #<Animal:0x007fdb2b818ba8 @type="dog", @sound="woof"> dog.speak # => "The dog makes a woof sound!" dog.ears = 'droopy' dog[:nose] = ['cold', 'wet'] dog['tail'] = 'waggable' dog # => #<Animal:0x007fc26b1841d0 @type="dog", @sound="woof", @ears="droopy", @nose=["cold", "wet"], @tail=“waggable"> Animal.instance_methods(false) # => [:to_h, :speak, :type=, :type, :sound=, :sound, :ears=, :ears,
 :nose=, :nose, :tail=, :tail]
  104. HOW IT WORKS DynamicClass class << self def attributes @attributes

    ||= Set.new end def add_methods!(key) class_exec do attr_writer key unless method_defined?("#{key}=") attr_reader key unless method_defined?("#{key}") attributes << key end end end
  105. HOW IT WORKS DynamicClass Animal.new(baz: 4)

  106. HOW IT WORKS DynamicClass def initialize(attributes = {}) attributes.each_pair do

    |key, value| __send__(:"#{key}=", value) end end Animal.new(baz: 4)
  107. HOW IT WORKS DynamicClass def method_missing(mid, *args) if (mname =

    mid[/.*(?==\z)/m]) self[mname] = args.first else self[mid] end end def initialize(attributes = {}) attributes.each_pair do |key, value| __send__(:"#{key}=", value) end end Animal.new(baz: 4)
  108. HOW IT WORKS DynamicClass def method_missing(mid, *args) if (mname =

    mid[/.*(?==\z)/m]) self[mname] = args.first else self[mid] end end def initialize(attributes = {}) attributes.each_pair do |key, value| __send__(:"#{key}=", value) end end animal.baz = 4 Animal.new(baz: 4)
  109. HOW IT WORKS DynamicClass def method_missing(mid, *args) if (mname =

    mid[/.*(?==\z)/m]) self[mname] = args.first else self[mid] end end def initialize(attributes = {}) attributes.each_pair do |key, value| __send__(:"#{key}=", value) end end animal.baz = 4 animal.baz Animal.new(baz: 4)
  110. HOW IT WORKS DynamicClass def method_missing(mid, *args) if (mname =

    mid[/.*(?==\z)/m]) self[mname] = args.first else self[mid] end end def []=(key, value) key = key.to_sym instance_variable_set(:"@#{key}", value) unless self.class.attributes.include?(key) self.class.add_methods!(key) end end def [](key) instance_variable_get(:"@#{key}") end def initialize(attributes = {}) attributes.each_pair do |key, value| __send__(:"#{key}=", value) end end animal.baz = 4 animal.baz Animal.new(baz: 4)
  111. HOW IT WORKS DynamicClass def method_missing(mid, *args) if (mname =

    mid[/.*(?==\z)/m]) self[mname] = args.first else self[mid] end end def []=(key, value) key = key.to_sym instance_variable_set(:"@#{key}", value) unless self.class.attributes.include?(key) self.class.add_methods!(key) end end def [](key) instance_variable_get(:"@#{key}") end def initialize(attributes = {}) attributes.each_pair do |key, value| __send__(:"#{key}=", value) end end animal.baz = 4 animal.baz Instead of an internal hash @table, we use instance variables just as you would in a standard class! Animal.new(baz: 4)
  112. IS IT FAST? Comparison: RegularClass: 3757567.8 i/s DynamicClass: 1250634.2 i/s

    - 3.00x slower PersistentOpenStruct: 766792.7 i/s - 4.90x slower OpenFastStruct: 525565.1 i/s - 7.15x slower OpenStruct: 147361.4 i/s - 25.50x slower
  113. DYNAMICCLASS IS GOOD FOR THE SAME PURPOSES AS PERSISTENTOPENSTRUCT BUT

    IT’S FASTER
  114. DYNAMICCLASS WORKS INTERNALLY THE WAY THAT PERSISTENTOPENSTRUCT REALLY SHOULD

  115. PERSISTENTOPENSTRUCT MADE ME HAPPY. DYNAMICCLASS
 MADE ME HAPPIER. THAT’S THE

    BOTTOM LINE.
  116. ROUNDUP: WHAT HAVE WE LEARNED?

  117. 1. EVEN THE STANDARD LIBRARY MAY HAVE ROOM FOR IMPROVEMENT!

  118. 2. OPTIMIZE FOR YOUR USE CASE

  119. 3. BENCHMARK, BENCHMARK, BENCHMARK!

  120. 4. EXPERIMENTS ARE AWESOME

  121. 5. YOU HAVE SOMETHING TO OFFER!

  122. 1 PERSON CAN INVENT A GREAT TOOL. IT TAKES A

    COMMUNITY TO ADAPT IT FOR THE COMMUNITY.
  123. THANK YOU! ARIEL CAPLAN @AMCAPLAN AMCAPLAN.NINJA