1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Avoid creating helper modules until modified

In applications which use :all helpers (the default), most controllers
won't be making modifications to their _helpers module.

In CRuby this created many ICLASS objects which could cause a large
increase in memory usage in applications with many controllers and
helpers.

To avoid creating unnecessary modules this PR builds modules only when a
modification is being made: ethier by calling `helper`, `helper_method`,
or through having a default helper (one matching the controller's name)
included onto it.
This commit is contained in:
John Hawthorn 2020-07-21 18:48:01 -07:00
parent 10df6931de
commit 758ad13c58
2 changed files with 33 additions and 7 deletions

View file

@ -7,8 +7,19 @@ module AbstractController
extend ActiveSupport::Concern
included do
class_attribute :_helpers, default: define_helpers_module(self)
class_attribute :_helper_methods, default: Array.new
# This is here so that it is always higher in the inheritance chain than
# the definition in lib/action_view/rendering.rb
redefine_singleton_method(:_helpers) do
if @_helpers ||= nil
@_helpers
else
superclass._helpers
end
end
self._helpers = define_helpers_module(self)
end
class MissingHelperError < LoadError
@ -25,17 +36,24 @@ module AbstractController
end
end
def _helpers
self.class._helpers
end
module ClassMethods
# When a class is inherited, wrap its helper module in a new module.
# This ensures that the parent class's module can be changed
# independently of the child class's.
def inherited(klass)
helpers = _helpers
klass._helpers = define_helpers_module(klass, helpers)
# Inherited from parent by default
klass._helpers = nil
klass.class_eval { default_helper_module! } unless klass.anonymous?
super
end
attr_writer :_helpers
# Declare a controller method as a helper. For example, the following
# makes the +current_user+ and +logged_in?+ controller methods available
# to the view:
@ -65,7 +83,7 @@ module AbstractController
file, line = location.path, location.lineno
methods.each do |method|
_helpers.class_eval <<-ruby_eval, file, line
_helpers_for_modification.class_eval <<-ruby_eval, file, line
def #{method}(*args, &block) # def current_user(*args, &block)
controller.send(:'#{method}', *args, &block) # controller.send(:'current_user', *args, &block)
end # end
@ -127,10 +145,11 @@ module AbstractController
#
def helper(*args, &block)
modules_for_helpers(args).each do |mod|
_helpers.include(mod)
next if _helpers.include?(mod)
_helpers_for_modification.include(mod)
end
_helpers.module_eval(&block) if block_given?
_helpers_for_modification.module_eval(&block) if block_given?
end
# Clears up all existing helpers in this class, only keeping the helper
@ -161,6 +180,13 @@ module AbstractController
end
end
def _helpers_for_modification
unless @_helpers
self._helpers = define_helpers_module(self, superclass._helpers)
end
_helpers
end
private
def define_helpers_module(klass, helpers = nil)
# In some tests inherited is called explicitly. In that case, just

View file

@ -74,7 +74,7 @@ module ActionView
def helper_method(*methods)
# Almost a duplicate from ActionController::Helpers
methods.flatten.each do |method|
_helpers.module_eval <<-end_eval, __FILE__, __LINE__ + 1
_helpers_for_modification.module_eval <<-end_eval, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def current_user(*args, &block)
_test_case.send(:'#{method}', *args, &block) # _test_case.send(:'current_user', *args, &block)
end # end