• Home

  • Custom Ecommerce
  • Application Development
  • Database Consulting
  • Cloud Hosting
  • Systems Integration
  • Legacy Business Systems
  • Security & Compliance
  • GIS

  • Expertise

  • About Us
  • Our Team
  • Clients
  • Blog
  • Careers

  • VisionPort

  • Contact
  • Our Blog

    Ongoing observations by End Point Dev people

    Ruby on Rails Performance Overview

    Steph Skardal

    By Steph Skardal
    September 6, 2011

    Over the last few months, I’ve been involved in a Ruby on Rails (version 2.3) project that had a strong need to implement performance improvements using various methods. Here, I’ll summarize some the methods and tools used for performance optimization on this application.

    Fragment Caching

    Before I started on the project, there was already a significant amount of fragment caching in use throughout the site. In it’s most basic form, fragment caching wraps a cache method around existing view code:

    <%= cache "product-meta-#{product.id}" %>
    #insert view code
    <% end %>
    

    And Rails Sweepers are used to clear the cached fragments, which looks something like the code shown below. In our application, the Sweeper attaches cache clearing methods to object callbacks, such as after_save, after_create, before_update.

    class ProductSweeper < ActionController::Caching::Sweeper
      observe Product
    
      def after_save(record)
        expire_fragment "product-meta-#{product.id}"
      end
    end
    

    Fragment caching is a good way to reuse small modular view components throughout the site. In this application, fragment caches tended to contain object meta data that was shown on various index list pages and single item show pages.

    Page Caching

    I did not initially add page caching to the application because the system has complex role management where users can have edit access at an object, class, or super level. However, later I investigated advanced techniques to leverage full page caching, described in depth here. The benefit gained here was that the application server was not hit during full page requests, and a quick AJAX request was made after the page loaded to determine user access level.

    Raw SQL methods

    Another performance technique I employed on this application was using raw SQL rather than use standard ActiveRecord methods to lookup association data. The application uses ActsAsTaggable, a gem that enables you to tag objects. The simplified data model looks like this, which includes a polymorphic relationship in the taggings table to the items tagged (products, categories):

    In the application, the front-end required that we pull the most popular 25 tags for a specific class. Working with the objects and their associations, one might use the following code:

    def self.tag_list
      b = Product.all.collect { |p| p.tag_list }.flatten
      c = b.inject({}) { |h, a| h[a] ||= 0; h[a]+=1; h } 
      c.sort_by { |k, v| v }.reverse[0..25]
    end
    

    However, this request is quite sluggish because it has to iterate through each object and it’s tags. I wrote raw SQL to generate the Tag objects, which runs at least 10 times faster than using standard ActiveRecord association lookup:

    def self.tag_list
      Tag.find_by_sql("SELECT ts.tag_id AS id, t.name FROM taggings ts
        JOIN tags t ON ts.tag_id = t.id
        WHERE taggable_type = 'Product'
        GROUP BY ts.tag_id, t.name
        ORDER BY COUNT(*) DESC LIMIT 25")
    end
    

    Typically, using ActiveRecord find methods and the item associations may yield more readable code and require minimal knowledge of the underlying database structure. But in this example, having an understanding of the database model and how to work with it gave a significant performance bump. This technique was also combined with fragment caching.

    Rails Low Level Caching

    Next up, there were several opportunities through the site to use Rails low level caching. Here’s one example of a simple use of Rails low level caching, which pulls a list of products that the user has owner or creator rights to:

    class User < ActiveRecord::Base 
      def products
        Rails.cache.fetch("user-products-#{self.id}") do
          self.roles
            .find(:all, :conditions => {:authorizable_type => 'Product', :name => ['owner','creator']})
            .collect(&:authorizable)
            .uniq
            .compact
            .sort_by{|a| a.updated_at}
        end
      end
    end
    

    Rails low level caching makes sense for data that’s pulled throughout various actions but additional computations are applied to this data. We are unable to cache this at the page request or action level, but we can cache the data retrieved with low level caching. I also used Rails low level caching on the search index pages, which is described more in depth here.

    In addition to server-side optimization, I investigated several avenues of HTML asset related performance optimization:

    • Extensive use of CSS Sprites
    • Consolidation and minification of JS, CSS. Note that Rails 3.1 introduces new functionality to improve the process of serving minified and consolidated JS and CSS.
    • HTML caching, gzipping, and Expires headers

    Tools Used

    Throughout performance tweaking, I used the following tools:

    Conclusion

    There are a few Rails caching techniques that I did not use in the application, such as action caching and SQL Caching. The Rails caching overview provides a great summary of caching techniques, but does not cover Rails low level caching. Another great resources for performance optimization is Yahoo’s Best Practices for Speeding Up Your Web Site, but it focuses on asset related optimization opportunities. I typically recommend pursuing optimization on both the server-side and asset related fronts.

    performance ruby rails


    Comments