Improve the performance of the `ApplicationRecord#underscore` method

What does this MR do and why?

Using SafeRequestStore is safe(not thread safe but using the thread current anyway so each thread will have its own value) but we shouldn't be worried about the thread safety of this method as all the calls to this method would return the exact same value, therefore, memoizing the value by using a class instance variable should be safe, and more performant(~9 times).

Benchmark

Benchmark script
GL_PATH = "YOUR_PATH_TO_GITLAB"

ENV['ENABLE_BOOTSNAP'] = '0'
ENV['BUNDLE_GEMFILE'] = "#{GL_PATH}/Gemfile"
system "BUNDLE_GEMFILE=#{ENV['BUNDLE_GEMFILE']} bundle install"

puts "Loading the Application..."
require "#{GL_PATH}/config/environment.rb"

Rails.configuration.eager_load_namespaces.each(&:eager_load!)
puts "Application is loaded."

require 'benchmark/ips'

RequestStore.begin!

Benchmark.ips do |b|
  b.report('existing underscore') do
    Ci::Pipeline.underscore
  end

  b.report('to_s underscore') do
    Ci::Pipeline.to_s.underscore
  end

  b.report('new underscore') do
    Ci::Pipeline.new_underscore
  end

  b.report('mutex underscore') do
    Ci::Pipeline.mutex_underscore
  end

  b.compare!
end
Patch for other methods
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 291375f647c4..360e791a64f6 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -98,6 +98,18 @@ def self.underscore
     Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { to_s.underscore }
   end

+  def self.new_underscore
+    @new_underscore ||= to_s.underscore
+  end
+
+  MUTEX = Mutex.new
+
+  def self.mutex_underscore # this would probably perform worse under load with many threads though
+    MUTEX.synchronize do
+      @new_underscore ||= to_s.underscore
+    end
+  end
+
   def self.where_exists(query)
     where('EXISTS (?)', query.select(1))
   end
Warming up --------------------------------------
 existing underscore    83.144k i/100ms
     to_s underscore    52.636k i/100ms
      new underscore     1.465M i/100ms
    mutex underscore   779.233k i/100ms
Calculating -------------------------------------
 existing underscore      1.673M (± 4.9%) i/s -      8.398M in   5.030956s
     to_s underscore    533.463k (± 4.1%) i/s -      2.684M in   5.040301s
      new underscore     14.555M (± 2.6%) i/s -     73.261M in   5.037141s
    mutex underscore      7.774M (± 0.4%) i/s -     38.962M in   5.012015s

Comparison:
      new underscore: 14555381.7 i/s
    mutex underscore:  7773760.4 i/s - 1.87x  slower
 existing underscore:  1673418.3 i/s - 8.70x  slower
     to_s underscore:   533463.3 i/s - 27.28x  slower

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Mehmet Emin INAC

Merge request reports

Loading