Slide 1

Slide 1 text

Intro to Rails #webperf

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

@amateurhuman

Slide 4

Slide 4 text

1.9.2 1.8.7 1.9.3 1.8.6 ruby -v

Slide 5

Slide 5 text

MRI 1.9.3 MRI 1.8.7 require ‘benchmark’ RBX 1.2.4 JRuby 1.6.7

Slide 6

Slide 6 text

Passengers, Unicorns, Pumas. Oh my!

Slide 7

Slide 7 text

Passenger Simple to operate. Simple configuration. Handles worker management. Great for multi-app environments. Great for low resource environments. Attached to Nginx/Apache HTTPD.

Slide 8

Slide 8 text

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.

Slide 9

Slide 9 text

Puma Based on Mongrel. Designed for concurrency. Uses real threads.

Slide 10

Slide 10 text

Anatomy of a Web Request

Slide 11

Slide 11 text

Redirects Cache DNS TCP SSL Request App Response DOM Render Link Clicked First Byte DOM Loaded Load

Slide 12

Slide 12 text

80% in Front-End

Slide 13

Slide 13 text

Inside the Web App

Slide 14

Slide 14 text

Database Performance

Slide 15

Slide 15 text

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.

Slide 16

Slide 16 text

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 %>

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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)

Slide 19

Slide 19 text

Finding N+1

Slide 20

Slide 20 text

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.

Slide 21

Slide 21 text

Missing Indexes Hurt

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Cache All The Things

Slide 24

Slide 24 text

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; }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Expiring Caches is Hard

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Background Jobs

Slide 30

Slide 30 text

Procrastinate Reporting. Sending email. Processing images. Call external services. Building & Expiring Caches.

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

It’s Free in Rails 4 Establishing basic Queue API. Implement push and pop. Easily swap out for Resque, Sidekiq, Delayed job.

Slide 34

Slide 34 text

What’s One More Second?

Slide 35

Slide 35 text

7% Fewer Conversions

Slide 36

Slide 36 text

11% Fewer Page Views

Slide 37

Slide 37 text

Time is Money

Slide 38

Slide 38 text

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.

Slide 39

Slide 39 text

30-day free trial at newrelic.com/30 Q?