diff options
author | Ryuta Kamizono <kamipo@gmail.com> | 2019-04-09 08:28:31 +0900 |
---|---|---|
committer | Ryuta Kamizono <kamipo@gmail.com> | 2019-04-11 16:30:40 +0900 |
commit | 6b0a9de906f2f96a847b5eb93755db0911f2615d (patch) | |
tree | 572d4af77a85b9b804d97b77a0fc37691d249090 /activemodel/lib/active_model/dirty.rb | |
parent | d4e2824d8d9aea6d95e78275c0107bbbc48300d9 (diff) | |
download | rails-6b0a9de906f2f96a847b5eb93755db0911f2615d.tar.gz rails-6b0a9de906f2f96a847b5eb93755db0911f2615d.tar.bz2 rails-6b0a9de906f2f96a847b5eb93755db0911f2615d.zip |
PERF: 2x ~ 30x faster dirty tracking
Currently, although using both dirty tracking (ivar backed and
attributes backed) on one model is not supported (doesn't fully work at
least), both dirty tracking are being performed, that is very slow.
As long as attributes backed dirty tracking is used, ivar backed dirty
tracking should not need to be performed.
I've refactored to extract new `ForcedMutationTracker` which only tracks
`force_change` to be performed for ivar backed dirty tracking, that
makes dirty tracking on Active Record 2x ~ 30x faster.
https://gist.github.com/kamipo/971dfe0891f0fe1ec7db8ab31f016435
Before:
```
Warming up --------------------------------------
changed? 4.467k i/100ms
changed 5.134k i/100ms
changes 3.023k i/100ms
changed_attributes 4.358k i/100ms
title_change 3.185k i/100ms
title_was 3.381k i/100ms
Calculating -------------------------------------
changed? 42.197k (±28.5%) i/s - 187.614k in 5.050446s
changed 50.481k (±16.0%) i/s - 246.432k in 5.045759s
changes 30.799k (± 7.2%) i/s - 154.173k in 5.030765s
changed_attributes 51.530k (±14.2%) i/s - 252.764k in 5.041106s
title_change 44.667k (± 9.0%) i/s - 222.950k in 5.040646s
title_was 44.635k (±16.6%) i/s - 216.384k in 5.051098s
```
After:
```
Warming up --------------------------------------
changed? 24.130k i/100ms
changed 13.503k i/100ms
changes 6.511k i/100ms
changed_attributes 9.226k i/100ms
title_change 48.221k i/100ms
title_was 96.060k i/100ms
Calculating -------------------------------------
changed? 245.478k (±16.1%) i/s - 1.182M in 5.015837s
changed 157.641k (± 4.9%) i/s - 796.677k in 5.066734s
changes 70.633k (± 5.7%) i/s - 358.105k in 5.086553s
changed_attributes 95.155k (±13.6%) i/s - 470.526k in 5.082841s
title_change 566.481k (± 3.5%) i/s - 2.845M in 5.028852s
title_was 1.487M (± 3.9%) i/s - 7.493M in 5.046774s
```
Diffstat (limited to 'activemodel/lib/active_model/dirty.rb')
-rw-r--r-- | activemodel/lib/active_model/dirty.rb | 119 |
1 files changed, 28 insertions, 91 deletions
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 0d9e761b1e..ab2c9d04ae 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/hash_with_indifferent_access" -require "active_support/core_ext/object/duplicable" require "active_model/attribute_mutation_tracker" module ActiveModel @@ -122,9 +120,6 @@ module ActiveModel extend ActiveSupport::Concern include ActiveModel::AttributeMethods - OPTION_NOT_GIVEN = Object.new # :nodoc: - private_constant :OPTION_NOT_GIVEN - included do attribute_method_suffix "_changed?", "_change", "_will_change!", "_was" attribute_method_suffix "_previously_changed?", "_previous_change" @@ -145,10 +140,9 @@ module ActiveModel # +mutations_from_database+ to +mutations_before_last_save+ respectively. def changes_applied unless defined?(@attributes) - @previously_changed = changes + mutations_from_database.finalize_changes end @mutations_before_last_save = mutations_from_database - @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end @@ -159,7 +153,7 @@ module ActiveModel # person.name = 'bob' # person.changed? # => true def changed? - changed_attributes.present? + mutations_from_database.any_changes? end # Returns an array with the name of the attributes with unsaved changes. @@ -168,42 +162,37 @@ module ActiveModel # person.name = 'bob' # person.changed # => ["name"] def changed - changed_attributes.keys + mutations_from_database.changed_attribute_names end # Handles <tt>*_changed?</tt> for +method_missing+. - def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: - !!changes_include?(attr) && - (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) && - (from == OPTION_NOT_GIVEN || from == changed_attributes[attr]) + def attribute_changed?(attr_name, **options) # :nodoc: + mutations_from_database.changed?(attr_name.to_s, options) end # Handles <tt>*_was</tt> for +method_missing+. - def attribute_was(attr) # :nodoc: - attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) + def attribute_was(attr_name) # :nodoc: + mutations_from_database.original_value(attr_name.to_s) end # Handles <tt>*_previously_changed?</tt> for +method_missing+. - def attribute_previously_changed?(attr) #:nodoc: - previous_changes_include?(attr) + def attribute_previously_changed?(attr_name) # :nodoc: + mutations_before_last_save.changed?(attr_name.to_s) end # Restore all previous data of the provided attributes. - def restore_attributes(attributes = changed) - attributes.each { |attr| restore_attribute! attr } + def restore_attributes(attr_names = changed) + attr_names.each { |attr_name| restore_attribute!(attr_name) } end # Clears all dirty data: current changes and previous changes. def clear_changes_information - @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @mutations_before_last_save = nil - @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end def clear_attribute_changes(attr_names) - attributes_changed_by_setter.except!(*attr_names) attr_names.each do |attr_name| clear_attribute_change(attr_name) end @@ -216,13 +205,7 @@ module ActiveModel # person.name = 'robert' # person.changed_attributes # => {"name" => "bob"} def changed_attributes - # This should only be set by methods which will call changed_attributes - # multiple times when it is known that the computed value cannot change. - if defined?(@cached_changed_attributes) - @cached_changed_attributes - else - attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze - end + mutations_from_database.changed_values end # Returns a hash of changed attributes indicating their original @@ -232,9 +215,7 @@ module ActiveModel # person.name = 'bob' # person.changes # => { "name" => ["bill", "bob"] } def changes - cache_changed_attributes do - ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] - end + mutations_from_database.changes end # Returns a hash of attributes that were changed before the model was saved. @@ -244,27 +225,23 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new - @previously_changed.merge(mutations_before_last_save.changes) + mutations_before_last_save.changes end def attribute_changed_in_place?(attr_name) # :nodoc: - mutations_from_database.changed_in_place?(attr_name) + mutations_from_database.changed_in_place?(attr_name.to_s) end private def clear_attribute_change(attr_name) - mutations_from_database.forget_change(attr_name) + mutations_from_database.forget_change(attr_name.to_s) end def mutations_from_database - unless defined?(@mutations_from_database) - @mutations_from_database = nil - end @mutations_from_database ||= if defined?(@attributes) ActiveModel::AttributeMutationTracker.new(@attributes) else - NullMutationTracker.instance + ActiveModel::ForcedMutationTracker.new(self) end end @@ -276,68 +253,28 @@ module ActiveModel @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance end - def cache_changed_attributes - @cached_changed_attributes = changed_attributes - yield - ensure - clear_changed_attributes_cache - end - - def clear_changed_attributes_cache - remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes) - end - - # Returns +true+ if attr_name is changed, +false+ otherwise. - def changes_include?(attr_name) - attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name) - end - alias attribute_changed_by_setter? changes_include? - - # Returns +true+ if attr_name were changed before the model was saved, - # +false+ otherwise. - def previous_changes_include?(attr_name) - previous_changes.include?(attr_name) - end - # Handles <tt>*_change</tt> for +method_missing+. - def attribute_change(attr) - [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr) + def attribute_change(attr_name) + mutations_from_database.change_to_attribute(attr_name.to_s) end # Handles <tt>*_previous_change</tt> for +method_missing+. - def attribute_previous_change(attr) - previous_changes[attr] + def attribute_previous_change(attr_name) + mutations_before_last_save.change_to_attribute(attr_name.to_s) end # Handles <tt>*_will_change!</tt> for +method_missing+. - def attribute_will_change!(attr) - unless attribute_changed?(attr) - begin - value = _read_attribute(attr) - value = value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - end - - set_attribute_was(attr, value) - end - mutations_from_database.force_change(attr) + def attribute_will_change!(attr_name) + mutations_from_database.force_change(attr_name.to_s) end # Handles <tt>restore_*!</tt> for +method_missing+. - def restore_attribute!(attr) - if attribute_changed?(attr) - __send__("#{attr}=", changed_attributes[attr]) - clear_attribute_changes([attr]) + def restore_attribute!(attr_name) + attr_name = attr_name.to_s + if attribute_changed?(attr_name) + __send__("#{attr_name}=", attribute_was(attr_name)) + clear_attribute_change(attr_name) end end - - def attributes_changed_by_setter - @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new - end - - # Force an attribute to have a particular "before" value - def set_attribute_was(attr, old_value) - attributes_changed_by_setter[attr] = old_value - end end end |