Skip to content

Fix slow performance with compiling HAML templates

Stan Hu requested to merge sh-fix-slow-partial-rendering into master

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.

Closes https://gitlab.com/gitlab-org/gitlab-ee/issues/11198

Edited by Stan Hu

Merge request reports