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_set/builder.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

152 lines
4 KiB
Ruby

# frozen_string_literal: true
require "active_model/attribute"
module ActiveModel
class AttributeSet # :nodoc:
class Builder # :nodoc:
attr_reader :types, :default_attributes
def initialize(types, default_attributes = {})
@types = types
@default_attributes = default_attributes
end
def build_from_database(values = {}, additional_types = {})
attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
LazyAttributeSet.new(attributes)
end
end
end
class LazyAttributeSet < AttributeSet # :nodoc:
def fetch_value(name, &block)
attributes.fetch_value(name, &block)
end
end
class LazyAttributeHash # :nodoc:
delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
@types = types
@values = values
@additional_types = additional_types
@default_attributes = default_attributes
@delegate_hash = delegate_hash
@casted_values = {}
@materialized = false
end
def key?(key)
delegate_hash.key?(key) || values.key?(key) || types.key?(key)
end
def [](key)
delegate_hash[key] || assign_default_value(key)
end
def []=(key, value)
delegate_hash[key] = value
end
def fetch_value(name, &block)
if attr = delegate_hash[name]
return attr.value(&block)
end
@casted_values.fetch(name) do
value_present = true
value = values.fetch(name) { value_present = false }
if value_present
type = additional_types.fetch(name, types[name])
@casted_values[name] = type.deserialize(value)
else
attr = assign_default_value(name, value_present, value) || Attribute.null(name)
attr.value(&block)
end
end
end
def deep_dup
dup.tap do |copy|
copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
end
end
def initialize_dup(_)
@delegate_hash = Hash[delegate_hash]
super
end
def select
keys = types.keys | values.keys | delegate_hash.keys
keys.each_with_object({}) do |key, hash|
attribute = self[key]
if yield(key, attribute)
hash[key] = attribute
end
end
end
def ==(other)
if other.is_a?(LazyAttributeHash)
materialize == other.materialize
else
materialize == other
end
end
def marshal_dump
[@types, @values, @additional_types, @default_attributes, @delegate_hash]
end
def marshal_load(values)
if values.is_a?(Hash)
ActiveSupport::Deprecation.warn(<<~MSG)
Marshalling load from legacy attributes format is deprecated and will be removed in Rails 6.2.
MSG
empty_hash = {}.freeze
initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
@materialized = true
else
initialize(*values)
end
end
protected
def materialize
unless @materialized
values.each_key { |key| self[key] }
types.each_key { |key| self[key] }
unless frozen?
@materialized = true
end
end
delegate_hash
end
private
attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
def assign_default_value(
name,
value_present = true,
value = values.fetch(name) { value_present = false }
)
type = additional_types.fetch(name, types[name])
if value_present
delegate_hash[name] = Attribute.from_database(name, value, type, @casted_values[name])
elsif types.key?(name)
attr = default_attributes[name]
if attr
delegate_hash[name] = attr.dup
else
delegate_hash[name] = Attribute.uninitialized(name, type)
end
end
end
end
end