Amir Sharif
Engineer.
Weekend hacker.
Self improvement enthusiast.

Performance Cheatsheet for Ruby on Rails

Simple Tests

This one liner you can add to your Pry to see how long a single method call takes.

def pmeasure(&block)
  now = Time.now.to_f
  yield
  duration = Time.now.to_f - now
  puts "Finished in #{duration.round(6)}s"
end
# pmeasure { sleep 1 }

Benchmarking

You can use the benchmark library in native Ruby for simple tests.

require 'benchmark'
puts Benchmark.measure {
  evens = []
  arrays = [[1,2,3], [4,2,1], [3,4,2], [1,2,1]]
  arrays.each do |array|
    array.each do |num|
     evens << num if num.even?
    end
  end
}

evens = []
arrays = [[1,2,3], [4,2,1], [3,4,2], [1,2,1]]
arrays.each do |array|
  array.each do |num|
   evens << num if num.even?
  end
end

arrays.inject([]) do |result, arr|
  result + arr.select { |x| x.even? }
end

arrays = [[1,2,3], [4,2,1], [3,4,2], [1,2,1]]

n = 100_000
Benchmark.bmbm do |x|
  x.report("inject") do
    n.times do
      array.inject(0, :+)
    end
  end
  x.report("sum") do
    n.times do 
      array.sum
    end
  end
end

Use benchmark-ips to figure out how many times to run the given function to get interesting data.

require 'benchmark/ips'

Benchmark.ips do |x|
  # Configure the number of seconds used during
  # the warmup phase (default 2) and calculation phase (default 5)
  x.config(:time => 5, :warmup => 2)

  # These parameters can also be configured this way
  x.time = 5
  x.warmup = 2

  # Typical mode, runs the block as many times as it can
  x.report("addition") { 1 + 2 }

  # To reduce overhead, the number of iterations is passed in
  # and the block must run the code the specific number of times.
  # Used for when the workload is very small and any overhead
  # introduces incorrectable errors.
  x.report("addition2") do |times|
    i = 0
    while i < times
      1 + 2
      i += 1
    end
  end

  # To reduce overhead even more, grafts the code given into
  # the loop that performs the iterations internally to reduce
  # overhead. Typically not needed, use the |times| form instead.
  x.report("addition3", "1 + 2")

  # Really long labels should be formatted correctly
  x.report("addition-test-long-label") { 1 + 2 }

  # Compare the iterations per second of the various reports!
  x.compare!
end

Where’s the bottleneck?

Sometimes you want to find out why your logic is slow. You can use rack-mini-profiler for Ruby on Rails apps.

You can add these tags to the end of your URL to instrument any Rails action.

?pp=flamegraph
?pp=profile-memory

StackProf works well for Ruby code.

StackProf.run(mode: :cpu, out: 'tmp/stackprof-cpu-myapp.dump', raw: true) do
  # my expensive logic
end
# Open tmp/stackprof-cpu-myapp.dump to see the results.

An alternative is Vernier. It tracks multiple threads, GVL activity, GC pauses, idle time, and more.

Vernier.profile(out: "time_profile.json") do
  some_slow_method
end

Vernier.start_profile(out: "time_profile.json")
some_slow_method

# some other file
some_other_slow_method
Vernier.stop_profile

For identifying the source of long running queries, I like sqlcommenter.

gem "activerecord-sql_commenter", require: "active_record/sql_commenter"

# application.rb
config.active_record.query_log_tags_enabled = true
config.active_record.query_log_tags = [ :application, :controller, :action, :job ]
config.active_record.cache_query_log_tags = true

Memory

Use Memory Profiler.

# scratch.rb contains your code.
ruby-memory-profiler run rails r scratch.rb

# as a conveniance method
report = MemoryProfiler.report do
  # run your code here
end

Tags
Code

Date
November 3, 2022