796cab4556
Inspired by @tenderlove's work in c363fff29f060e6a2effe1e4bb2c4dd4cd805d6e, this reduces the number of strings allocated when running callbacks for ActiveRecord instances. I measured that using this script: ``` require 'objspace' require 'active_record' require 'allocation_tracer' ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:" ActiveRecord::Base.connection.instance_eval do create_table(:articles) { |t| t.string :name } end class Article < ActiveRecord::Base; end a = Article.create name: "foo" a = Article.find a.id N = 10 result = ObjectSpace::AllocationTracer.trace do N.times { Article.find a.id } end result.sort.each do |k,v| p k => v end puts "total: #{result.values.map(&:first).inject(:+)}" ``` When I run this against master and this branch I get this output: ``` pete@balloon:~/projects/rails/activerecord$ git checkout master M Gemfile Switched to branch 'master' pete@balloon:~/projects/rails/activerecord$ bundle exec ruby benchmark_allocation_with_callback_send.rb > allocations_before pete@balloon:~/projects/rails/activerecord$ git checkout remove-dynamic-send-on-built-in-callbacks M Gemfile Switched to branch 'remove-dynamic-send-on-built-in-callbacks' pete@balloon:~/projects/rails/activerecord$ bundle exec ruby benchmark_allocation_with_callback_send.rb > allocations_after pete@balloon:~/projects/rails/activerecord$ diff allocations_before allocations_after 39d38 < {["/home/pete/projects/rails/activesupport/lib/active_support/callbacks.rb", 81]=>[40, 0, 0, 0, 0, 0]} 42c41 < total: 630 --- > total: 590 ``` In addition to this, there are two micro-optimizations present: * Using `block.call if block` vs `yield if block_given?` when the block was being captured already. ``` pete@balloon:~/projects$ cat benchmark_block_call_vs_yield.rb require 'benchmark/ips' def block_capture_with_yield &block yield if block_given? end def block_capture_with_call &block block.call if block end def no_block_capture yield if block_given? end Benchmark.ips do |b| b.report("block_capture_with_yield") { block_capture_with_yield } b.report("block_capture_with_call") { block_capture_with_call } b.report("no_block_capture") { no_block_capture } end pete@balloon:~/projects$ ruby benchmark_block_call_vs_yield.rb Calculating ------------------------------------- block_capture_with_yield 124979 i/100ms block_capture_with_call 138340 i/100ms no_block_capture 136827 i/100ms ------------------------------------------------- block_capture_with_yield 5703108.9 (±2.4%) i/s - 28495212 in 4.999368s block_capture_with_call 6840730.5 (±3.6%) i/s - 34169980 in 5.002649s no_block_capture 5821141.4 (±2.8%) i/s - 29144151 in 5.010580s ``` * Defining and calling methods instead of using send. ``` pete@balloon:~/projects$ cat benchmark_method_call_vs_send.rb require 'benchmark/ips' class Foo def tacos nil end end my_foo = Foo.new Benchmark.ips do |b| b.report('send') { my_foo.send('tacos') } b.report('call') { my_foo.tacos } end pete@balloon:~/projects$ ruby benchmark_method_call_vs_send.rb Calculating ------------------------------------- send 97736 i/100ms call 151142 i/100ms ------------------------------------------------- send 2683730.3 (±2.8%) i/s - 13487568 in 5.029763s call 8005963.9 (±2.7%) i/s - 40052630 in 5.006604s ``` The result of this is making typical ActiveRecord operations slightly faster: https://gist.github.com/phiggins/e46e51dcc7edb45b5f98 |
||
---|---|---|
.. | ||
lib | ||
test | ||
activemodel.gemspec | ||
CHANGELOG.md | ||
MIT-LICENSE | ||
Rakefile | ||
README.rdoc |
= Active Model -- model interfaces for Rails Active Model provides a known set of interfaces for usage in model classes. They allow for Action Pack helpers to interact with non-Active Record models, for example. Active Model also helps with building custom ORMs for use outside of the Rails framework. Prior to Rails 3.0, if a plugin or gem developer wanted to have an object interact with Action Pack helpers, it was required to either copy chunks of code from Rails, or monkey patch entire helpers to make them handle objects that did not exactly conform to the Active Record interface. This would result in code duplication and fragile applications that broke on upgrades. Active Model solves this by defining an explicit API. You can read more about the API in <tt>ActiveModel::Lint::Tests</tt>. Active Model provides a default module that implements the basic API required to integrate with Action Pack out of the box: <tt>ActiveModel::Model</tt>. class Person include ActiveModel::Model attr_accessor :name, :age validates_presence_of :name end person = Person.new(name: 'bob', age: '18') person.name # => 'bob' person.age # => '18' person.valid? # => true It includes model name introspections, conversions, translations and validations, resulting in a class suitable to be used with Action Pack. See <tt>ActiveModel::Model</tt> for more examples. Active Model also provides the following functionality to have ORM-like behavior out of the box: * Add attribute magic to objects class Person include ActiveModel::AttributeMethods attribute_method_prefix 'clear_' define_attribute_methods :name, :age attr_accessor :name, :age def clear_attribute(attr) send("#{attr}=", nil) end end person = Person.new person.clear_name person.clear_age {Learn more}[link:classes/ActiveModel/AttributeMethods.html] * Callbacks for certain operations class Person extend ActiveModel::Callbacks define_model_callbacks :create def create run_callbacks :create do # Your create action methods here end end end This generates +before_create+, +around_create+ and +after_create+ class methods that wrap your create method. {Learn more}[link:classes/ActiveModel/Callbacks.html] * Tracking value changes class Person include ActiveModel::Dirty define_attribute_methods :name def name @name end def name=(val) name_will_change! unless val == @name @name = val end def save # do persistence work changes_applied end end person = Person.new person.name # => nil person.changed? # => false person.name = 'bob' person.changed? # => true person.changed # => ['name'] person.changes # => { 'name' => [nil, 'bob'] } person.save person.name = 'robert' person.save person.previous_changes # => {'name' => ['bob, 'robert']} {Learn more}[link:classes/ActiveModel/Dirty.html] * Adding +errors+ interface to objects Exposing error messages allows objects to interact with Action Pack helpers seamlessly. class Person def initialize @errors = ActiveModel::Errors.new(self) end attr_accessor :name attr_reader :errors def validate! errors.add(:name, "cannot be nil") if name.nil? end def self.human_attribute_name(attr, options = {}) "Name" end end person = Person.new person.name = nil person.validate! person.errors.full_messages # => ["Name cannot be nil"] {Learn more}[link:classes/ActiveModel/Errors.html] * Model name introspection class NamedPerson extend ActiveModel::Naming end NamedPerson.model_name.name # => "NamedPerson" NamedPerson.model_name.human # => "Named person" {Learn more}[link:classes/ActiveModel/Naming.html] * Making objects serializable ActiveModel::Serialization provides a standard interface for your object to provide +to_json+ or +to_xml+ serialization. class SerialPerson include ActiveModel::Serialization attr_accessor :name def attributes {'name' => name} end end s = SerialPerson.new s.serializable_hash # => {"name"=>nil} class SerialPerson include ActiveModel::Serializers::JSON end s = SerialPerson.new s.to_json # => "{\"name\":null}" class SerialPerson include ActiveModel::Serializers::Xml end s = SerialPerson.new s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... {Learn more}[link:classes/ActiveModel/Serialization.html] * Internationalization (i18n) support class Person extend ActiveModel::Translation end Person.human_attribute_name('my_attribute') # => "My attribute" {Learn more}[link:classes/ActiveModel/Translation.html] * Validation support class Person include ActiveModel::Validations attr_accessor :first_name, :last_name validates_each :first_name, :last_name do |record, attr, value| record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z end end person = Person.new person.first_name = 'zoolander' person.valid? # => false {Learn more}[link:classes/ActiveModel/Validations.html] * Custom validators class HasNameValidator < ActiveModel::Validator def validate(record) record.errors[:name] = "must exist" if record.name.blank? end end class ValidatorPerson include ActiveModel::Validations validates_with HasNameValidator attr_accessor :name end p = ValidatorPerson.new p.valid? # => false p.errors.full_messages # => ["Name must exist"] p.name = "Bob" p.valid? # => true {Learn more}[link:classes/ActiveModel/Validator.html] == Download and installation The latest version of Active Model can be installed with RubyGems: % [sudo] gem install activemodel Source code can be downloaded as part of the Rails project on GitHub * https://github.com/rails/rails/tree/master/activemodel == License Active Model is released under the MIT license: * http://www.opensource.org/licenses/MIT == Support API documentation is at * http://api.rubyonrails.org Bug reports can be filed for the Ruby on Rails project here: * https://github.com/rails/rails/issues Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core