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 0adcec4954 PERF: Avoid extra delegation to LazyAttributeHash
The extra delegation to `LazyAttributeHash` has non-negligible overhead.

Avoiding that delegation makes attributes access about 45% 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      5.066  (±19.7%) i/s -     25.000  in   5.024650s
MEMORY
Calculating -------------------------------------
    attribute access    27.382M memsize (     0.000  retained)
                       160.044k objects (     0.000  retained)
                        15.000  strings (     0.000  retained)
```
2020-06-15 09:26:24 +09:00

191 lines
5.1 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 = {})
LazyAttributeSet.new(values, types, additional_types, default_attributes)
end
end
end
class LazyAttributeSet < AttributeSet # :nodoc:
def initialize(values, types, additional_types, default_attributes, attributes = {})
super(attributes)
@values = values
@types = types
@additional_types = additional_types
@default_attributes = default_attributes
@casted_values = {}
@materialized = false
end
def key?(name)
(values.key?(name) || types.key?(name) || @attributes.key?(name)) && self[name].initialized?
end
def keys
keys = values.keys | types.keys | @attributes.keys
keys.keep_if { |name| self[name].initialized? }
end
def fetch_value(name, &block)
if attr = @attributes[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 = default_attribute(name, value_present, value)
attr.value(&block)
end
end
end
protected
def attributes
unless @materialized
values.each_key { |key| self[key] }
types.each_key { |key| self[key] }
@materialized = true
end
@attributes
end
private
attr_reader :values, :types, :additional_types, :default_attributes
def default_attribute(
name,
value_present = true,
value = values.fetch(name) { value_present = false }
)
type = additional_types.fetch(name, types[name])
if value_present
@attributes[name] = Attribute.from_database(name, value, type, @casted_values[name])
elsif types.key?(name)
if attr = default_attributes[name]
@attributes[name] = attr.dup
else
@attributes[name] = Attribute.uninitialized(name, type)
end
else
Attribute.null(name)
end
end
end
class LazyAttributeHash # :nodoc:
delegate :transform_values, :each_value, :fetch, :except, to: :materialize
def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
@types = types
@values = values
@additional_types = additional_types
@materialized = false
@delegate_hash = delegate_hash
@default_attributes = default_attributes
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 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 each_key(&block)
keys = types.keys | values.keys | delegate_hash.keys
keys.each(&block)
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)
type = additional_types.fetch(name, types[name])
value_present = true
value = values.fetch(name) { value_present = false }
if value_present
delegate_hash[name] = Attribute.from_database(name, value, type)
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