codahale.com٭blog

Coda Hale lives in Berkeley, CA, where he writes about Ruby on Rails, usability, web design and development, and the occasional bit about bicycles.

Content-only caching for Rails

So you’ve got a Rails app which is mostly static content, but it’s got some dynamic, user-specific stuff mixed in with the layout. You’d love to cache the static data, since it doesn’t change often, but that would leave you updating the dynamic content via AJAX or something, and as cool as AJAX is, it’s for crap when the Javascript is turned off.

You try page caching, but you notice that the dynamic content doesn’t update. You try action caching, but it’s the same story. You try fragment caching, but then your app still performs all the big database queries in your actions. There’s a level of granularity missing in Rails’ caching system. Cached pages are stupid-quick for very static content, cached actions allow you to filter via ActionController, and cached fragments clean up the messy bits of your views. But you can’t cache just a rendered view. Until now… (dun dun duuun)

Content caching is a different level of granularity for Rails. Like action caching, requests are routed through the ActionController framework. Unlike action caching, none of the layout is cached, allowing you to provide some dynamic, user-specific content while reducing DB loads and rendering times. It’s a bit like fragement caching, but if a copy exists in the cache, the controller action isn’t called, meaning the database is never queried.

Installation

Subversionality:


./script/plugin install -x http://svn.codahale.com/content_cache

Living dangerously?


./script/plugin install http://svn.codahale.com/content_cache

Usage


class NotesController < ActionController::Base
  caches_action_content :index

  def index
    @notes = Note.find(:all, :include => [:monkeys, dirigibles, robots])
  end
end

The first time /notes/index is requested, #index is executed and whatever it renders is stored wherever you have the cache store configured. None of the layout is stored, just the rendered view. The next time /notes/index is requested, the cached action content is read from the cache, placed within the layout, and sent to the client.

Sometimes an action’s instance variables are used in the layout itself–to set the title, for example. Instance variables your layout depends on can be specified as such:


ActionContentFilter.preserved_instance_variables += ['@title', '@content_type']

The content of these instance variables are cached alongside the action’s content, and sent to the layout during a request. (These types are marshalled, which means that simple data types are preferred, and anything which refers to records in a database will likely break after a certain period of time. Best to limit this to strings, integers, arrays, and other simple types which play well with marshalling.)

Speed

I did some rough benchmarking with this, and created an SQLite3 database with a single table and a thousand records in it. I generated a scaffold for this model and removed the pagination from the #list action. I made an alias of the #index action, called #index_with_cache, because I’m creative like this.

I requested each location twice. The first round was, as you’d expect, identical. The second round shows the benefit of the content caching mojo:


Completed in 3.93749 (0  reqs/sec) | Rendering: 3.38100 (85%) | DB: 0.48483 (12%) | 200 OK [http://localhost/monkeys/index]
Completed in 0.01605 (62 reqs/sec) | Rendering: 0.01167 (72%) | DB: 0.00000 ( 0%) | 200 OK [http://localhost/monkeys/index_with_cache]

That’s 24,532% faster!

Then I limited the number of results to 100, to get a more realistic picture:


                     user     system      total        real
without cache   15.360000   0.870000  16.230000 ( 20.844324)
with cache       3.250000   0.280000   3.530000 (  5.775237)

And this is just in development mode, too. Granted, this is a pretty edge case, but if you’ve got a lot of database traffic, caching will speed your app up something fierce. Plenty of people have been putting off adding caching because until now it’s been an all-or-nothing affair.

Have fun, kids!

31 comments »