Skip to content

Commit a2fb851

Browse files
authored
Merge pull request rails#52118 from Shopify/fix-code-generator-bloat
Fix a performance regression in attribute methods
2 parents 3ec2f19 + 514d474 commit a2fb851

File tree

7 files changed

+69
-44
lines changed

7 files changed

+69
-44
lines changed

activemodel/lib/active_model/attribute_methods.rb

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,7 @@ def eagerly_generate_alias_attribute_methods(new_name, old_name) # :nodoc:
215215
end
216216

217217
def generate_alias_attribute_methods(code_generator, new_name, old_name)
218-
attribute_method_patterns.each do |pattern|
219-
alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
220-
end
218+
define_attribute_method(old_name, _owner: code_generator, as: new_name)
221219
end
222220

223221
def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc:
@@ -305,22 +303,35 @@ def define_attribute_methods(*attr_names)
305303
# person.name = 'Bob'
306304
# person.name # => "Bob"
307305
# person.name_short? # => true
308-
def define_attribute_method(attr_name, _owner: generated_attribute_methods)
306+
def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name)
309307
ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
310308
attribute_method_patterns.each do |pattern|
311-
method_name = pattern.method_name(attr_name)
309+
define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as)
310+
end
311+
attribute_method_patterns_cache.clear
312+
end
313+
end
312314

313-
unless instance_method_already_implemented?(method_name)
314-
generate_method = "define_method_#{pattern.proxy_target}"
315+
def define_attribute_method_pattern(pattern, attr_name, owner:, as:) # :nodoc:
316+
canonical_method_name = pattern.method_name(attr_name)
317+
public_method_name = pattern.method_name(as)
315318

316-
if respond_to?(generate_method, true)
317-
send(generate_method, attr_name.to_s, owner: owner)
318-
else
319-
define_proxy_call(owner, method_name, pattern.proxy_target, pattern.parameters, attr_name.to_s, namespace: :active_model_proxy)
320-
end
321-
end
319+
unless instance_method_already_implemented?(public_method_name)
320+
generate_method = "define_method_#{pattern.proxy_target}"
321+
322+
if respond_to?(generate_method, true)
323+
send(generate_method, attr_name.to_s, owner: owner, as: as)
324+
else
325+
define_proxy_call(
326+
owner,
327+
canonical_method_name,
328+
pattern.proxy_target,
329+
pattern.parameters,
330+
attr_name.to_s,
331+
namespace: :active_model_proxy,
332+
as: public_method_name
333+
)
322334
end
323-
attribute_method_patterns_cache.clear
324335
end
325336
end
326337

@@ -404,14 +415,19 @@ def attribute_method_patterns_matching(method_name)
404415
# Define a method `name` in `mod` that dispatches to `send`
405416
# using the given `extra` args. This falls back on `send`
406417
# if the called name cannot be compiled.
407-
def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:)
418+
def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name)
408419
mangled_name = build_mangled_name(name)
409420

410421
call_args.map!(&:inspect)
411422
call_args << parameters if parameters
412-
namespace = :"#{namespace}_#{proxy_target}_#{call_args.join("_")}}"
413423

414-
define_call(code_generator, name, proxy_target, mangled_name, parameters, call_args, namespace: namespace)
424+
# We have to use a different namespace for every target method, because
425+
# if someone defines an attribute that look like an attribute method we could clash, e.g.
426+
# attribute :title_was
427+
# attribute :title
428+
namespace = :"#{namespace}_#{proxy_target}"
429+
430+
define_call(code_generator, name, proxy_target, mangled_name, parameters, call_args, namespace: namespace, as: as)
415431
end
416432

417433
def build_mangled_name(name)
@@ -424,8 +440,8 @@ def build_mangled_name(name)
424440
mangled_name
425441
end
426442

427-
def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:)
428-
code_generator.define_cached_method(name, as: mangled_name, namespace: namespace) do |batch|
443+
def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:, as:)
444+
code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch|
429445
body = if CALL_COMPILABLE_REGEXP.match?(target_name)
430446
"self.#{target_name}(#{call_args.join(", ")})"
431447
else

activemodel/lib/active_model/attributes.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ def attribute_names
8989

9090
##
9191
private
92-
def define_method_attribute=(name, owner:)
92+
def define_method_attribute=(canonical_name, owner:, as: canonical_name)
9393
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
94-
owner, name, writer: true,
94+
owner, canonical_name, writer: true,
9595
) do |temp_method_name, attr_name_expr|
96-
owner.define_cached_method("#{name}=", as: temp_method_name, namespace: :active_model) do |batch|
96+
owner.define_cached_method(temp_method_name, as: "#{as}=", namespace: :active_model) do |batch|
9797
batch <<
9898
"def #{temp_method_name}(value)" <<
9999
" _write_attribute(#{attr_name_expr}, value)" <<

activerecord/lib/active_record/attribute_methods.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,21 @@ def eagerly_generate_alias_attribute_methods(_new_name, _old_name) # :nodoc:
7777
# alias attributes in Active Record are lazily generated
7878
end
7979

80+
def generate_alias_attribute_methods(code_generator, new_name, old_name) # :nodoc:
81+
attribute_method_patterns.each do |pattern|
82+
alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
83+
end
84+
attribute_method_patterns_cache.clear
85+
end
86+
8087
def alias_attribute_method_definition(code_generator, pattern, new_name, old_name)
8188
old_name = old_name.to_s
8289

8390
if !abstract_class? && !has_attribute?(old_name)
8491
raise ArgumentError, "#{self.name} model aliases `#{old_name}`, but `#{old_name}` is not an attribute. " \
8592
"Use `alias_method :#{new_name}, :#{old_name}` or define the method manually."
8693
else
87-
method_name = pattern.method_name(new_name).to_s
88-
parameters = pattern.parameters
89-
90-
define_proxy_call(code_generator, method_name, pattern.proxy_target, parameters, old_name,
91-
namespace: :proxy_alias_attribute
92-
)
94+
define_attribute_method_pattern(pattern, old_name, owner: code_generator, as: new_name)
9395
end
9496
end
9597

activerecord/lib/active_record/attribute_methods/read.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ module Read
88

99
module ClassMethods # :nodoc:
1010
private
11-
def define_method_attribute(name, owner:)
11+
def define_method_attribute(canonical_name, owner:, as: canonical_name)
1212
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
13-
owner, name
13+
owner, canonical_name
1414
) do |temp_method_name, attr_name_expr|
15-
owner.define_cached_method(name, as: temp_method_name, namespace: :active_record) do |batch|
15+
owner.define_cached_method(temp_method_name, as: as, namespace: :active_record) do |batch|
1616
batch <<
1717
"def #{temp_method_name}" <<
1818
" _read_attribute(#{attr_name_expr}) { |n| missing_attribute(n, caller) }" <<

activerecord/lib/active_record/attribute_methods/write.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ module Write
1212

1313
module ClassMethods # :nodoc:
1414
private
15-
def define_method_attribute=(name, owner:)
15+
def define_method_attribute=(canonical_name, owner:, as: canonical_name)
1616
ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
17-
owner, name, writer: true,
17+
owner, canonical_name, writer: true,
1818
) do |temp_method_name, attr_name_expr|
19-
owner.define_cached_method("#{name}=", as: temp_method_name, namespace: :active_record) do |batch|
19+
owner.define_cached_method(temp_method_name, as: "#{as}=", namespace: :active_record) do |batch|
2020
batch <<
2121
"def #{temp_method_name}(value)" <<
2222
" _write_attribute(#{attr_name_expr}, value)" <<

activerecord/test/cases/attribute_methods_test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,8 +1220,10 @@ def some_method_that_is_not_on_super
12201220
test "aliases to the same attribute name do not conflict with each other" do
12211221
first_model_object = ToBeLoadedFirst.new(author_name: "author 1")
12221222
assert_equal("author 1", first_model_object.subject)
1223+
assert_equal([nil, "author 1"], first_model_object.subject_change)
12231224
second_model_object = ToBeLoadedSecond.new(title: "foo")
12241225
assert_equal("foo", second_model_object.subject)
1226+
assert_equal([nil, "foo"], second_model_object.subject_change)
12251227
end
12261228

12271229
test "#alias_attribute with an overridden original method does not use the overridden original method" do

activesupport/lib/active_support/code_generator.rb

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,30 @@ def initialize(namespace)
99
@cache = METHOD_CACHES[namespace]
1010
@sources = []
1111
@methods = {}
12+
@canonical_methods = {}
1213
end
1314

14-
def define_cached_method(name, as: name)
15-
name = name.to_sym
16-
as = as.to_sym
17-
@methods.fetch(name) do
18-
unless @cache.method_defined?(as)
15+
def define_cached_method(canonical_name, as: nil)
16+
canonical_name = canonical_name.to_sym
17+
as = (as || canonical_name).to_sym
18+
19+
@methods.fetch(as) do
20+
unless @cache.method_defined?(canonical_name) || @canonical_methods[canonical_name]
1921
yield @sources
2022
end
21-
@methods[name] = as
23+
@canonical_methods[canonical_name] = true
24+
@methods[as] = canonical_name
2225
end
2326
end
2427

2528
def apply(owner, path, line)
2629
unless @sources.empty?
2730
@cache.module_eval("# frozen_string_literal: true\n" + @sources.join(";"), path, line)
2831
end
29-
@methods.each do |name, as|
30-
owner.define_method(name, @cache.instance_method(as))
32+
@canonical_methods.clear
33+
34+
@methods.each do |as, canonical_name|
35+
owner.define_method(as, @cache.instance_method(canonical_name))
3136
end
3237
end
3338
end
@@ -52,8 +57,8 @@ def initialize(owner, path, line)
5257
@namespaces = Hash.new { |h, k| h[k] = MethodSet.new(k) }
5358
end
5459

55-
def define_cached_method(name, namespace:, as: name, &block)
56-
@namespaces[namespace].define_cached_method(name, as: as, &block)
60+
def define_cached_method(canonical_name, namespace:, as: nil, &block)
61+
@namespaces[namespace].define_cached_method(canonical_name, as: as, &block)
5762
end
5863

5964
def execute

0 commit comments

Comments
 (0)