Tracking Common Rails Performance Issues with Skylight

Share this article

Tracking Common Rails Performance Issues with Skylight
skylight

Today I am going to introduce you Skylight – a smart and easy to use online tool for profiling Rails, Sinatra, and Grape applications. While discovering its main features, we will discuss common Rails performance issues along with ways to fix them.

Skylight is somewhat similar to the popular service called New Relic, but it was designed specifically for Rails apps with a goal to make interacting with the dashboard easier. Skylight’s team believes that New Relic has too many features and not all of them are really useful. Therefore, the Skylight team are focusing on core components that provide immediate value. Skylight is free for up to 100k requests per month and you also receive a free 30-day trial to test its functionality. Not bad, eh?

The source code for the sample application can be found on GitHub.

Preparations

To see Skylight in action, we need a sample Rails application. It is going to be very simple, but that’ll be enough for this introductory article:

$ rails new SkylightDiag -T

I am using Rails 5.0.0.1, but Skylight works with versions 3 and 4 as well.

Suppose, in this application, we want to list musical albums and their tracks. All data will be completely fake, but that does not really matter. Create an Album and a Song model:

$ rails g model Album title:string musician:string
$ rails g model Song title:string duration:integer album:belongs_to
$ rails db:migrate

Make sure associations are set up properly:

models/album.rb

[...]
has_many :songs, dependent: :delete_all
[...]

models/song.rb

[...]
belongs_to :album
[...]

Add a controller and the root route (the view will be added later):

albums_controller.rb

class AlbumsController < ApplicationController
  def index
    @albums = Album.all
  end
end

config/routes.rb

[...]
root 'albums#index'
[...]

Instead of populating sample data by hand, let’s rely on db/seeds.rb and the Faker gem that returns random data of various kind:

Gemfile

[...]
gem 'faker'
[...]

Don’t forget to run

$ bundle install

This gem was recently updated and now includes even more types of sample data (beer and Star Wars heroes’ names included). Prepare the seeds.rb file:

db/seeds.rb

50.times do
  album = Album.create({title: Faker::Book.title, musician: Faker::StarWars.character})
  30.times do
    album.songs.create({title: Faker::Book.title, duration: rand(500)})
  end
end

Well, if you are a fan of “Game of Thrones”, you may take its characters’ names instead

Lastly, run

$ rails db:seed

to populate the database. Now our lab environment is prepared and we can proceed to the next step.

Integrating Skylight

Before diving into the code, let’s quickly discuss which environments Skylight supports. It works with Ruby 1.9.2 and higher, but 2.1+ is recommended to take full advantage of the services’ tools. As I already said before, it works with Rails 3+, but Sinatra 1.2+, and even Grape 0.10+ are supported, as well (but you’ll have less detailed info). Lastly, Skylight should work with any *nix server, no special configuration is needed even for Heroku. The tracking client is written in Rust and C so it should not consume much memory.

To start off, grab your 30-day trial here (you’ll also receive a $50 credit as a little bonus from me :) ). Populate the basic information about yourself and your application, then add a new gem into the Gemfile:

Gemfile

[...]
gem "skylight"
[...]

Run

$ bundle install

Also note that Skylight’s setup wizard asks you to run a command looking like:

$ bundle exec skylight setup SOME_KEY

This command finalizes the integration by creating a config/skylight.yml file. After that, you are good to go. Note that a private token used to share application’s data was presented to you. It can be regenerated in your account’s settings page. Also don’t forget that Skylight won’t send any data in the development environment, so your application has to be deployed somewhere to start tracking its performance. For example, when deploying on Heroku, you may set Skylight’s token using the following command:

$ heroku config:add SKYLIGHT_AUTHENTICATION='123abc'

This page provides the complete overview of Skylight’s integration process with other platforms and explains how to track services outside of Rails, like Net::HTTP, Moped, Redis, and others.

N+1 Query Problem

Once you are done setting everything up, let’s emulate the first and probably the most well-known problem, called an “N+1 query”. To see it in action, list all the albums and their tracks on the main page of the app:

views/albums/index.html.erb

<h1>Albums</h1>
<ul>
  <%= render @albums %>
</ul>

views/albums/_album.html.erb

<li>
  <strong><%= album.title %></strong> by <%= album.musician %>
  <ul>
    <% album.songs.each do |song| %>
      <li><%= song.title %> (<%= song.duration %>s)</li>
    <% end %>
  </ul>
</li>

Boot the server and navigate to http://localhost:3000. Inside the terminal you’ll see output similar to this one:

Album Load (1.0ms)  SELECT "albums".* FROM "albums" ORDER BY published DESC
Song Load (0.0ms)  SELECT "songs".* FROM "songs" WHERE "songs"."album_id" = ?  [["album_id", 301]]
Song Load (1.0ms)  SELECT "songs".* FROM "songs" WHERE "songs"."album_id" = ?  [["album_id", 300]]
Song Load (1.0ms)  SELECT "songs".* FROM "songs" WHERE "songs"."album_id" = ?  [["album_id", 299]]
Song Load (0.0ms)  SELECT "songs".* FROM "songs" WHERE "songs"."album_id" = ?  [["album_id", 298]]
....
.... many similar stuff goes here

Basically, for each parent record (the album) we send a separate query to fetch its children (songs) and effectively spam the database. Deploy your application to a production environment, open its main page, and then after a couple of minutes refresh Skylight’s dashboard.

The dashboard itself is really nice and neat, and you can read this document to get to know it better and learn about Skylight’s main concepts. It presents information about requests per minute, typical and problem response times, as well as general information about endpoints. In our case, the endpoint will be the AlbumsController#create method. At the bottom of the page you can choose the time range to display – that’s pretty much it.

nplusone-1

The red database icon near the endpoint’s name means that it has a number of repeating SQL queries, whereas the pie indicates it has high object allocations. Also note the “Agony” column that is being calculated by Skylight’s special algorithm to find out which endpoint is having the most adverse affect on the app’s users. Suppose you have two endpoints: one has the response time of 600ms but receives loads of requests, whereas the second one has the response time of 3s but has nearly no requests. Obviously, 600ms is much better than 3s, but as long as the first endpoint gets way more requests, it should be top priority to further reduce its response time.

Now if you click on the AlbumsController#create endpoint, you’ll see something like this:

nplusone-2

Those two cycled red arrows near the green SQL row means that the query is not optimal and most likely has a N+1 problem (though it’s not always the case and in the later section we’ll see an example on this).

So, the fix for this problem is very simple. All you have to do is implement a so-called “eager loading” that greatly reduces the number of queries. This is done by employing the includes method:

albums_controller.rb

[...]
def index
  @albums = Album.includes(:songs)
end
[...]

Now in the terminal you’ll see

Album Load (1.0ms)  SELECT "albums".* FROM "albums"
Song Load (13.0ms)  SELECT "songs".* FROM "songs" WHERE "songs"."album_id" IN (202, 203, 204, 205, 206, ...)

which if of course much better. After deploying the updated version of the app to the server, Skylight no longer reports that problem:

nplusone-gone

Great!

Enabling Fragment Caching

Another very common technique to speed up your application is enabling caching. In this demo we’ll utilize the nested fragment caching method. First, enable it for the parent collection:

views/albums/index.html.erb

<%= render @albums, cached: true %>

Next for the children records:

views/albums/_album.html.erb

<li>
  <strong><%= album.title %></strong> by <%= album.musician %>
  <ul>
    <% album.songs.each do |song| %>
      <% cache song do %>
        <li><%= song.title %> (<%= song.duration %>s)</li>
      <% end %>
    <% end %>
  </ul>
</li>

Don’t forget that you have to set up caching on your server in order for this to work properly. As for Heroku, the setup is very simple. Enable the Memcachier addon (the free version):

$ heroku addons:create memcachier:dev

Drop in the Dalli gem

Gemfile

[...]
gem 'dalli'
[...]

install it

$ bundle install

and tweak the configuration for the production environment:

config/environment/production.rb

[...]
config.cache_store = :dalli_store,
    (ENV["MEMCACHIER_SERVERS"] || "").split(","),
    {:username => ENV["MEMCACHIER_USERNAME"],
     :password => ENV["MEMCACHIER_PASSWORD"],
     :failover => true,
     :socket_timeout => 1.5,
     :socket_failure_delay => 0.2,
     :down_retry_delay => 60
    }
[...]

After deploying to production, Skylight will show additional information about working with the cache store, while response time will notably decrease:

cache

Grouping Transactions

Another less common technique to boost app performance is grouping multiple transactions into one. Suppose that users can add additional albums into the application by uploading a CSV file. Of course, we won’t perform the actual parsing of the file – let’s just mimic it by introducing a simple cycle:

albums_controller.rb

  [...]
  def create
    50.times do
      Album.create({title: Faker::Book.title, musician: Faker::StarWars.character})
    end
    redirect_to root_path
  end
  [...]

Add the route

config/routes.rb

[...]
resources :albums, only: [:create]
[...]

and place the button on the main page to run the import process:

views/albums/index.html.erb

[...]
<%= link_to 'add more albums', albums_path, method: :post %>
[...]

When this button is pressed you’ll see that each operation is done in its own transaction (yes, Yoda is now a musician):

(0.0ms)  begin transaction
SQL (4.0ms)  INSERT INTO "albums" ("title", "musician", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "The Far-Distant Oxus"], ["musician", "Yoda"], ["created_at", 2016-09-13 13:09:29 UTC], ["updated_at", 2016-09-13 13:09:29 UTC]]
(81.0ms)  commit transaction

We spend 4ms for the INSERT operation whilst the transaction’s commitment lasts 20 times more! This is the Skylight’s visualization:

no_transaction

Instead, it’s better to group all these INSERT instructions into one transaction and apply it only once:

albums_controller.rb

  [...]
  def create
    Album.transaction do
      50.times do
        Album.create({title: Faker::Book.title, musician: Faker::StarWars.character})
      end
    end
    redirect_to root_path
  end
  [...]

Now the output is:

(1.0ms)  begin transaction
SQL (1.0ms)  INSERT INTO "albums" ("title", "musician", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "A Handful of Dust"], ["musician", "Yoda"], ["created_at", 2016-09-13 13:14:26 UTC], ["updated_at", 2016-09-13 13:14:26 UTC]]
...
many other INSERTs here
...
(133.0ms)  commit transaction

That’s also better when you wish to abort the import process if an error is raised, as the transaction cannot be partly applied.

Here is the graphical representation:

with_transaction

Those red cycled arrows are still there because we really do have queries performed in a cycle.

Adding an Index

Another very simple, but sometimes overlooked performance optimization technique is the addition of a database index. It is absolutely necessary to make all your columns with foreign and primary keys indexed; columns with boolean values (like admin, banned, or published) are good candidates as well. The lol_dba gem can scan your models and present a list of columns that should be indexed.

Let’s introduce a new published column that says whether an album was already released or not:

$ rails g migration add_published_to_albums published:boolean

Modify migration a bit

def change
  add_column :albums, :published, :boolean, default: false
end

and apply it:

$ rails db:migrate

“Publish” 20 random albums:

db/seeds.rb

Album.all.shuffle.take(20).each {|a| a.toggle!(:published)}

$ rails db:seed

Now, let’s use this new column to either display a list of released albums or a full ordered list:

albums_controller.rb

def index
  if params[:published] == 't'
    @albums = Album.includes(:songs).where(published: true)
  else
    @albums = Album.includes(:songs).order('published DESC')
  end
end

Here are the results from Skylight’s dashboard:

published_no_index

Of course, as you’ve guessed, the published column is a great candidate for adding an index, so let’s do it now:

$ rails g migration add_missing_index_to_albums

migrations/xyz_add_missing_index_to_albums.rb

[...]
def change
  add_index :albums, :published
end
[...]

Apply the migration:

$ rails db:migrate

and observe the results on Skylight:

published_index

The performance boost may not be that high but when performing many operations involving a column, but the presence of an index can make a big difference.

Conclusion

In this article we’ve discussed Skylight – a smart profiler for Rails, Sinatra, and Grape apps. While exploring its main features we had a chance to take a look at some of the most common performance issues in Rails apps. Of course, our sample application is not very representative, but you’ve got at least a basic idea of what Skylight is capable of, so be sure to try it for a real project.

Hopefully, you enjoyed reading this article and now will care a bit more about the performance of your apps. I thank you for staying with me. Until next time!

Frequently Asked Questions (FAQs) about Tracking Common Rails Performance Issues with Skylight

What is Skylight and how does it help in tracking Rails performance issues?

Skylight is a smart profiler for Ruby on Rails applications. It provides detailed insights into the performance of your Rails application, helping you identify and resolve issues that may be slowing down your app. Skylight offers a user-friendly interface that visualizes your app’s performance data, making it easier to understand and analyze. It tracks a variety of metrics, including response time, database queries, and more, allowing you to pinpoint exactly where performance issues are occurring.

How do I install and set up Skylight in my Rails application?

Installing Skylight in your Rails application is a straightforward process. First, add the Skylight gem to your Gemfile and run the bundle install command. Next, you’ll need to sign up for a Skylight account and create a new application within the Skylight dashboard. You’ll be provided with an authentication token, which you’ll need to add to your application’s configuration file. Once this is done, Skylight will start tracking your application’s performance.

What kind of performance issues can Skylight help me identify?

Skylight can help you identify a wide range of performance issues in your Rails application. This includes slow database queries, inefficient rendering, excessive garbage collection, and more. By providing a detailed breakdown of each request, Skylight allows you to pinpoint exactly where these issues are occurring, making it easier to resolve them and improve your application’s overall performance.

How does Skylight visualize performance data?

Skylight visualizes performance data using a variety of charts and graphs. This includes a timeline view that shows the duration of each request, a histogram of response times, and a breakdown of database queries. These visualizations make it easy to understand your application’s performance at a glance and identify any potential issues.

Can Skylight help me improve the performance of my Rails application?

Absolutely. By providing detailed insights into your application’s performance, Skylight can help you identify areas where your app could be optimized. This could involve optimizing database queries, reducing memory usage, or improving the efficiency of your code. By addressing these issues, you can significantly improve the performance of your Rails application.

Is Skylight suitable for large-scale Rails applications?

Yes, Skylight is designed to handle the demands of large-scale Rails applications. It can track performance across multiple servers and environments, making it a suitable choice for both small and large applications.

How does Skylight compare to other Rails performance tracking tools?

Skylight stands out for its user-friendly interface, detailed visualizations, and smart profiling capabilities. While other tools may offer similar features, Skylight’s focus on simplicity and ease of use makes it a popular choice among Rails developers.

Can I use Skylight with other programming languages or frameworks?

Currently, Skylight is specifically designed for Ruby on Rails applications. However, the team behind Skylight is always working on updates and improvements, so it’s possible that support for other languages or frameworks may be added in the future.

Is there a cost associated with using Skylight?

Skylight offers a free tier that includes a limited set of features. For access to more advanced features, there are several paid plans available. The cost of these plans varies depending on the size and needs of your application.

How can I get started with Skylight?

To get started with Skylight, you’ll need to sign up for an account on the Skylight website. From there, you can create a new application, install the Skylight gem in your Rails application, and start tracking your app’s performance.

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

GlennGperformanceRuby on Rails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week