Debugging Rails Views in Production

Today I'm going to share a quick technique for debugging Rails views in production. When there is a nasty bug or performance issue, the easiest way to find the cause is to reproduce it in the environment where it's happening with the real data and in the real context.

The technique involves monkey-patching production code in Rails console that adds print statements, defines or redefines methods that when called will get us some insights to understand what's going on. Investigating and isolating segment by segment, usually using read only operations to prevent undesirable data side effects, and we'll eventually figure out the cause.

It's easy to use this approach with small and isolated classes and methods that can be initialized and called without much setup, but we can use the same approach with the standard request-response cycle to debug views with ConsoleMethods from Rails.

Find the slow partial

Say we have a controller action that we want to investigate where exactly it's getting slow in the views. Btw, imagine that this is happening only for a single user in production and transaction average metric is not revealing us any useful info.

class HomeController < ApplicationController

  before_filter :authenticate_account!

  def index
    # some stuff
  end
end

We can now use the app instance available in console to make a GET request to / path in the app.

>> app.get('/')

Started GET "/" for 127.0.0.1 at 2017-06-11 08:45:15 +0200
Processing by HomeController#index as HTML
Completed 401 Unauthorized in 10ms (ActiveRecord: 0.0ms)
=> 302

Oh, of course. We cannot get to the view rendering yet because of the before filter and we'll need to authenticate first. We can either login first with another request or we can just stub it for the duration of this console session that will also avoid logging our credentials in production console history.

class HomeController
  skip_before_filter :authenticate_account!

  def current_account
    Account.find(1)
  end
end

Then, by making the GET request to / path we'll get the details from the views rendering:

>> app.get('/')

Started GET "/" for 127.0.0.1 at 2017-06-11 00:59:44 +0200
Processing by HomeController#index as HTML
  Rendered home/_view1.html.erb (0.0ms)
  Rendered home/_view2.html.erb (10000.2ms)
  Rendered home/index.html.erb within layouts/application (10001.3ms)
  Rendered shared/_topnav.html.erb (0.2ms)
  Rendered shared/_flash_messages.html.erb (0.1ms)
  Rendered shared/_header.html.erb (0.1ms)
  Rendered shared/_footer.html.erb (0.0ms)
Completed 200 OK in 10010ms (Views: 10009.8ms | ActiveRecord: 0.0ms)
=> 200

From the rendering info, we can see that most of the time, that is around 10 seconds, is spent rendering home/_view2.html.erb partial. We have identified that something slow is happening there but we don't know what exactly it is.

Get the stacktrace

While the request is processing the slow part we can just press CTRL+C to stop it and get a stacktrace:

>> app.get('/')

Started GET "/" for 127.0.0.1 at 2017-06-11 01:05:30 +0200
Processing by HomeController#index as HTML
  Rendered home/_view1.html.erb (0.0ms)
^C  Rendered home/_view2.html.erb (1309.6ms)
  Rendered home/index.html.erb within layouts/application (1310.6ms)
Completed 500 Internal Server Error in 1312ms (ActiveRecord: 0.0ms)

IRB::Abort (abort then interrupt!):
  app/views/home/_view2.html.erb:1:in `sleep'
  app/views/home/_view2.html.erb:1:in `_app_views_home__view__html_erb__3830501997270886489_69842991281620'
  app/views/home/index.html.erb:3:in `_app_views_home_index_html_erb___2357466009542976056_69842998601520'

  Rendered /home/dalibor/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/actionpack-4.2.8/lib/action_dispatch/middleware/templates/rescues/_source.erb (5.6ms)
  Rendered /home/dalibor/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/actionpack-4.2.8/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb (2.2ms)
  Rendered /home/dalibor/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/actionpack-4.2.8/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb (0.7ms)
  Rendered /home/dalibor/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/actionpack-4.2.8/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb within rescues/layout (18.0ms)
=> 500

From the stacktrace we can see that the slow call is the call to sleep method in view2 partial. So, once we know the "what", we can go and start figuring out the "why".

Alternatively to this, we can use TracePoint as explained in tracing ruby blog post to get a stacktrace and then play roulette to sample individual calls to figure out what's slow.

trace { app.get('/') }

ConsoleMethods module has few handy methods that you can check out.

For example, we can get the response body.

>> app.response.body.first(15)
=> "<!DOCTYPE html>"

We can call routes and helper methods, etc.

>> helper.link_to(app.root_path, 'Home')
=> "<a href=\"Home\">/</a>"