Fix slow performance with compiling HAML templates
In Rails 5, including ActionView::Context
can have a
significant and hidden performance penalty because this module also
includes ActionView::CompiledTemplates
. This means that any module
that includes ActionView::Context
becomes a descendant of
CompiledTemplates
.
When a partial is rendered for the first time, it runs
ActionView::CompiledTemplates#module_eval
, which will evaluate a string that defines a new
method for that partial. For example, the source of the partial might
be this string:
def _app_views_profiles_show_html_haml___12345(local_assigns, output_buffer)
"hello world"
end
When this string is evaluated, the Ruby interpreter will define the
method and clear the global method cache for all descendants of
ActionView::CompiledTemplates
. Previous to this change, we
inadvertently made a number of modules fall into this category:
- GroupChildEntity
- NoteUserEntity
- Notify
- MergeRequestUserEntity
- AnalyticsCommitEntity
- CommitEntity
- UserEntity
- Kaminari::Helpers::Paginator
- CurrentUserEntity
- ActionView::Base
- ActionDispatch::DebugExceptions::DebugView
- MarkupHelper
- MergeRequestPresenter
After this change:
- Kaminari::Helpers::Paginator
- ActionView::Base
- ActionDispatch::DebugExceptions::DebugView
The list was generated via:
ObjectSpace.each_object(Class).select { |klass| klass < ActionView::CompiledTemplates }
Each time a partial is rendered for the first time, all methods for those modules will have to be redefined. This can exact a significant performance penalty.
How bad is this penalty? Using the following DTrace script, we can attach to a running Rails console process and run the benchmark script:
DTrace script
*:::*-entry
{
k = copyinstr(arg0);
m = copyinstr(arg1);
}
tick-5000hz
/k != 0/
{
@[k, m] = count();
}
Benchmark script
Benchmark.bm do |x|
x.report do
1000.times do
ActionView::CompiledTemplates.module_eval("def testme\nend")
end
end
end
$ sudo dtrace -q -s /tmp/sample.d -p <PID of rails console>
# In Rails console, run:
load '/tmp/bm.rb'
This revealed a 11x jump in the amount of time spent in core#define_method
alone.
Rails 6 fixes this behavior by moving the include CompiledTemplates
into ActionView::Base
so that including ActionView::Context
doesn't
quietly affect other modules in this way.