MiniProfiler Ruby Edition

about 2 years ago

About a year ago we released a MiniProfiler for .Net. Ever since, I have been wishing for a Ruby port.

It seems I was not alone as this Stack Overflow question attests.

I am very happy to now announce our first release of MiniProfiler for Ruby. This has been made possible thanks to a large amount of effort by Aleks Totic, Robin Ward and myself. It reuses the UI created by Jarrod Dixon.

Why do you care, what does it do?

MiniProfiler is a ubiquitous production and development profiler, it allows you to quickly isolate performance bottlenecks, both on the server and client.

example of production profiling on this blog

In the example above it is clear there is an N+1 query executing for my comments on this blog. This could also be gleaned using bullet, New Relic or the traditional Rails log, however having this clearly visible on the page you are are navigating makes a very big difference.

MiniProfiler will display timings for POST requests, failed requests, json requests and so on. It keeps track of a list of "sessions" you have not seen and displays them all to you next time you have access to a UI. It allows you to easily tell how much time is spent in SQL and how much time outside of SQL.

Additionally, MiniProfiler makes it VERY easy for you to share profiling sessions with other members of your team, just send them the share link.

As an extra added bonus, MiniProfiler allows you to add probes to your html so you can tell when the client started rendering the BODY and so on (out there, scripts are usually a big cause of slow pages)

Installation and help

We will be keeping the documentation at GitHub up to date, there is also a support site at http://community.miniprofiler.com

Installation on Rails is fairly trivial, all you need to do is add the following to your Gemfile:

gem 'rack-mini-profiler'

We have a robust railtie that takes care of hooking stuff up.

In production, you are going to need to white list requests you would like to enable profiling on, for example this blog uses:

before_filter :admin_check

protected 

def admin_check
   # required only in production
   if is_admin? 
      Rack::MiniProfiler.authorize_request
   end
end

Current impact of production profiling

MiniProfiler is designed to be minimally invasive in production. However, in Ruby, method calls can get rather expensive and a few are needed to determine if profiling is allowed or not. For Rails, MiniProfiler inserts itself first in the Rack chain even before Rack::Lock, the middleware then determines if profiling is required or not. In production - it uses a cookie to determine if it is going to even try profiling, if the application whitelists the request the results are displayed, if not, the cookie is reset.

Additionally, we need to intercept the db calls with a method chain and a few checks are performed there as well.

I have tested this and am running a few sites with production profiling on, I am not able to detect any noticeable impact for unauthorized users. We are open to patches that make this area have even less impact.

If you are worried you can always only load the gem in development.

Enough chatter, show me an example

Recently, I upgraded my sites to Rails 3. I had an admin user page that displays a few fields of all 6,000 users in the system on one page. I was a bit surprised to see that rendering this page now takes about 1 second in production. Digging in to this problem was quite interesting.

time in SQL

Looking at the initial trace I could see that the time in SQL - about 280ms or so was spent in SQL. The first surprise I had was that Active Record was reporting a far lower number, 51ms only.

Started GET "/admin/users" for 10.0.0.6 at 2012-07-12 19:52:12 +1000
Processing by Admin::UsersController#index as HTML
  Rendered admin/users/index.haml within layouts/default (228.9ms)
["MB Store", "http://www.mediabrowser.tv/store", false]
  Rendered default/_navbar.haml (4.4ms)
Completed 200 OK in 955ms (Views: 242.2ms | ActiveRecord: 51.1ms)

Which makes one wonder, who is telling the truth.

Well, we can use MiniProfiler to figure this out, I attempted to query the data raw:

def index

  step("Raw") do
    a = []
    users = nil

    step("All") do 
      users = ActiveRecord::Base.connection.raw_connection
        .query('select email from users')
    end
    step("Iteration") do 
      users.each do |u|
        a << u
      end
    end
  end
  @users = User.all
end

protected 
def step(name, &blk)
  Rack::MiniProfiler.step(name, &blk)
end

This produced:

raw results

Aha, so even though the query is quite cheap, streaming it from the DB is NOT being measured in the Rails logs. Browsing through my site I have noticed that quite consistently the time to iterate through the results quite often exceeds the initial execution. This is, in fact, by design.

MiniProfiler is smart enough to intercept both pg and mysql2 for these cases. If you are using another DB it has fall-backs for ActiveRecord and Sequel interceptions.

However, we are not done.

I was still a bit confused here, where can the rest of the time be hiding, so I added the ?pp=sample option to the page to gather some call stacks. This option attempts to gather call stacks from the thread running the page (one every millisecond). The reality is that the Ruby VM will not schedule it that aggressively and you only get 120 or so stack for a 1 second operation.

Anyway, I could see that a whole bunch of stacks looked like this:

 block in execute
 block in log
ActiveSupport::Notifications::Instrumenter instrument
ActiveRecord::ConnectionAdapters::AbstractAdapter log
Rack::MiniProfiler::ActiveRecordInstrumentation log_with_miniprofiler
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter execute
ActiveRecord::ConnectionAdapters::Mysql2Adapter execute
ActiveRecord::ConnectionAdapters::Mysql2Adapter exec_query
ActiveRecord::ConnectionAdapters::Mysql2Adapter select
ActiveRecord::ConnectionAdapters::DatabaseStatements select_all
 block in select_all
ActiveRecord::ConnectionAdapters::QueryCache cache_sql
ActiveRecord::ConnectionAdapters::QueryCache select_all
 block in find_by_sql
ActiveRecord::Explain logging_query_plan
ActiveRecord::Querying find_by_sql
ActiveRecord::Relation exec_queries
 block in to_a
ActiveRecord::Explain logging_query_plan
ActiveRecord::Relation to_a
ActiveRecord::FinderMethods all
ActiveRecord::Querying all

So I added this line to app/config/initializers/mini_profiler.rb (you will need to create it)

Rack::MiniProfiler.profile_method(ActiveRecord::Querying, "all")

The I ran the page again:

after

Interesting, time in ActiveRecord and SQL is actually responsible for at least 700ms here, the logs are WAY off.

You may claim that this example is ridiculous.

  • You should only be selecting the fields you need (which is true).
  • You should be paging for that much date (sometimes can be true).
  • If you are displaying that much data and use link_to on each row that will take up more time than your db time (also true)
  • You should be caching (sure)
  • Server time is usually negligible compared to client time (MiniProfiler also display client time so you can answer that yourself)

My point here is that, more often than not, the amount of time spent in ActiveRecord is way higher than the number displayed in the logs. MiniProfiler can help you dig in to this and give you nice stack traces to explain what is happening and where it is happening.

Further more, this is low impact, ready to go in dev and easily added to production.


Start profiling your apps, in production, you will be amazed at the improvements you can make. Use MiniProfiler, use Google PageSpeed, use WebPageTest. Most of the performance tools out there are complimentary.

PS. The website http://miniprofiler.com is very .NET centric, we would love a new design that integrates the Ruby port. If you feel like making that site better - fork it - and send a pull request. Any contributions to MiniProfiler are more than welcome.

Comments

Jgw_Maxwell about 2 years ago
Jgw_Maxwell

Really like this approach to profiling apps – I've used New Relic developer mode a lot, but it's a hassle having another window open alongside and having to refresh it each time etc – bringing it into the same window makes the world of difference to development time. As we all know, if it doesn't take long we are more likely to do it, and it makes it more likely to see improvements as a result.

Great work, many thanks!

Sam Saffron about 2 years ago
Sam Saffron

Thanks! glad you like it!

Selem about 2 years ago
Selem

This looks like a great addition to profiling toolbox.

One question: Is there a way to see the quantitated overhead added by MiniProfiler?

Sam Saffron almost 2 years ago
Sam Saffron

Not really, but there are 2 contexts for this question, when it is dormant it is negligible (less than 1ms) when it is active, the numbers it shows try really hard to exclude the profiler times.

Roger about 2 years ago
Roger

So it's kind of like new relic then?

Sam Saffron almost 2 years ago
Sam Saffron

I guess, a bit, except it has a different goal. MiniProfiler aims to be constantly available and constantly there, without the need to navigate to a different place.

Chad_Moran about 2 years ago
Chad_Moran

Sam,

Glad to see you guys came through with this. Great work.

Sam Saffron almost 2 years ago
Sam Saffron

Thanks Chad!

Unix_And_Linux_Forum almost 2 years ago
Unix_And_Linux_Forum

This is really awesome work and great addition to toolbox profile.

Chuck_Illingworth almost 2 years ago
Chuck_Illingworth

The miniprofiler is a great tool for testing ror changes to see what speeds up pages. Thank you for building and sharing the miniprofiler.

Daniel_Doezema almost 2 years ago
Daniel_Doezema

I was wondering if there was support for including rails controller before_filters within the profiling results.

I have API controllers that fetch their data (collection vs member) via a before_filter :load_resources and this method does not get captured — out of the box anyway.

Sam Saffron almost 2 years ago
Sam Saffron

before filters should be captured, mini profiler injects itself first in the rack chain, that is way before these filters fire


comments powered by Discourse