mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
8f2bb58ba2
* PERF: Recover marshaling dump/load performance This performance regression which is described in #30680 was caused byf0ddf87
due to force materialized `LazyAttributeHash`. Since95b86e5
, 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
274 lines
9.3 KiB
Ruby
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
|