1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activemodel/lib/active_model/attribute.rb
Ryuta Kamizono ab8b12eaf6 PERF: 35% faster attributes for readonly usage
Instantiating attributes hash from raw database values is one of the
slower part of attributes.

Why that is necessary is to detect mutations. In other words, that isn't
necessary until mutations are happened.

`LazyAttributeHash` which was introduced at 0f29c21 is to instantiate
attribute lazily until first accessing the attribute (i.e.
`Model.find(1)` isn't slow yet, but `Model.find(1).attr_name` is still
slow).

This introduces `LazyAttributeSet` to instantiate attribute more lazily,
it doesn't instantiate attribute until first assigning/dirty checking
the attribute (i.e. `Model.find(1).attr_name` is no longer slow).

It makes attributes access about 35% faster for readonly (non-mutation)
usage.

https://gist.github.com/kamipo/4002c96a02859d8fe6503e26d7be4ad8

Before:

```
IPS
Warming up --------------------------------------
    attribute access     1.000  i/100ms
Calculating -------------------------------------
    attribute access      3.444  (± 0.0%) i/s -     18.000  in   5.259030s
MEMORY
Calculating -------------------------------------
    attribute access    38.902M memsize (     0.000  retained)
                       350.044k objects (     0.000  retained)
                        15.000  strings (     0.000  retained)
```

After (with `immutable_strings_by_default = true`):

```
IPS
Warming up --------------------------------------
    attribute access     1.000  i/100ms
Calculating -------------------------------------
    attribute access      4.652  (±21.5%) i/s -     23.000  in   5.034853s
MEMORY
Calculating -------------------------------------
    attribute access    27.782M memsize (     0.000  retained)
                       170.044k objects (     0.000  retained)
                        15.000  strings (     0.000  retained)
```
2020-06-13 16:01:08 +09:00

248 lines
5.9 KiB
Ruby

# frozen_string_literal: true
require "active_support/core_ext/object/duplicable"
module ActiveModel
class Attribute # :nodoc:
class << self
def from_database(name, value_before_type_cast, type, value = nil)
FromDatabase.new(name, value_before_type_cast, type, nil, value)
end
def from_user(name, value_before_type_cast, type, original_attribute = nil)
FromUser.new(name, value_before_type_cast, type, original_attribute)
end
def with_cast_value(name, value_before_type_cast, type)
WithCastValue.new(name, value_before_type_cast, type)
end
def null(name)
Null.new(name)
end
def uninitialized(name, type)
Uninitialized.new(name, type)
end
end
attr_reader :name, :value_before_type_cast, :type
# This method should not be called directly.
# Use #from_database or #from_user
def initialize(name, value_before_type_cast, type, original_attribute = nil, value = nil)
@name = name
@value_before_type_cast = value_before_type_cast
@type = type
@original_attribute = original_attribute
@value = value unless value.nil?
end
def value
# `defined?` is cheaper than `||=` when we get back falsy values
@value = type_cast(value_before_type_cast) unless defined?(@value)
@value
end
def original_value
if assigned?
original_attribute.original_value
else
type_cast(value_before_type_cast)
end
end
def value_for_database
type.serialize(value)
end
def changed?
changed_from_assignment? || changed_in_place?
end
def changed_in_place?
has_been_read? && type.changed_in_place?(original_value_for_database, value)
end
def forgetting_assignment
with_value_from_database(value_for_database)
end
def with_value_from_user(value)
type.assert_valid_value(value)
self.class.from_user(name, value, type, original_attribute || self)
end
def with_value_from_database(value)
self.class.from_database(name, value, type)
end
def with_cast_value(value)
self.class.with_cast_value(name, value, type)
end
def with_type(type)
if changed_in_place?
with_value_from_user(value).with_type(type)
else
self.class.new(name, value_before_type_cast, type, original_attribute)
end
end
def type_cast(*)
raise NotImplementedError
end
def initialized?
true
end
def came_from_user?
false
end
def has_been_read?
defined?(@value)
end
def ==(other)
self.class == other.class &&
name == other.name &&
value_before_type_cast == other.value_before_type_cast &&
type == other.type
end
alias eql? ==
def hash
[self.class, name, value_before_type_cast, type].hash
end
def init_with(coder)
@name = coder["name"]
@value_before_type_cast = coder["value_before_type_cast"]
@type = coder["type"]
@original_attribute = coder["original_attribute"]
@value = coder["value"] if coder.map.key?("value")
end
def encode_with(coder)
coder["name"] = name
coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil?
coder["type"] = type if type
coder["original_attribute"] = original_attribute if original_attribute
coder["value"] = value if defined?(@value)
end
def original_value_for_database
if assigned?
original_attribute.original_value_for_database
else
_original_value_for_database
end
end
private
attr_reader :original_attribute
alias :assigned? :original_attribute
def initialize_dup(other)
if defined?(@value) && @value.duplicable?
@value = @value.dup
end
end
def changed_from_assignment?
assigned? && type.changed?(original_value, value, value_before_type_cast)
end
def _original_value_for_database
type.serialize(original_value)
end
class FromDatabase < Attribute # :nodoc:
def type_cast(value)
type.deserialize(value)
end
def _original_value_for_database
value_before_type_cast
end
private :_original_value_for_database
end
class FromUser < Attribute # :nodoc:
def type_cast(value)
type.cast(value)
end
def came_from_user?
!type.value_constructed_by_mass_assignment?(value_before_type_cast)
end
end
class WithCastValue < Attribute # :nodoc:
def type_cast(value)
value
end
def changed_in_place?
false
end
end
class Null < Attribute # :nodoc:
def initialize(name)
super(name, nil, Type.default_value)
end
def type_cast(*)
nil
end
def with_type(type)
self.class.with_cast_value(name, nil, type)
end
def with_value_from_database(value)
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
end
alias_method :with_value_from_user, :with_value_from_database
alias_method :with_cast_value, :with_value_from_database
end
class Uninitialized < Attribute # :nodoc:
UNINITIALIZED_ORIGINAL_VALUE = Object.new
def initialize(name, type)
super(name, nil, type)
end
def value
if block_given?
yield name
end
end
def original_value
UNINITIALIZED_ORIGINAL_VALUE
end
def value_for_database
end
def initialized?
false
end
def forgetting_assignment
dup
end
def with_type(type)
self.class.new(name, type)
end
end
private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
end
end