Wednesday, 16 May 2007

2. It's best to do one thing really, really well. (Rails refactoring with painless AOP)

I like Google philosophy a lot, especially Thing Number 9 :
  • 9. You can be serious without a suit.
, but that's not the subject of this sermon; today I'd like to tell you about Thing Number Two (TNT) :
  • 2. It's best to do one thing really, really well.
- Amen -, and its IT translation : the "Separation Of Concerns". At the humble coder's level, separated concerns make for a more readable code. Counter example:
1
2
3
4
5
6
def fetch_stuff(*args)
  benchmark = Benchmark.measure{
    Stuff.fetch_all
  }
  puts "time spent: #{benchmark} "
end
It's ugly, but it works. It works, but it's ugly: this code does TWO completely unrelated things:
  1. fetch some stuff, and
  2. benchmark an action, any action
We MUST find a way to separate those 2 concerns: The first and main concern is obvious :
1
2
3
def fetch_stuff
  Stuff.fetch_all # <<--- CONCERN 1 
end
The second concern is easy to code
1
2
3
4
5
6
def generic_benchmarking_method
  benchmark = Benchmark.measure
    action_to_measure   # <<--- (concern 1)       
  }
  puts "time spent : #{benchmark}}" # <<--- CONCERN 2
end   
but it's difficult to connect to the first method, because :
  • users of concern 1 should not have to know about concern 2 => they will just call the first method
  • as the benchmarking action is completely independent from the action it measures, this action should be passed as a parameter in some way
The solution: piggy-back the benchmarking method on the fetching method, transparently. Q: how do you piggy back ? A: with Rails' alias_method_chain , and some "convention over configuration magic"** (** hint: it's all in the methods naming) Take a deep breath and watch the mystery unveil :

Before

1
2
3
4
5
6
def fetch_stuff(*args)
  benchmark = Benchmark.measure{
    Stuff.fetch_all
  }
  puts "time spent: #{benchmark} "
end

After

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# concern 1: just do it.
def fetch_stuff
  Stuff.fetch_all
end

# concern 2: just benchmark it.
def fetch_stuff_with_benchmarking(*args)
  benchmark = Benchmark.measure{
  fetch_stuff_without_benchmarking(*args)
  }
  puts "time spent: #{benchmark} "
end

alias_method_chain :fetch_stuff, :benchmarking
That's it : alias_method_chain wrapped benchmarking around fetch_stuff. After line 14 has been parsed, calls to the original fetch_stuff are redirected to fetch_stuff_with_benchmarking, and fetch_stuff_without_benchmarking points to the original fetch_stuff. It's transparent and automatic. All you have to do is respect the naming convention : with and without. That's it. This method was introduced in Rails a year ago to DRY up the internals. There is no reason your code should not benefit shamelessly from it too. Strive for it. You'll thank me later. API : Articles :

Sunday, 13 May 2007

Rails plugins : group_by(environment)

If your vendor/plugins directory gets overcrowded and you use some test/development-specific plugins, here is a quick way to clean up your source tree :

Step 1: split the vendor/plugins directory

Create two directories : vendor/plugins-development and vendor/plugins-test, and move your plugins accordingly :

Step 2: tell Rails where to find the moved plugins

In config/environment/development.rb add this line:
1
2
3

config.plugin_paths.push('vendor/plugins-development' ).uniq!
, and in config/environment/test.rb add:
1
2
3

config.plugin_paths.push('vendor/plugins-test' ).uniq!
And that's it.

Thursday, 10 May 2007

Rails refactoring with delegate(..)

“When something is too hard it means that you’re not cheating enough.”
DHH Before:
1
2
3
4
5
6
7
8
9
10
11
class Cart
  def initialize
    @items    = []
  end

  def size   ;    @items.size    end
  def clear  ;    @items.clear   end
  def empty? ;    @items.empty?  end

  attr_reader :items
end

After

1
2
3
4
5
6
7
8
9
class Cart
  def initialize 
    @items    = []
  end

  delegate :clear, :empty?, :size, :to => :items

  attr_reader :items
end
What it does: When you call cart.size , Rails delegates the size() method call to the items object and executes: cart.items.size How it works: If you look at the source, you'll notice that Rails actually generates 3 methods equivalent to the code we have just removed (see 'Before'), and injects them in the class code. The behaviour does not change; it's just a shorter way to write this code. Should you want (be careful) to delegate ALL the unknown methods calls, you could write :
1
2
3
4
# delegate unknown methods calls to @items
def method_missing(method, *args, &block)
   @items.send method, *args, &block
end
API doc : http://caboo.se/doc/classes/Module.html#M003442

Thursday, 3 May 2007

<% cache_unless(params.include?('no_cache_please') , ... do %>

Is adding fragments/actions caching worth it for your Rails application, on your server?
  1. Adding caching to an application complexifies the code (and can be hard to test).
  2. The caching efficiency (in req/sec) depends on many factors : RAM size, CPU power, HD speed, DB caches size, ... A spectacular improvement measured on an old sluggish staging server could be barely noticeable on a modern 4-core machine with 8GB of RAM and the fastest disk money can buy.
Is caching worth it for your application, on your server? It depends. For example, in the Vooruit project the staging server is an old fart with a slow HD and barely enough memory to run 1 Mongrel instance. This is the machine I used to hand-tune the fragment-caching code. I needed to confirm that caching was also improving significantly the performance on the production server. Tip:
  • disable remotely some/all caching on the production server through a parameter in the url:
http://example.com/foobar?no_cache_please
1
2
3
4
5
6
7
8

9
10
11
12
13
14
In the views you want to test :
---------------------------------

<% cache_unless(params.include?('no_cache_please') , :genre => params[:genre]) do%>
   ... the cached code (html and erb)
<% end %>      


In application_helper.rb :
--------------------------

  def cache_unless(condition, name = {}, &block)
    if condition then block.call; return end
    cache(name, &block)
  end

Now you can turn the caching on and off to measure the speed increase on any remote machine:
httperf --server production.example.com --uri /nl --num-conns 100
Reply rate [replies/s]: min 6.6 avg 6.8 max 7.0 stddev 0.3 (2 samples)
Reply rate [replies/s]: min 5.6 avg 6.6 max 7.2 stddev 0.9 (3 samples)


httperf --server production.example.com --uri /nl?no_cache --num-conns 100
Reply rate [replies/s]: min 7.0 avg 7.1 max 7.2 stddev 0.1 (2 samples)
Reply rate [replies/s]: min 3.2 avg 5.0 max 7.2 stddev 1.7 (4 samples)
Was caching worth it? In this case, barely. Updated:
Clarified that this article is mostly about fragments and actions caching.

Tuesday, 1 May 2007

Bugs have a life of their own

I've never put any bug in my code. They all sneeked in, on their own, without my consent. Bugs have a life of their own. Bugs were born and normally live in Bugland. When they want to take some vacation, they visit their family that happens to live in your code base. Some like it so much abroad that they decide stay longer, or even become permanent resident (MS products).