Skip to content

Commit 1962e8d

Browse files
committed
class_attribute: reduce reliance on define_method
Full benchmark: https://gist.github.com/casperisfine/539e310eadd0cb6 Followup: rails#37903 Instead of always redefining a bmethod on assignment, if the reciever already has its own bmethod, we just update the local variable. This wasn't really considered before because class attributes are mostly meant for configuration, so not really expected to be changed at runtime, but it can happen. This is 10 to 17x faster to assign a class attributes: ``` interpreter: old: 1834149.6 i/s new: 17722346.6 i/s - 9.66x faster yjit: old: 2561173.8 i/s new: 43004422.5 i/s - 16.79x faster ``` The difference is even bigger if something is redefining `method_added` events, e.g. sorbet... All other cases are unchanged in performance, except definition that is marginally faster. One downside however is that generated accessors source location will no longer point to the `class_attribute` callsite.
1 parent 32e549a commit 1962e8d

File tree

6 files changed

+79
-23
lines changed

6 files changed

+79
-23
lines changed

activerecord/test/cases/test_case.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,6 @@ def with_has_many_inversing(model = ActiveRecord::Base)
141141
yield
142142
ensure
143143
model.has_many_inversing = old
144-
if model != ActiveRecord::Base && !old
145-
model.singleton_class.remove_method(:has_many_inversing) # reset the class_attribute
146-
end
147144
end
148145

149146
def with_automatic_scope_inversing(*reflections)

activesupport/lib/active_support.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ module ActiveSupport
6262
autoload :Cache
6363
autoload :Callbacks
6464
autoload :Configurable
65+
autoload :ClassAttribute
6566
autoload :Deprecation
6667
autoload :Delegation
6768
autoload :Digest

activesupport/lib/active_support/callbacks.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ module Callbacks
6666

6767
included do
6868
extend ActiveSupport::DescendantsTracker
69-
class_attribute :__callbacks, instance_writer: false, default: {}
69+
class_attribute :__callbacks, instance_writer: false, instance_predicate: false, default: {}
7070
end
7171

7272
CALLBACK_FILTER_TYPES = [:before, :after, :around].freeze
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveSupport
4+
module ClassAttribute # :nodoc:
5+
class << self
6+
def redefine(owner, name, value)
7+
if owner.singleton_class?
8+
owner.redefine_method(name) { value }
9+
owner.send(:public, name)
10+
end
11+
12+
owner.redefine_singleton_method(name) { value }
13+
owner.singleton_class.send(:public, name)
14+
15+
owner.redefine_singleton_method("#{name}=") do |new_value|
16+
if owner.equal?(self)
17+
value = new_value
18+
else
19+
::ActiveSupport::ClassAttribute.redefine(self, name, new_value)
20+
end
21+
end
22+
owner.singleton_class.send(:public, "#{name}=")
23+
end
24+
end
25+
end
26+
end

activesupport/lib/active_support/core_ext/class/attribute.rb

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -91,24 +91,16 @@ def class_attribute(*attrs, instance_accessor: true,
9191
raise TypeError, "#{name.inspect} is not a symbol nor a string"
9292
end
9393

94-
class_methods << <<~RUBY # In case the method exists and is not public
95-
silence_redefinition_of_method def #{name}
96-
end
97-
RUBY
98-
99-
methods << <<~RUBY if instance_reader
100-
silence_redefinition_of_method def #{name}
101-
defined?(@#{name}) ? @#{name} : self.class.#{name}
102-
end
103-
RUBY
94+
name = name.to_sym
95+
::ActiveSupport::ClassAttribute.redefine(self, name, default)
10496

105-
class_methods << <<~RUBY
106-
silence_redefinition_of_method def #{name}=(value)
107-
redefine_method(:#{name}) { value } if singleton_class?
108-
redefine_singleton_method(:#{name}) { value }
109-
value
110-
end
111-
RUBY
97+
unless singleton_class?
98+
methods << <<~RUBY if instance_reader
99+
silence_redefinition_of_method def #{name}
100+
defined?(@#{name}) ? @#{name} : self.class.#{name}
101+
end
102+
RUBY
103+
end
112104

113105
methods << <<~RUBY if instance_writer
114106
silence_redefinition_of_method(:#{name}=)
@@ -125,7 +117,5 @@ def class_attribute(*attrs, instance_accessor: true,
125117

126118
location = caller_locations(1, 1).first
127119
class_eval(["class << self", *class_methods, "end", *methods].join(";").tr("\n", ";"), location.path, location.lineno)
128-
129-
attrs.each { |name| public_send("#{name}=", default) }
130120
end
131121
end

activesupport/test/core_ext/class/attribute_test.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ def setup
88
@klass = Class.new do
99
class_attribute :setting
1010
class_attribute :timeout, default: 5
11+
class_attribute :system # private kernel method
1112
end
1213

1314
@sub = Class.new(@klass)
@@ -30,11 +31,14 @@ def setup
3031
test "overridable" do
3132
@sub.setting = 1
3233
assert_nil @klass.setting
34+
assert_equal 1, @sub.setting
3335

3436
@klass.setting = 2
37+
assert_equal 2, @klass.setting
3538
assert_equal 1, @sub.setting
3639

3740
assert_equal 1, Class.new(@sub).setting
41+
assert_equal 2, Class.new(@klass).setting
3842
end
3943

4044
test "predicate method" do
@@ -97,8 +101,34 @@ def setup
97101

98102
test "works well with singleton classes" do
99103
object = @klass.new
104+
100105
object.singleton_class.setting = "foo"
101106
assert_equal "foo", object.setting
107+
assert_nil @klass.setting
108+
109+
object.singleton_class.setting = "bar"
110+
assert_equal "bar", object.setting
111+
assert_nil @klass.setting
112+
113+
@klass.setting = "plop"
114+
assert_equal "bar", object.setting
115+
assert_equal "plop", @klass.setting
116+
end
117+
118+
test "when defined in a class's singleton" do
119+
@klass = Class.new do
120+
class << self
121+
class_attribute :__callbacks, default: 1
122+
end
123+
end
124+
125+
assert_equal 1, @klass.__callbacks
126+
assert_equal 1, @klass.singleton_class.__callbacks
127+
128+
# I honestly think this is a bug, but that's how it used to behave
129+
@klass.__callbacks = 4
130+
assert_equal 1, @klass.__callbacks
131+
assert_equal 1, @klass.singleton_class.__callbacks
102132
end
103133

104134
test "works well with module singleton classes" do
@@ -115,4 +145,16 @@ class << self
115145
val = @klass.public_send(:setting=, 1)
116146
assert_equal 1, val
117147
end
148+
149+
test "works when overriding private methods from an ancestor" do
150+
assert_nil @klass.system
151+
@klass.system = 1
152+
assert_equal 1, @klass.system
153+
154+
instance = @klass.new
155+
assert_equal 1, instance.system
156+
assert_predicate @klass.new, :system?
157+
instance.system = 2
158+
assert_equal 2, instance.system
159+
end
118160
end

0 commit comments

Comments
 (0)