Performance Guidelines

by Ashish Tajane

How to improve performance?


  • Find the target baseline
  • Know where you are now
  • Profile to find bottlenecks
  • Remove bottlenecks
  • Repeat

General Guidelines

for writing efficient code

Interpolate strings instead of adding

slow

str = s1.to_s + " " + s2.to_s + " " + s3.to_s
          

fast


str = "#{s1} #{s2} #{s3}"
          

Use destructive operations


#String
+   <<  "#{}"
sub  sub!
gsub  gsub!

#Hash
merge  merge!

#Array
+  concat
map  map!
compact  compact!
uniq  uniq!
flatten  flatten!
          

Use instance variable over attribute accessor

slow

self.var = "requires walking the AST, and results in multiple lookups"
          

fast


@var = "local variable, cached by Ruby, and requires a single lookup"
          

Use blocks instead of symbol procs


# Symbol.to_proc method, order of magniture slower...
@widget_ids = @widgets.map(&:id)
          

# same effect, not as pretty, but much faster
@widget_ids = @widgets.inject([]) {|w, a| w.push(a.id)}
          

# faster, and simpler than inject
@widget_ids = @widgets.collect {|w| w.id }
          

# yet another (faster) way to tackle the problem
@widget_ids = @widgets.map {|w| w.id }
          

Fetch only what is needed

don't

User.all.each do |user|
  user_hash[user.id] = user.name
end
          

do


User.select([:id, :name]).each do |user|
  user_hash[user.id] = user.name
end
          

Use 'pluck' if only values are needed and not objects

don't

User.all.collect(:id)
          

User.select(:id).collect(&:id)
          

do


User.pluck(:id)
          

Combine the queries and reduce the number of queries that are being fired

don't

program.groups.each do |group|
  group.connection_memberships.each do |membership|
    membership.send_email
  end
end
          

do


Connection::Membership.joins(:group).where('groups.program_id = ?', program.id).each do |membership|
  membership.send_email
end
          

Avoid firing queries whenever we have eager loaded

don't

groups.includes(:connection_memberships).each do |group|
  group.memberships.find_by_id(membership.id)
end
          

do


groups.includes(:connection_memberships).each do |group|
  group.memberships.find{|mem| mem.id == membership.id}
end
          

Avoid nested queries. Use joins instead of nesting.

don't

SELECT id
FROM users
WHERE member_id IN (SELECT id from members where admin = true)
          

do


SELECT id
FROM users LEFT JOIN members ON users.member_id = members.id
WHERE members.admin = true
          

Avoid firing queries repeatedly in the loop. precompute instead

don't

User.all.each do |user|
  if user.pending_notifications.size > 0
    user.send_email
  end
end
          

do


pending_notifications = PendingNotification.group_by(&:user_id)
User.each do |user|
  if pending_notifications[user.id].present?
    user.send_email
  end
end
          

Avoid conditional adding to the array. Instead form the hash

don't

data = []
elements.each do |element|
  data << element.id unless data.include?(element.id)
end
          

do


data = {}
elements.each do |element|
  data[element.id] = element.id
end
data = data.keys  # if needed
          

Reduce the outer loop data set in the iteration

don't

calendar_meetings = {}
User.each do |user|
  calendar_meetings[user.id] = user.meetings.with_calendar_time_available.size
end
          

do


calendar_meetings = {}
User.where(meetings: {calendar_time_available: true}).each do |user|
  calendar_meetings[user.id] = user.meetings.with_calendar_time_available.size
end
          

Avoid passing big list of ids in the query. Use joins instead

don't

SELECT id from users WHERE member_id IN (1, 2, 3, ........100000)
          

do


SELECT id from users LEFT JOIN members ON users.member_id = members.id WHERE members.admin = true
          

Avoid iterating over a big set of objects

don't

User.each do |user|
  user.send_email
end
          

do


User.find_each do |user|
  user.send_email
end
          

find_in_batches
batch_size
          

Index database columns

counter caches


(0.3ms) SELECT COUNT(*) FROM "wheels" WHERE "wheels"."vehicle_id" = 1
(0.3ms) SELECT COUNT(*) FROM "wheels" WHERE "wheels"."vehicle_id" = 2
(0.2ms) SELECT COUNT(*) FROM "wheels" WHERE "wheels"."vehicle_id" = 3
(1.1ms) SELECT COUNT(*) FROM "wheels" WHERE "wheels"."vehicle_id" = 4
(1.0ms) SELECT COUNT(*) FROM "wheels" WHERE "wheels"."vehicle_id" = 5
(1.1ms) SELECT COUNT(*) FROM "wheels" WHERE "wheels"."vehicle_id" = 6
(0.5ms) SELECT COUNT(*) FROM "wheels" WHERE "wheels"."vehicle_id" = 7
(0.3ms) SELECT COUNT(*) FROM "wheels" WHERE "wheels"."vehicle_id" = 8
          

class Wheel < ActiveRecord::Base
  belongs_to :vehicle, counter_cache: true
end

class Vehicle < ActiveRecord::Base
  has_many :wheels
end
          

Bullet Gem

  • Find N+1 queries
  • Find if counter cache is needed
  • Find unused eagerloading

Bullet Gem


# config/environments/development.rb
config.after_initialize do
  Bullet.enable = false
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
  Bullet.disable_browser_cache = true
end
          

Benchmark Everything!


require 'benchmark'

LOOP_COUNT = 1_000_000
x = ["1","2","3"]
Benchmark.bmbm do |test|
  test.report("Method invoke") do
    LOOP_COUNT.times {x.map {|elem| elem.to_i}}
  end

 test.report("Symbol.to_proc") do
   LOOP_COUNT.times {x.map(&:to_i)}
 end
end
          

--------------------------------------------------
Method invoke    1.600000   0.000000   1.600000 (  1.783758)
Symbol.to_proc  13.240000   0.070000  13.310000 ( 13.770514)
---------------------------------------- total: 14.910000sec

                     user     system      total        real
Method invoke    1.590000   0.010000   1.600000 (  1.628473)
Symbol.to_proc  13.210000   0.060000  13.270000 ( 13.678768)
          

Browser Profiling

Firefox

Profiling with built-in profiler

Firefox

Chrome

Performance Monitoring, Requests Analysis

New Relic Monitoring

Use Background Jobs

delayed_job, resque, sidekiq etc.

  • Sending emails
  • Indexing search engine
  • Generating reports
  • ...

Resources