Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cache = Cash

Cache = Cash

Don't waste money and computer resources because of lazy coding.

Ad005fac83baa60843ddf2bc3bc8fe93?s=128

Stefan Wintermeyer

March 01, 2013
Tweet

Transcript

  1. Cache = Cash Stefan Wintermeyer

  2. Stefan Wintermeyer • Author of a Ruby on Rails book:

    http://xyzpub.com/en/ ruby-on-rails/3.2/ • @wintermeyer
  3. Why should we care? I borrow some data from Ilya

    Grigor for this. http://www.igvita.com/
  4. Delay User reaction 0 - 1 s :-)) 1 -

    2 s :-) 2 - 3 s hmmm... 3 - 4 s :-( > 4 s :-((
  5. Speed matters! •Don‘t lose customers. •Don‘t invest a fortune in

    hardware. •Don‘t lose the mobile market.
  6. Our aim should be: > 2 seconds BTW: google.com delivers

    in < 2 s
  7. The Application

  8. None
  9. None
  10. current_user

  11. None
  12. None
  13. None
  14. Bob‘s cart

  15. None
  16. Discounted price

  17. Rate this product

  18. Rules for today

  19. •We use the same test procedure for every time measurement.

    •That means that actual data is changed in the SQL database. •Every page has to show the current data set (no guestimations).
  20. The Software

  21. The Basic Setup • Ruby on Rails 3.2 • Ruby

    1.9.3 • Nginx • Unicorn • MySQL • Raspbian Wheezy
  22. The Models

  23. Cart state string LineItem price decimal (8,2) quantity integer Category

    name string Product description text name string price decimal (8,2) DiscountGroup discount integer name string User email string ∗ encrypted_password string ∗ first_name string last_name string Rating value integer
  24. Time Measurement

  25. We use Watir to automate the use of our browser.

    http://watir.com
  26. This is what a human does:

  27. None
  28. This is the corresponding code to replay it 3 times:

  29. require 'watir-webdriver' url = 'http://0.0.0.0:3000' b = Watir::Browser.new # A

    anonymous user looks around. # ['AA', 'AB', 'AH'].each do |product_name| b.goto url Watir::Wait.until { b.link(:text, product_name).exist? } b.link(:text, product_name).click Watir::Wait.until { b.link(:text, 'Webshop').exist? } b.link(:text, 'Webshop').click end
  30. require 'watir-webdriver' url = 'http://0.0.0.0:3000' b = Watir::Browser.new # A

    anonymous user looks around. # ['AA', 'AB', 'AH'].each do |product_name| b.goto url Watir::Wait.until { b.link(:text, product_name).exist? } b.link(:text, product_name).click Watir::Wait.until { b.link(:text, 'Webshop').exist? } b.link(:text, 'Webshop').click end 3 different products
  31. bob@aol.com

  32. Login Bob b.goto url b.goto "#{url}/users/sign_in" b.text_field(:id, "user_email").set("bob@aol.com") b.text_field(:id, "user_password").set("123")

    b.button(:name, "commit").click
  33. Bob looks around too ['AA', 'AB', 'AH'].each do |product_name| b.goto

    url Watir::Wait.until { b.link(:text, product_name).exist? } b.link(:text, product_name).click Watir::Wait.until { b.link(:text, 'Webshop').exist? } b.link(:text, 'Webshop').click end
  34. Bob rates products ['AA', 'AD', 'AI'].each do |product_name| b.goto url

    b.link(:text, product_name).click star_id = 'product_' + product_name + '_rating_2' b.link(:id, star_id).click end
  35. Bob fills his cart [1, 3, 5].each do |product_id| b.goto

    url add_button_id = 'add_' + product_id.to_s b.link(:id, add_button_id).click end
  36. Logout b.link(:text, 'Logout').click

  37. Total: 43 webpages

  38. I once run the total script on my MacBook.

  39. None
  40. The Hardware

  41. Raspberry Pi (35 USD)

  42. So... how fast is it?

  43. 116 seconds!

  44. / Started GET "/" for x.x.x.x at 2013-02-28 21:05:34 +0000

    Processing by ProductsController#index as HTML Rendered products/index.html.haml within layouts/application (3022.3ms) Rendered layouts/_navbar.html.haml (12.5ms) Rendered layouts/_footer.html.haml (2.9ms) Completed 200 OK in 3097ms (Views: 3013.3ms | ActiveRecord: 72.6ms)
  45. 0 29 58 87 116 Vanilla 116 s

  46. I bet that @wintermeyer can improve this example webshop by

    x %. @wrocloverb
  47. Fragment Caching

  48. Let‘s start with 4 new lines of code.

  49. Before %table.table.table-striped{:id => 'products'} %thead %tr %th Name %th Category

    [...] %tbody - @products.each do |product| %tr %td= link_to product.name, [...] %td= product.description
  50. After - cache [current_user, @products] do %table.table.table-striped{:id => 'products'} %thead

    %tr %th Name %th Category [...] %tbody - @products.each do |product| - cache [current_user, product] do %tr %td= link_to product.name, [...] %td= product.description
  51. After - cache [current_user, @products] do %table.table.table-striped{:id => 'products'} %thead

    %tr %th Name %th Category [...] %tbody - @products.each do |product| - cache [current_user, product] do %tr %td= link_to product.name, [...] %td= product.description complete table one row
  52. The 3rd line is in Gemfile: gem 'dalli' Don‘t forget

    to run bundle install.
  53. The 4th line is in production.rb: config.cache_store = :dalli_store

  54. How much difference can THAT make?!

  55. 0 30 60 90 120 Vanilla Fragment Caching (4 LOC)

    116 s 60 s
  56. •Each row and each table (#index). •Each #show content. •The

    shopping cart. •The navigation bar and the footer.
  57. More fragment caching > grep "cache " app/views/*/* | sed

    "s/.*-//g" cache line_item do cache [current_user, 'navigation_bar'] do cache [current_user, @current_cart, 'application_html'] do cache ['footer'] do cache [current_user, @products, 'products_index_table'] do cache [current_user, product, 'products_index_table_row'] do cache [current_user, @product, 'products_show'] do cache [current_user, @product, 'products_show_ratings_table'] do cache [rating, 'products_show_ratings_table_row'] do >
  58. 0 30 60 90 120 Vanilla Fragment Caching 116 s

    45 s
  59. HTTP Caching

  60. A browser can use Last-Modified: and Etag: to check if

    a locally cached webpage is still good.
  61. > curl -I http://0.0.0.0:3000/ HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8

    X-Ua-Compatible: IE=Edge Etag: "9a779b80e4b0ac3c60d29807e302deb7" [...] > curl -I http://0.0.0.0:3000/ HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 X-Ua-Compatible: IE=Edge Etag: "fa8fc1e981833a6885b583d351c4d823"
  62. > curl -I http://0.0.0.0:3000/ HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8

    X-Ua-Compatible: IE=Edge Etag: "9a779b80e4b0ac3c60d29807e302deb7" [...] > curl -I http://0.0.0.0:3000/ HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 X-Ua-Compatible: IE=Edge Etag: "fa8fc1e981833a6885b583d351c4d823"
  63. Set the Etag class ProductsController < ApplicationController # GET /products

    def index @products = Product.limit(25) fresh_when :etag => [current_user, @products.order(:updated_at).last] end [...]
  64. Set the Etag class ProductsController < ApplicationController # GET /products

    def index @products = Product.limit(25) fresh_when :etag => [current_user, @products.order(:updated_at).last] end [...]
  65. Set the Etag class ProductsController < ApplicationController # GET /products

    def index @products = Product.limit(25) fresh_when :etag => [current_user, @products.order(:updated_at).last] end [...]
  66. > curl -I http://0.0.0.0:3000/ -c cookies.txt HTTP/1.1 200 OK Etag:

    "4d348810e69400799e2ab684c0ef4777" > curl -I http://0.0.0.0:3000/ -b cookies.txt HTTP/1.1 200 OK Etag: "4d348810e69400799e2ab684c0ef4777" The cookie is needed for the CSRF-Token.
  67. > curl -I http://0.0.0.0:3000/ -b cookies.txt --header 'If-None-Match: "4d348810e69400799e2ab684c0ef4777"' HTTP/1.1

    304 Not Modified Etag: "4d348810e69400799e2ab684c0ef4777" 304!
  68. Let the numbers speak!

  69. 0 30 60 90 120 Vanilla Fragment Caching HTTP Cache

    45 s 35 s
  70. 35 s / 43 pages = 0,8 s/p

  71. Not good enough?

  72. We do waste a lot of time for writing the

    initial cache!
  73. What does happen when we preheat the cache?

  74. 0 30 60 90 120 Vanilla Fragment Caching HTTP Cache

    Preheater 26 s
  75. 26 s / 43 pages = 0,6 s/p WHOA!

  76. Pro-Tip: You the night to preheat your cache!

  77. Thank you!

  78. No, not yet! ;-)

  79. There is our forgotten friend ...

  80. caches_page :index, :show, :gzip => :true

  81. page_cache stores completely rendered HTML pages in a gz-format. Nginx

    is happy to deliver them without bothering Rails at all.
  82. But, we have to take care that out of date

    gz files get deleted.
  83. after_update :expire_cache before_destroy :expire_cache private def expire_cache ActionController::Base.expire_page(Rails.application.routes.url_h elpers.company_path(self)) ActionController::Base.expire_page(Rails.application.routes.url_h

    elpers.companies_path) end app/models/product.rb
  84. Obviously this needs preheating for maximum effect.

  85. page caching for current_users gets a bit tricky. But nginx

    will happily read the cookie and use a different directory for the cached pages.
  86. 0 30 60 90 120 Vanilla Fragment Caching HTTP Cache

    Preheater Page Caching 19 s
  87. 19 s / 43 pages = 0,4 s/p MEGA WHOA!

  88. Start: 116 s End: 19 s. Improvement: 610%

  89. Lessons learned • Use the not so busy time (e.g.

    3 am) to preheat your cache. • Harddrive space is cheap. Take advantage of this! • Start to think about caching at the beginning of your development. You need a clean model structure!
  90. Thank you! stefan.wintermeyer@amooma.de