1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activemodel/test/cases/attribute_set_test.rb
Ryuta Kamizono 8f2bb58ba2
PERF: Recover marshaling dump/load performance (#31827)
* PERF: Recover marshaling dump/load performance

This performance regression which is described in #30680 was caused by
f0ddf87 due to force materialized `LazyAttributeHash`.

Since 95b86e5, default proc has been removed in the class, so it is no
longer needed that force materialized.

Avoiding force materialized will recover marshaling dump/load
performance.

Benchmark:

https://gist.github.com/blimmer/1360ea51cd3147bae8aeb7c6d09bff17

Before:

```
it took 0.6248569069430232 seconds to unmarshal the objects

Total allocated: 38681544 bytes (530060 objects)

allocated memory by class
-----------------------------------
  12138848  Hash
  10542384  String
   7920000  ActiveModel::Attribute::Uninitialized
   5600000  ActiveModel::Attribute::FromDatabase
   1200000  Foo
    880000  ActiveModel::LazyAttributeHash
    400000  ActiveModel::AttributeSet
        80  Integer
        72  ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
        40  ActiveModel::Type::String
        40  ActiveRecord::Type::DateTime
        40  Object
        40  Range

allocated objects by class
-----------------------------------
    250052  String
    110000  ActiveModel::Attribute::Uninitialized
     70001  Hash
     70000  ActiveModel::Attribute::FromDatabase
     10000  ActiveModel::AttributeSet
     10000  ActiveModel::LazyAttributeHash
     10000  Foo
         2  Integer
         1  ActiveModel::Type::String
         1  ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
         1  ActiveRecord::Type::DateTime
         1  Object
         1  Range
```

After:

```
it took 0.1660824950085953 seconds to unmarshal the objects

Total allocated: 13883811 bytes (220090 objects)

allocated memory by class
-----------------------------------
   5743371  String
   4940008  Hash
   1200000  Foo
    880000  ActiveModel::LazyAttributeHash
    720000  Array
    400000  ActiveModel::AttributeSet
        80  ActiveModel::Attribute::FromDatabase
        80  Integer
        72  ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
        40  ActiveModel::Type::String
        40  ActiveModel::Type::Value
        40  ActiveRecord::Type::DateTime
        40  Object
        40  Range

allocated objects by class
-----------------------------------
    130077  String
     50004  Hash
     10000  ActiveModel::AttributeSet
     10000  ActiveModel::LazyAttributeHash
     10000  Array
     10000  Foo
         2  Integer
         1  ActiveModel::Attribute::FromDatabase
         1  ActiveModel::Type::String
         1  ActiveModel::Type::Value
         1  ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer
         1  ActiveRecord::Type::DateTime
         1  Object
         1  Range
```

Fixes #30680.

* Keep the `@delegate_hash` to avoid to lose any mutations that have been made to the record
2018-02-02 07:52:33 +09:00

274 lines
9.3 KiB
Ruby

# frozen_string_literal: true
require "cases/helper"
require "active_model/attribute_set"
module ActiveModel
class AttributeSetTest < ActiveModel::TestCase
test "building a new set from raw attributes" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
assert_equal 1, attributes[:foo].value
assert_equal 2.2, attributes[:bar].value
assert_equal :foo, attributes[:foo].name
assert_equal :bar, attributes[:bar].name
end
test "building with custom types" do
builder = AttributeSet::Builder.new(foo: Type::Float.new)
attributes = builder.build_from_database({ foo: "3.3", bar: "4.4" }, { bar: Type::Integer.new })
assert_equal 3.3, attributes[:foo].value
assert_equal 4, attributes[:bar].value
end
test "[] returns a null object" do
builder = AttributeSet::Builder.new(foo: Type::Float.new)
attributes = builder.build_from_database(foo: "3.3")
assert_equal "3.3", attributes[:foo].value_before_type_cast
assert_nil attributes[:bar].value_before_type_cast
assert_equal :bar, attributes[:bar].name
end
test "duping creates a new hash, but does not dup the attributes" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
attributes = builder.build_from_database(foo: 1, bar: "foo")
# Ensure the type cast value is cached
attributes[:foo].value
attributes[:bar].value
duped = attributes.dup
duped.write_from_database(:foo, 2)
duped[:bar].value << "bar"
assert_equal 1, attributes[:foo].value
assert_equal 2, duped[:foo].value
assert_equal "foobar", attributes[:bar].value
assert_equal "foobar", duped[:bar].value
end
test "deep_duping creates a new hash and dups each attribute" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
attributes = builder.build_from_database(foo: 1, bar: "foo")
# Ensure the type cast value is cached
attributes[:foo].value
attributes[:bar].value
duped = attributes.deep_dup
duped.write_from_database(:foo, 2)
duped[:bar].value << "bar"
assert_equal 1, attributes[:foo].value
assert_equal 2, duped[:foo].value
assert_equal "foo", attributes[:bar].value
assert_equal "foobar", duped[:bar].value
end
test "freezing cloned set does not freeze original" do
attributes = AttributeSet.new({})
clone = attributes.clone
clone.freeze
assert_predicate clone, :frozen?
assert_not_predicate attributes, :frozen?
end
test "to_hash returns a hash of the type cast values" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
end
test "to_hash maintains order" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
attributes = builder.build_from_database(foo: "2.2", bar: "3.3")
attributes[:bar]
hash = attributes.to_h
assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a
end
test "values_before_type_cast" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
assert_equal({ foo: "1.1", bar: "2.2" }, attributes.values_before_type_cast)
end
test "known columns are built with uninitialized attributes" do
attributes = attributes_with_uninitialized_key
assert_predicate attributes[:foo], :initialized?
assert_not_predicate attributes[:bar], :initialized?
end
test "uninitialized attributes are not included in the attributes hash" do
attributes = attributes_with_uninitialized_key
assert_equal({ foo: 1 }, attributes.to_hash)
end
test "uninitialized attributes are not included in keys" do
attributes = attributes_with_uninitialized_key
assert_equal [:foo], attributes.keys
end
test "uninitialized attributes return false for key?" do
attributes = attributes_with_uninitialized_key
assert attributes.key?(:foo)
assert_not attributes.key?(:bar)
end
test "unknown attributes return false for key?" do
attributes = attributes_with_uninitialized_key
assert_not attributes.key?(:wibble)
end
test "fetch_value returns the value for the given initialized attribute" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
assert_equal 1, attributes.fetch_value(:foo)
assert_equal 2.2, attributes.fetch_value(:bar)
end
test "fetch_value returns nil for unknown attributes" do
attributes = attributes_with_uninitialized_key
assert_nil attributes.fetch_value(:wibble) { "hello" }
end
test "fetch_value returns nil for unknown attributes when types has a default" do
types = Hash.new(Type::Value.new)
builder = AttributeSet::Builder.new(types)
attributes = builder.build_from_database
assert_nil attributes.fetch_value(:wibble) { "hello" }
end
test "fetch_value uses the given block for uninitialized attributes" do
attributes = attributes_with_uninitialized_key
value = attributes.fetch_value(:bar) { |n| n.to_s + "!" }
assert_equal "bar!", value
end
test "fetch_value returns nil for uninitialized attributes if no block is given" do
attributes = attributes_with_uninitialized_key
assert_nil attributes.fetch_value(:bar)
end
test "the primary_key is always initialized" do
defaults = { foo: Attribute.from_user(:foo, nil, nil) }
builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, defaults)
attributes = builder.build_from_database
assert attributes.key?(:foo)
assert_equal [:foo], attributes.keys
assert_predicate attributes[:foo], :initialized?
end
class MyType
def cast(value)
return if value.nil?
value + " from user"
end
def deserialize(value)
return if value.nil?
value + " from database"
end
def assert_valid_value(*)
end
end
test "write_from_database sets the attribute with database typecasting" do
builder = AttributeSet::Builder.new(foo: MyType.new)
attributes = builder.build_from_database
assert_nil attributes.fetch_value(:foo)
attributes.write_from_database(:foo, "value")
assert_equal "value from database", attributes.fetch_value(:foo)
end
test "write_from_user sets the attribute with user typecasting" do
builder = AttributeSet::Builder.new(foo: MyType.new)
attributes = builder.build_from_database
assert_nil attributes.fetch_value(:foo)
attributes.write_from_user(:foo, "value")
assert_equal "value from user", attributes.fetch_value(:foo)
end
test "freezing doesn't prevent the set from materializing" do
builder = AttributeSet::Builder.new(foo: Type::String.new)
attributes = builder.build_from_database(foo: "1")
attributes.freeze
assert_equal({ foo: "1" }, attributes.to_hash)
end
test "marshaling dump/load legacy materialized attribute hash" do
builder = AttributeSet::Builder.new(foo: Type::String.new)
attributes = builder.build_from_database(foo: "1")
attributes.instance_variable_get(:@attributes).instance_eval do
class << self
def marshal_dump
materialize
end
end
end
attributes = Marshal.load(Marshal.dump(attributes))
assert_equal({ foo: "1" }, attributes.to_hash)
end
test "#accessed_attributes returns only attributes which have been read" do
builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new)
attributes = builder.build_from_database(foo: "1", bar: "2")
assert_equal [], attributes.accessed
attributes.fetch_value(:foo)
assert_equal [:foo], attributes.accessed
end
test "#map returns a new attribute set with the changes applied" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
attributes = builder.build_from_database(foo: "1", bar: "2")
new_attributes = attributes.map do |attr|
attr.with_cast_value(attr.value + 1)
end
assert_equal 2, new_attributes.fetch_value(:foo)
assert_equal 3, new_attributes.fetch_value(:bar)
end
test "comparison for equality is correctly implemented" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
attributes = builder.build_from_database(foo: "1", bar: "2")
attributes2 = builder.build_from_database(foo: "1", bar: "2")
attributes3 = builder.build_from_database(foo: "2", bar: "2")
assert_equal attributes, attributes2
assert_not_equal attributes2, attributes3
end
private
def attributes_with_uninitialized_key
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
builder.build_from_database(foo: "1.1")
end
end
end