mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
6b0a9de906
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 ```
178 lines
3.8 KiB
Ruby
178 lines
3.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "active_support/core_ext/hash/indifferent_access"
|
|
require "active_support/core_ext/object/duplicable"
|
|
|
|
module ActiveModel
|
|
class AttributeMutationTracker # :nodoc:
|
|
OPTION_NOT_GIVEN = Object.new
|
|
|
|
def initialize(attributes, forced_changes = Set.new)
|
|
@attributes = attributes
|
|
@forced_changes = forced_changes
|
|
end
|
|
|
|
def changed_attribute_names
|
|
attr_names.select { |attr_name| changed?(attr_name) }
|
|
end
|
|
|
|
def changed_values
|
|
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
|
|
if changed?(attr_name)
|
|
result[attr_name] = original_value(attr_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
def changes
|
|
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
|
|
if change = change_to_attribute(attr_name)
|
|
result.merge!(attr_name => change)
|
|
end
|
|
end
|
|
end
|
|
|
|
def change_to_attribute(attr_name)
|
|
if changed?(attr_name)
|
|
[original_value(attr_name), fetch_value(attr_name)]
|
|
end
|
|
end
|
|
|
|
def any_changes?
|
|
attr_names.any? { |attr| changed?(attr) }
|
|
end
|
|
|
|
def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
|
|
attribute_changed?(attr_name) &&
|
|
(OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
|
|
(OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
|
|
end
|
|
|
|
def changed_in_place?(attr_name)
|
|
attributes[attr_name].changed_in_place?
|
|
end
|
|
|
|
def forget_change(attr_name)
|
|
attributes[attr_name] = attributes[attr_name].forgetting_assignment
|
|
forced_changes.delete(attr_name)
|
|
end
|
|
|
|
def original_value(attr_name)
|
|
attributes[attr_name].original_value
|
|
end
|
|
|
|
def force_change(attr_name)
|
|
forced_changes << attr_name
|
|
end
|
|
|
|
private
|
|
attr_reader :attributes, :forced_changes
|
|
|
|
def attr_names
|
|
attributes.keys
|
|
end
|
|
|
|
def attribute_changed?(attr_name)
|
|
forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
|
|
end
|
|
|
|
def fetch_value(attr_name)
|
|
attributes.fetch_value(attr_name)
|
|
end
|
|
end
|
|
|
|
class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
|
|
def initialize(attributes, forced_changes = {})
|
|
super
|
|
@finalized_changes = nil
|
|
end
|
|
|
|
def changed_in_place?(attr_name)
|
|
false
|
|
end
|
|
|
|
def change_to_attribute(attr_name)
|
|
if finalized_changes&.include?(attr_name)
|
|
finalized_changes[attr_name].dup
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
def forget_change(attr_name)
|
|
forced_changes.delete(attr_name)
|
|
end
|
|
|
|
def original_value(attr_name)
|
|
if changed?(attr_name)
|
|
forced_changes[attr_name]
|
|
else
|
|
fetch_value(attr_name)
|
|
end
|
|
end
|
|
|
|
def force_change(attr_name)
|
|
forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
|
|
end
|
|
|
|
def finalize_changes
|
|
@finalized_changes = changes
|
|
end
|
|
|
|
private
|
|
attr_reader :finalized_changes
|
|
|
|
def attr_names
|
|
forced_changes.keys
|
|
end
|
|
|
|
def attribute_changed?(attr_name)
|
|
forced_changes.include?(attr_name)
|
|
end
|
|
|
|
def fetch_value(attr_name)
|
|
attributes.send(:_read_attribute, attr_name)
|
|
end
|
|
|
|
def clone_value(attr_name)
|
|
value = fetch_value(attr_name)
|
|
value.duplicable? ? value.clone : value
|
|
rescue TypeError, NoMethodError
|
|
value
|
|
end
|
|
end
|
|
|
|
class NullMutationTracker # :nodoc:
|
|
include Singleton
|
|
|
|
def changed_attribute_names
|
|
[]
|
|
end
|
|
|
|
def changed_values
|
|
{}
|
|
end
|
|
|
|
def changes
|
|
{}
|
|
end
|
|
|
|
def change_to_attribute(attr_name)
|
|
end
|
|
|
|
def any_changes?
|
|
false
|
|
end
|
|
|
|
def changed?(attr_name, **)
|
|
false
|
|
end
|
|
|
|
def changed_in_place?(attr_name)
|
|
false
|
|
end
|
|
|
|
def original_value(attr_name)
|
|
end
|
|
end
|
|
end
|