Rails performance scripts: profiler and benchmarker

Filed Under (Performance, Rails, Ruby) by Leonardo Borges on 20-11-2008

Tagged Under : , ,

There are several ways you can measure your rails application’s performance. The techniques range from filling your code with “puts” statements - :p - to fancy ones like NewRelic - which is quite nice, I must say.

But what many people don’t know is that rails ships with a handful of scripts to help you out. One of which is called profiler, located under your application’s scripts/performance directory.

By default it uses the standard ruby profiler but if you want more speed - and additional reporting options - , consider installing the ruby-prof gem.

So if you execute it without params, you’ll get a clue of how it works:

$ script/performance/profiler
Usage: ./script/performance/profiler 'Person.expensive_method(10)' [times] [flat|graph|graph_html]

Pretty self explanatory, right?

As a sample code, I have in my rails app a dumb model with a really dumb method I wanna profile:

class Article < ActiveRecord::Base
  def self.find_all_with_delay
    sleep 10
    self.find(:all)
  end
end

Clearly this method doesn’t perform well and is a bottle neck in our super application! But let’s see what rails’ profiler tells us:

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
$ script/performance/profiler 'Article.find_all_with_delay' 1 graph > text_graph.perf
Loading Rails...
Using the ruby-prof extension.
Thread ID: 109440
Total Time: 10.147995
 
  %total   %self     total      self      wait     child            calls   Name
--------------------------------------------------------------------------------
 100.00%   0.00%     10.15      0.00      0.00     10.15                1     Global#[No method] (/Users/leo/projects/test/vendor/rails/railties/lib/commands/performance/profiler.rb:24}  /Users/leo/projects/test/vendor/rails/railties/lib/commands/performance/profiler.rb:24
                     10.15      0.00      0.00     10.15              1/1     Object#profile_me
--------------------------------------------------------------------------------
                     10.15      0.00      0.00     10.15              1/1     Global#[No method]
 100.00%   0.00%     10.15      0.00      0.00     10.15                1     Object#profile_me ((eval):1}  (eval):1
                      0.00      0.00      0.00      0.00              1/1     Class#const_missing
                     10.15      0.00      0.00     10.15              1/1     <Class::Article(id: integer, name: string, content: string, created_at: datetime, updated_at: datetime)>#find_all_with_delay
--------------------------------------------------------------------------------
                     10.15      0.00      0.00     10.15              1/1     Object#profile_me
  99.97%   0.00%     10.15      0.00      0.00     10.15                1     <Class::Article(id: integer, name: string, content: string, created_at: datetime, updated_at: datetime)>#find_all_with_delay (/Users/leo/projects/test/app/models/article.rb:2}  /Users/leo/projects/test/app/models/article.rb:2
                      0.15      0.00      0.00      0.15              1/1     <Class::ActiveRecord::Base>#find
                     10.00     10.00      0.00      0.00              1/1     Kernel#sleep
--------------------------------------------------------------------------------
                     10.00     10.00      0.00      0.00              1/1     <Class::Article(id: integer, name: string, content: string, created_at: datetime, updated_at: datetime)>#find_all_with_delay
  98.54%  98.54%     10.00     10.00      0.00      0.00                1     Kernel#sleep (ruby_runtime:0}  ruby_runtime:0
--------------------------------------------------------------------------------
...

As you can see, the group around line 13 is where most of the time is spent, going through our stupid call to Kernel#sleep and detailing every and each call from the very beginning. The report is much larger, so I recommend you give it a try. It’s really useful.

Now, displeased with my method’s performance, I wrote a new one that I think performs much better:

  def self.find_all_with_less_delay
    sleep 5
    self.find(:all)
  end

Nice huh? :) But how can we be sure it performs better? It turns out that under scripts/performance there is another useful script: benchmarker

Again, running it without arguments reveals it’s usage:

$ script/performance/benchmarker 
Usage: ./script/performance/benchmarker [times] 'Person.expensive_way' 'Person.another_expensive_way' ...

So, ready to see which one of my methods performs better? Let’s check:

$ script/performance/benchmarker 1 'Article.find_all_with_delay' 'Article.find_all_with_less_delay'
            user     system      total        real
#1      0.020000   0.000000   0.020000 ( 10.016033)
#2      0.010000   0.000000   0.010000 (  5.015390)

Pretty neat way to benchmark your methods huh?
Profiler and benchmarker are a powerful combination that have been helping me a lot in the projects I’m working on.

Hope you like it! See u soon! ;)

Merb turns 1.0, and started driving me crazy

Filed Under (Merb, Ruby) by Leonardo Borges on 18-11-2008

Tagged Under : ,

As you may already know, Merb turned 1.0 a while ago and I decided to resume my studies to learn the framework.

So I just built a new app with a few resources and fired the migration scripts:

$ rake db:automigrate

And this is what I got:

Loading init file from /Users/leo/projects/merb/my-first-app/config/init.rb
Loading /Users/leo/projects/merb/my-first-app/config/environments/development.rb
 ~ Connecting to database...
/opt/local/lib/ruby/gems/1.8/gems/dm-core-0.9.6/lib/dm-core/adapters/data_objects_adapter.rb:137:in `initialize': wrong number of arguments (8 for 1) (ArgumentError)
	from /opt/local/lib/ruby/gems/1.8/gems/dm-core-0.9.6/lib/dm-core/adapters/data_objects_adapter.rb:137:in `new'
	from /opt/local/lib/ruby/gems/1.8/gems/dm-core-0.9.6/lib/dm-core/adapters/data_objects_adapter.rb:137:in `normalize_uri'
	from /opt/local/lib/ruby/gems/1.8/gems/dm-core-0.9.6/lib/dm-core/adapters/abstract_adapter.rb:44:in `initialize'
	from /opt/local/lib/ruby/gems/1.8/gems/dm-core-0.9.6/lib/dm-core/adapters/data_objects_adapter.rb:159:in `initialize'
	from /opt/local/lib/ruby/gems/1.8/gems/dm-core-0.9.6/lib/dm-core.rb:157:in `new'
	from /opt/local/lib/ruby/gems/1.8/gems/dm-core-0.9.6/lib/dm-core.rb:157:in `setup'
	from /opt/local/lib/ruby/gems/1.8/gems/merb_datamapper-1.0/lib/merb/orms/data_mapper/connection.rb:44:in `setup_connections'
	from /opt/local/lib/ruby/gems/1.8/gems/merb_datamapper-1.0/lib/merb/orms/data_mapper/connection.rb:27:in `connect'
	from /opt/local/lib/ruby/gems/1.8/gems/merb_datamapper-1.0/lib/merb_datamapper.rb:17:in `run'
	from /opt/local/lib/ruby/gems/1.8/gems/merb-core-1.0/lib/merb-core/bootloader.rb:99:in `run'
	from /opt/local/lib/ruby/gems/1.8/gems/merb-core-1.0/lib/merb-core/server.rb:172:in `bootup'
	from /opt/local/lib/ruby/gems/1.8/gems/merb-core-1.0/lib/merb-core/server.rb:42:in `start'
	from /opt/local/lib/ruby/gems/1.8/gems/merb-core-1.0/lib/merb-core.rb:169:in `start'
	from /opt/local/lib/ruby/gems/1.8/gems/merb-core-1.0/bin/merb:11
	from /opt/local/bin/merb:19:in `load'
	from /opt/local/bin/merb:19

Lovely, isn’t it?
After a fair amount of googling around it turns out that there seems to be a problem with the URI parser Merb uses underneath, called Addressable.

As of the installation of Merb 1.0, the installed version of this library was 2.0.0.
The solution? Install a older Addressable version:

$ sudo gem uninstall addressable
 
You have requested to uninstall the gem:
	addressable-2.0.0
dm-core-0.9.6 depends on [addressable (>= 1.0.4)]
data_objects-0.9.6 depends on [addressable (>= 1.0.3)]
If you remove this gems, one or more dependencies will not be met.
Continue with Uninstall? [Yn]  Y
Successfully uninstalled addressable-2.0.0
 
$ sudo gem install addressable -v 1.0.4
Successfully installed addressable-1.0.4
1 gem installed
Installing ri documentation for addressable-1.0.4...
Installing RDoc documentation for addressable-1.0.4...

I haven’t figured out the reason for this issue, so please do share if you know. I’ll be glad to update the post. :)