Skip to content

Commit 6c5a042

Browse files
committed
Reduce memory used by ActiveSupport::Callbacks
Previously, callbacks which were shared with subclasses would not share between them the procs generated in Filters::Before and Filters::After. This was also lazily generated so would cause memory growth after an application is booted (and not sharable between workers via CoW). This was because we would rebuild the objects used to invoke the callbacks via CallbackChain#compile, so any difference in the callback chain would result in all of the callback procs being rebuilt. This commit changes before and after callbacks (but not around!) to be shared between all subclasses of where it was defined. This is done by changing Filters::Before and Filters::After to plain classes which respond to call instead of generating procs (which wasn't strictly necessary but was easier to implement, and also results in simpler objects which use less memory). These objects avoid referencing and tied to a specific callback sequence and so can be memoized and reused. This has the most impact on applications with many Controllers, and many callbacks in the ApplicationController (or similar). I also took this opportunity to merge together all the different forms of procs generated (halting, halting_and_conditional, conditional, simple) into one form with if statements. There isn't any significant performance benefit from the specialization previously being done.
1 parent 7c58911 commit 6c5a042

File tree

1 file changed

+63
-108
lines changed

1 file changed

+63
-108
lines changed

activesupport/lib/active_support/callbacks.rb

Lines changed: 63 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def run_callbacks(kind, type = nil)
150150
def halted_callback_hook(filter, name)
151151
end
152152

153-
module Conditionals # :nodoc:
153+
module Conditionals # :nodoc: all
154154
class Value
155155
def initialize(&block)
156156
@block = block
@@ -159,128 +159,76 @@ def call(target, value); @block.call(value); end
159159
end
160160
end
161161

162-
module Filters
162+
module Filters # :nodoc: all
163163
Environment = Struct.new(:target, :halted, :value)
164164

165165
class Before
166-
def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter, name)
166+
def initialize(user_callback, user_conditions, chain_config, filter, name)
167167
halted_lambda = chain_config[:terminator]
168-
169-
if user_conditions.any?
170-
halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter, name)
171-
else
172-
halting(callback_sequence, user_callback, halted_lambda, filter, name)
173-
end
168+
@user_callback, @user_conditions, @halted_lambda, @filter, @name = user_callback, user_conditions, halted_lambda, filter, name
169+
freeze
174170
end
171+
attr_reader :user_callback, :user_conditions, :halted_lambda, :filter, :name
175172

176-
def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter, name)
177-
callback_sequence.before do |env|
178-
target = env.target
179-
value = env.value
180-
halted = env.halted
173+
def call(env)
174+
target = env.target
175+
value = env.value
176+
halted = env.halted
181177

182-
if !halted && user_conditions.all? { |c| c.call(target, value) }
183-
result_lambda = -> { user_callback.call target, value }
184-
env.halted = halted_lambda.call(target, result_lambda)
185-
if env.halted
186-
target.send :halted_callback_hook, filter, name
187-
end
178+
if !halted && user_conditions.all? { |c| c.call(target, value) }
179+
result_lambda = -> { user_callback.call target, value }
180+
env.halted = halted_lambda.call(target, result_lambda)
181+
if env.halted
182+
target.send :halted_callback_hook, filter, name
188183
end
189-
190-
env
191184
end
192-
end
193-
private_class_method :halting_and_conditional
194-
195-
def self.halting(callback_sequence, user_callback, halted_lambda, filter, name)
196-
callback_sequence.before do |env|
197-
target = env.target
198-
value = env.value
199-
halted = env.halted
200185

201-
unless halted
202-
result_lambda = -> { user_callback.call target, value }
203-
env.halted = halted_lambda.call(target, result_lambda)
204-
if env.halted
205-
target.send :halted_callback_hook, filter, name
206-
end
207-
end
186+
env
187+
end
208188

209-
env
210-
end
189+
def apply(callback_sequence)
190+
callback_sequence.before(self)
211191
end
212-
private_class_method :halting
213192
end
214193

215194
class After
216-
def self.build(callback_sequence, user_callback, user_conditions, chain_config)
217-
if chain_config[:skip_after_callbacks_if_terminated]
218-
if user_conditions.any?
219-
halting_and_conditional(callback_sequence, user_callback, user_conditions)
220-
else
221-
halting(callback_sequence, user_callback)
222-
end
223-
else
224-
if user_conditions.any?
225-
conditional callback_sequence, user_callback, user_conditions
226-
else
227-
simple callback_sequence, user_callback
228-
end
229-
end
195+
attr_reader :user_callback, :user_conditions, :halting
196+
def initialize(user_callback, user_conditions, chain_config)
197+
halting = chain_config[:skip_after_callbacks_if_terminated]
198+
@user_callback, @user_conditions, @halting = user_callback, user_conditions, halting
199+
freeze
230200
end
231201

232-
def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
233-
callback_sequence.after do |env|
234-
target = env.target
235-
value = env.value
236-
halted = env.halted
237-
238-
if !halted && user_conditions.all? { |c| c.call(target, value) }
239-
user_callback.call target, value
240-
end
202+
def call(env)
203+
target = env.target
204+
value = env.value
205+
halted = env.halted
241206

242-
env
207+
if (!halted || !@halting) && user_conditions.all? { |c| c.call(target, value) }
208+
user_callback.call target, value
243209
end
244-
end
245-
private_class_method :halting_and_conditional
246-
247-
def self.halting(callback_sequence, user_callback)
248-
callback_sequence.after do |env|
249-
unless env.halted
250-
user_callback.call env.target, env.value
251-
end
252210

253-
env
254-
end
211+
env
255212
end
256-
private_class_method :halting
257-
258-
def self.conditional(callback_sequence, user_callback, user_conditions)
259-
callback_sequence.after do |env|
260-
target = env.target
261-
value = env.value
262213

263-
if user_conditions.all? { |c| c.call(target, value) }
264-
user_callback.call target, value
265-
end
266-
267-
env
268-
end
214+
def apply(callback_sequence)
215+
callback_sequence.after(self)
269216
end
270-
private_class_method :conditional
217+
end
271218

272-
def self.simple(callback_sequence, user_callback)
273-
callback_sequence.after do |env|
274-
user_callback.call env.target, env.value
219+
class Around
220+
def initialize(user_callback, user_conditions)
221+
@user_callback, @user_conditions = user_callback, user_conditions
222+
freeze
223+
end
275224

276-
env
277-
end
225+
def apply(callback_sequence)
226+
callback_sequence.around(@user_callback, @user_conditions)
278227
end
279-
private_class_method :simple
280228
end
281229
end
282230

283-
class Callback # :nodoc:#
231+
class Callback # :nodoc:
284232
def self.build(chain, filter, kind, options)
285233
if filter.is_a?(String)
286234
raise ArgumentError, <<-MSG.squish
@@ -329,19 +277,26 @@ def duplicates?(other)
329277
end
330278
end
331279

280+
def compiled
281+
@compiled ||=
282+
begin
283+
user_conditions = conditions_lambdas
284+
user_callback = CallTemplate.build(@filter, self)
285+
286+
case kind
287+
when :before
288+
Filters::Before.new(user_callback.make_lambda, user_conditions, chain_config, @filter, name)
289+
when :after
290+
Filters::After.new(user_callback.make_lambda, user_conditions, chain_config)
291+
when :around
292+
Filters::Around.new(user_callback, user_conditions)
293+
end
294+
end
295+
end
296+
332297
# Wraps code with filter
333298
def apply(callback_sequence)
334-
user_conditions = conditions_lambdas
335-
user_callback = CallTemplate.build(@filter, self)
336-
337-
case kind
338-
when :before
339-
Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter, name)
340-
when :after
341-
Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config)
342-
when :around
343-
callback_sequence.around(user_callback, user_conditions)
344-
end
299+
compiled.apply(callback_sequence)
345300
end
346301

347302
def current_scopes
@@ -375,7 +330,7 @@ def conditions_lambdas
375330

376331
# A future invocation of user-supplied code (either as a callback,
377332
# or a condition filter).
378-
module CallTemplate # :nodoc:
333+
module CallTemplate # :nodoc: all
379334
class MethodCall
380335
def initialize(method)
381336
@method_name = method
@@ -566,12 +521,12 @@ def initialize(nested = nil, call_template = nil, user_conditions = nil)
566521
@after = []
567522
end
568523

569-
def before(&before)
524+
def before(before)
570525
@before.unshift(before)
571526
self
572527
end
573528

574-
def after(&after)
529+
def after(after)
575530
@after.push(after)
576531
self
577532
end

0 commit comments

Comments
 (0)