Skip to content

Commit 9edf922

Browse files
Eliminate allocations on Model.respond_to? calls
We're trying to make instantiating models cheaper (for example doing `Post.new`), and discovered in the course of profiling that `respond_to?` is responsible for some amount of initialization allocations. This patch changes `Model.respond_to?` to not allocate anymore which should help initialization performance (as well as other queries). Here is the benchmark we used: ```ruby require "active_record" ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Schema.define do create_table :posts, force: true do |t| t.string :title, default: "hello" t.text :body, default: "i am a blog post" t.string :author, default: "aaron" t.boolean :published, default: true t.integer :likes, default: 1000000 end end class Post < ActiveRecord::Base end 5.times { Post.new } def m x = GC.stat(:total_allocated_objects) yield GC.stat(:total_allocated_objects) - x end allocs = m { 5000.times { Post.new } } p ALLOCATIONS_PER_MODEL: (allocs / 5000) allocs = m { 5000.times { Post.respond_to?(:default_scope) } } p ALLOCATIONS_PER_RESPOND_TO: (allocs / 5000) ``` Before this patch: ``` $ bundle exec ruby -Iactiverecord/lib:~/git/vernier/lib test.rb -- create_table(:posts, {force: true}) -> 0.0045s {ALLOCATIONS_PER_MODEL: 9} {ALLOCATIONS_PER_RESPOND_TO: 2} ``` After this patch: ``` $ bundle exec ruby -Iactiverecord/lib:~/git/vernier/lib test.rb -- create_table(:posts, {force: true}) -> 0.0045s {ALLOCATIONS_PER_MODEL: 7} {ALLOCATIONS_PER_RESPOND_TO: 0} ``` Co-Authored-By: Eileen M. Uchitelle <[email protected]>
1 parent 25f1357 commit 9edf922

File tree

1 file changed

+54
-69
lines changed

1 file changed

+54
-69
lines changed

activerecord/lib/active_record/dynamic_matchers.rb

Lines changed: 54 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -7,114 +7,99 @@ def respond_to_missing?(name, _)
77
if self == Base
88
super
99
else
10-
match = Method.match(self, name)
11-
match && match.valid? || super
10+
super || begin
11+
match = Method.match(name)
12+
match && match.valid?(self, name)
13+
end
1214
end
1315
end
1416

1517
def method_missing(name, ...)
16-
match = Method.match(self, name)
18+
match = Method.match(name)
1719

18-
if match && match.valid?
19-
match.define
20+
if match && match.valid?(self, name)
21+
match.define(self, name)
2022
send(name, ...)
2123
else
2224
super
2325
end
2426
end
2527

2628
class Method
27-
@matchers = []
28-
2929
class << self
30-
attr_reader :matchers
31-
32-
def match(model, name)
33-
klass = matchers.find { |k| k.pattern.match?(name) }
34-
klass.new(model, name) if klass
30+
def match(name)
31+
FindBy.match?(name) || FindByBang.match?(name)
3532
end
3633

37-
def pattern
38-
@pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
34+
def valid?(model, name)
35+
attribute_names(model, name.to_s).all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
3936
end
4037

41-
def prefix
42-
raise NotImplementedError
38+
def define(model, name)
39+
model.class_eval <<-CODE, __FILE__, __LINE__ + 1
40+
def self.#{name}(#{signature(model, name)})
41+
#{body(model, name)}
42+
end
43+
CODE
4344
end
4445

45-
def suffix
46-
""
47-
end
48-
end
46+
private
47+
def make_pattern(prefix, suffix)
48+
/\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
49+
end
4950

50-
attr_reader :model, :name, :attribute_names
51+
def attribute_names(model, name)
52+
attribute_names = name.match(pattern)[1].split("_and_")
53+
attribute_names.map! { |name| model.attribute_aliases[name] || name }
54+
end
5155

52-
def initialize(model, method_name)
53-
@model = model
54-
@name = method_name.to_s
55-
@attribute_names = @name.match(self.class.pattern)[1].split("_and_")
56-
@attribute_names.map! { |name| @model.attribute_aliases[name] || name }
57-
end
56+
def body(model, method_name)
57+
"#{finder}(#{attributes_hash(model, method_name)})"
58+
end
5859

59-
def valid?
60-
attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
61-
end
60+
# The parameters in the signature may have reserved Ruby words, in order
61+
# to prevent errors, we start each param name with `_`.
62+
def signature(model, method_name)
63+
attribute_names(model, method_name.to_s).map { |name| "_#{name}" }.join(", ")
64+
end
6265

63-
def define
64-
model.class_eval <<-CODE, __FILE__, __LINE__ + 1
65-
def self.#{name}(#{signature})
66-
#{body}
66+
# Given that the parameters starts with `_`, the finder needs to use the
67+
# same parameter name.
68+
def attributes_hash(model, method_name)
69+
"{" + attribute_names(model, method_name).map { |name| ":#{name} => _#{name}" }.join(",") + "}"
6770
end
68-
CODE
6971
end
72+
end
7073

71-
private
72-
def body
73-
"#{finder}(#{attributes_hash})"
74-
end
74+
class FindBy < Method
75+
@pattern = make_pattern("find_by", "")
7576

76-
# The parameters in the signature may have reserved Ruby words, in order
77-
# to prevent errors, we start each param name with `_`.
78-
def signature
79-
attribute_names.map { |name| "_#{name}" }.join(", ")
80-
end
77+
class << self
78+
attr_reader :pattern
8179

82-
# Given that the parameters starts with `_`, the finder needs to use the
83-
# same parameter name.
84-
def attributes_hash
85-
"{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}"
80+
def match?(name)
81+
pattern.match?(name) && self
8682
end
8783

8884
def finder
89-
raise NotImplementedError
85+
"find_by"
9086
end
91-
end
92-
93-
class FindBy < Method
94-
Method.matchers << self
95-
96-
def self.prefix
97-
"find_by"
98-
end
99-
100-
def finder
101-
"find_by"
10287
end
10388
end
10489

10590
class FindByBang < Method
106-
Method.matchers << self
91+
@pattern = make_pattern("find_by", "!")
10792

108-
def self.prefix
109-
"find_by"
110-
end
93+
class << self
94+
attr_reader :pattern
11195

112-
def self.suffix
113-
"!"
114-
end
96+
def match?(name)
97+
pattern.match?(name) && self
98+
end
11599

116-
def finder
117-
"find_by!"
100+
def finder
101+
"find_by!"
102+
end
118103
end
119104
end
120105
end

0 commit comments

Comments
 (0)