Intro to Rails #webperf
teu rhu man
@ ama
ruby -v
                1.9.2
            1.8.7
1.9.3
1.8.6
require ‘benchmark’
  MRI 1.9.3
              MRI 1.8.7
              RBX 1.2.4
         JRuby 1.6.7
Passengers,
Unicorns,
Pumas.
Oh my!
Passenger
Simple to operate.
Simple configuration.
Handles worker management.
Great for multi-app environments.
Great for low resource environments.
Attached to Nginx/Apache HTTPD.
Unicorn
Highly configurable.
Independent of front-end web server.
Master will reap children on timeout.
Great for single app environments.
Allows for zero downtime deploys.
Puma
Based on Mongrel.
Designed for concurrency.
Uses real threads.
Anatomy of a Web Request
Link Clicked                DOM Loaded


Redirects Cache DNS TCP SSL Request App Response DOM Render




                                First Byte        Load
80% in Front-End
Inside the Web App
Database Performance
Lazy Loading
๏   ORMs make it easy to access data.
๏   Easy access to data can create issues.
๏   Performance issues are hard to see in
    development mode.
๏   Look to production metrics to optimize
    and refactor.
N+1 Query Creep
# app/models/customer.rb
class Customer < ActiveRecord::Base
  has_many :addresses
end

# app/models/address.rb
class Address < ActiveRecord::Base
  belongs_to :customer
end

# app/controllers/customers_controller.rb
class CustomersController < ApplicationController
  def index
      @customers = Customer.all
  end
end

# app/views/customers/index.html.erb
<% @customers.each do |customer| %>
  <%= content_tag :h1, customer.name %>
<% end %>
N+1 Query Creep
# app/views/customers/index.html.erb
<% @customers.each do |customer| %>
  <%= content_tag :h1, customer.name %>
  <%= content_tag :h2, customer.addresses.first.city %>
<% end %>


If @customers has 100 records, you'll have 101 queries:
SELECT "customers".* FROM "customers"
SELECT "addresses".* FROM "addresses" WHERE   "addresses"."customer_id" = 1 AND
  "addresses"."primary" = 't' LIMIT 1
SELECT "addresses".* FROM "addresses" WHERE   "addresses"."customer_id" = 2 AND
  "addresses"."primary" = 't' LIMIT 1
SELECT "addresses".* FROM "addresses" WHERE   "addresses"."customer_id" = 3 AND
  "addresses"."primary" = 't' LIMIT 1
...
...
SELECT "addresses".* FROM "addresses" WHERE   "addresses"."customer_id" = 100
  AND "addresses"."primary" = 't' LIMIT 1
Eager Load Instead
# app/controllers/customers_controller.rb
class CustomersController < ApplicationController
  def index
      @customers = Customer.includes(:addresses).all
  end
end

If @customers has 100 records, now we only have 2 queries:
SELECT "customers".* FROM "customers"
SELECT "addresses".* FROM "addresses" WHERE   "addresses"."customer_id" IN (1,
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,   15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,   34, 35, 36, 37, 38, 39, 40, 41,
42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52,   53, 54, 55, 56, 57, 58, 59, 60,
61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,   72, 73, 74, 75, 76, 77, 78, 79,
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,   91, 92, 93, 94, 95, 96, 97, 98,
99, 100)
Finding N+1
Missing Indexes
๏   Searching a 1,000 row table with an
    index is 100x faster than searching
    without.
๏   Put indexes anywhere you might need to
    query; less is not more with indexes.
๏   Writing an index will lock your tables.
Missing Indexes Hurt
Indexes are Easy
# db/migrate/20120201040247_add_index_for_shop_id_on_orders.rb

class AddIndexForShopIdOnOrders < ActiveRecord::Migration
  def change
      add_index :orders, :shop_id
  end
end
Cache All The Things
Page Caching
# app/controllers/products_controller.rb
class ProductsController < ActionController

  caches_page :index

  def index
    @products = Products.all
  end

  def create
    expire_page :action => :index
  end

end

# /opt/nginx/conf/nginx.conf
location / {
  gzip_static on;
}
Action Caching
# app/controllers/products_controller.rb
class ProductsController < ActionController

  before_filter :authenticate
  caches_action :index

  def index
    @products = Product.all
  end

  def create
    expire_action :action => :index
  end

end
Fragment Caching
# app/views/products/index.html.erb

<% Order.find_recent.each do |o| %>
  <%= o.buyer.name %> bought <%= o.product.name %>
<% end %>

<% cache(‘all_products’) do %>
  All available products:
  <% Product.all.each do |p| %>
    <%= link_to p.name, product_url(p) %>
  <% end %>
<% end %>

# app/controllers/products_controller.rb
class ProductsController < ActionController

  def update
    expire_fragment(‘all_products’)
  end

end
Expiring Caches
    is Hard
Russian Doll Caching
# app/views/products/show.html.erb
<% cache product do %>
  Product options:
  <%= render product.options %>
<% end %>

# app/views/options/_option.html.erb
<% cache option do %>
  <%= option.name %>
  <%= option.description %>
<% end %>

# app/models/product.rb
class Product < ActiveRecord::Base
  has_many :options
end

# app/models/option.rb
class Option < ActiveRecord::Base
  belongs_to :product, touch: true
end
Background Jobs
Procrastinate
Reporting.
Sending email.
Processing images.
Call external services.
Building & Expiring Caches.
Rescued by Resque
class ReferralProcessor
  @queue = :referrals_queue

  def self.perform(schema_name, order_item_id)
    order_item = OrderItem.find(order_item_id)
    order = order_item.order
    user = order.user

    credit = AccountCredit.credit(order_item.unit_price, user, 'referral')
    credit.message = I18n.t('account_credits.predefined_messages.referral',
                            :description => order_item.description)
    credit.save!

    debit = Transaction.account_debit(credit.amount, user)
    debit.order = order
    debit.save!

    order.issue_refund(return_to_inventory: false, gateway_first: true,
                       cancel_items: false, cancel_certificates: false,
                       amount: credit.amount, as: 'original', notify_user: false)

    if user.receives_mail_for?(:referral_purchase)
      SystemMailer.referral_refund(order_item, credit).deliver
    end
  end
end
Get in Line
class ReferralObserver < ActiveRecord::Observer

  def after_create(referral)
    Resque.enqueue_in(1.day, ReferralProcessor, referral.item.id)
  end

end



# Get it started
$ PIDFILE=./resque.pid 
  BACKGROUND=yes 
  QUEUE=referrals_queue 
  rake environment resque:work
It’s Free in Rails 4

Establishing basic Queue API.
Implement push and pop.
Easily swap out for Resque, Sidekiq,
Delayed job.
What’s One More Second?
7%
Fewer Conversions
11%
Fewer Page Views
Time is
Money
Monitor your applications.
Performance is not set it and forget it.
Database indexes are cheap, make more.
Cache something, somewhere.
Push work off to the background.
Don’t neglect front-end performance.
Q?
30-day free trial at
newrelic.com/30

Intro to-rails-webperf

  • 1.
  • 3.
  • 4.
    ruby -v 1.9.2 1.8.7 1.9.3 1.8.6
  • 5.
    require ‘benchmark’ MRI 1.9.3 MRI 1.8.7 RBX 1.2.4 JRuby 1.6.7
  • 6.
  • 7.
    Passenger Simple to operate. Simpleconfiguration. Handles worker management. Great for multi-app environments. Great for low resource environments. Attached to Nginx/Apache HTTPD.
  • 8.
    Unicorn Highly configurable. Independent offront-end web server. Master will reap children on timeout. Great for single app environments. Allows for zero downtime deploys.
  • 9.
    Puma Based on Mongrel. Designedfor concurrency. Uses real threads.
  • 10.
    Anatomy of aWeb Request
  • 11.
    Link Clicked DOM Loaded Redirects Cache DNS TCP SSL Request App Response DOM Render First Byte Load
  • 12.
  • 13.
  • 14.
  • 15.
    Lazy Loading ๏ ORMs make it easy to access data. ๏ Easy access to data can create issues. ๏ Performance issues are hard to see in development mode. ๏ Look to production metrics to optimize and refactor.
  • 16.
    N+1 Query Creep #app/models/customer.rb class Customer < ActiveRecord::Base has_many :addresses end # app/models/address.rb class Address < ActiveRecord::Base belongs_to :customer end # app/controllers/customers_controller.rb class CustomersController < ApplicationController def index @customers = Customer.all end end # app/views/customers/index.html.erb <% @customers.each do |customer| %> <%= content_tag :h1, customer.name %> <% end %>
  • 17.
    N+1 Query Creep #app/views/customers/index.html.erb <% @customers.each do |customer| %> <%= content_tag :h1, customer.name %> <%= content_tag :h2, customer.addresses.first.city %> <% end %> If @customers has 100 records, you'll have 101 queries: SELECT "customers".* FROM "customers" SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" = 1 AND "addresses"."primary" = 't' LIMIT 1 SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" = 2 AND "addresses"."primary" = 't' LIMIT 1 SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" = 3 AND "addresses"."primary" = 't' LIMIT 1 ... ... SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" = 100 AND "addresses"."primary" = 't' LIMIT 1
  • 18.
    Eager Load Instead #app/controllers/customers_controller.rb class CustomersController < ApplicationController def index @customers = Customer.includes(:addresses).all end end If @customers has 100 records, now we only have 2 queries: SELECT "customers".* FROM "customers" SELECT "addresses".* FROM "addresses" WHERE "addresses"."customer_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100)
  • 19.
  • 20.
    Missing Indexes ๏ Searching a 1,000 row table with an index is 100x faster than searching without. ๏ Put indexes anywhere you might need to query; less is not more with indexes. ๏ Writing an index will lock your tables.
  • 21.
  • 22.
    Indexes are Easy #db/migrate/20120201040247_add_index_for_shop_id_on_orders.rb class AddIndexForShopIdOnOrders < ActiveRecord::Migration def change add_index :orders, :shop_id end end
  • 23.
  • 24.
    Page Caching # app/controllers/products_controller.rb classProductsController < ActionController caches_page :index def index @products = Products.all end def create expire_page :action => :index end end # /opt/nginx/conf/nginx.conf location / { gzip_static on; }
  • 25.
    Action Caching # app/controllers/products_controller.rb classProductsController < ActionController before_filter :authenticate caches_action :index def index @products = Product.all end def create expire_action :action => :index end end
  • 26.
    Fragment Caching # app/views/products/index.html.erb <%Order.find_recent.each do |o| %> <%= o.buyer.name %> bought <%= o.product.name %> <% end %> <% cache(‘all_products’) do %> All available products: <% Product.all.each do |p| %> <%= link_to p.name, product_url(p) %> <% end %> <% end %> # app/controllers/products_controller.rb class ProductsController < ActionController def update expire_fragment(‘all_products’) end end
  • 27.
  • 28.
    Russian Doll Caching #app/views/products/show.html.erb <% cache product do %> Product options: <%= render product.options %> <% end %> # app/views/options/_option.html.erb <% cache option do %> <%= option.name %> <%= option.description %> <% end %> # app/models/product.rb class Product < ActiveRecord::Base has_many :options end # app/models/option.rb class Option < ActiveRecord::Base belongs_to :product, touch: true end
  • 29.
  • 30.
    Procrastinate Reporting. Sending email. Processing images. Callexternal services. Building & Expiring Caches.
  • 31.
    Rescued by Resque classReferralProcessor @queue = :referrals_queue def self.perform(schema_name, order_item_id) order_item = OrderItem.find(order_item_id) order = order_item.order user = order.user credit = AccountCredit.credit(order_item.unit_price, user, 'referral') credit.message = I18n.t('account_credits.predefined_messages.referral', :description => order_item.description) credit.save! debit = Transaction.account_debit(credit.amount, user) debit.order = order debit.save! order.issue_refund(return_to_inventory: false, gateway_first: true, cancel_items: false, cancel_certificates: false, amount: credit.amount, as: 'original', notify_user: false) if user.receives_mail_for?(:referral_purchase) SystemMailer.referral_refund(order_item, credit).deliver end end end
  • 32.
    Get in Line classReferralObserver < ActiveRecord::Observer def after_create(referral) Resque.enqueue_in(1.day, ReferralProcessor, referral.item.id) end end # Get it started $ PIDFILE=./resque.pid BACKGROUND=yes QUEUE=referrals_queue rake environment resque:work
  • 33.
    It’s Free inRails 4 Establishing basic Queue API. Implement push and pop. Easily swap out for Resque, Sidekiq, Delayed job.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
    Monitor your applications. Performanceis not set it and forget it. Database indexes are cheap, make more. Cache something, somewhere. Push work off to the background. Don’t neglect front-end performance.
  • 39.
    Q? 30-day free trialat newrelic.com/30