Working with Rails for some time you start nitpicking on how to improve it. This is a first in the series of articles regarding on how to improve (even marginally) Rails's performance.
I'll focus on a bunch of gems that speed up, in some cases considerably, small parts of Rails, like the html escaping, the String#blank? and JSON utils.
Benchmarking methodology
Methodology is a strong word for just running a couple of times in the console wrk
but I'm not searching for the holy grail here, just to get a raw idea.
I switched from the old apache ab to wrk :
wrk is a modern HTTP benchmarking tool capable of generating significant
load when run on a single multi-core CPU.
wrk -t10 -c10 -d10s http://localhost:3000
This runs a benchmark for 10 seconds, using 10 threads, and keeping 50 HTTP connections open i.e. this should suffice. Just remember to benchmark on your actual app to see the real improvements.
The escape_utils gem
Just faster all html escaping via the lovely escape_utils gem. In order to use it in Rails one needs to add an initializer that patches things up:
begin require 'escape_utils/html/rack' # to patch Rack::Utils require 'escape_utils/html/erb' # to patch ERB::Util require 'escape_utils/html/cgi' # to patch CGI require 'escape_utils/html/haml' # to patch Haml::Helpers rescue LoadError Rails.logger.info 'Escape_utils is not in the gemfile' end
The logic to test it:
def escape_utils @escape_me = <<-HTML <body class="application articles_show"> <!-- Responsive navigation ==================================================== --> <div class="container"> <nav id="nav"> <ul> <li><a href="/"><i class="ss-standard ss-home"></i>home</a></li> <li><a href="/home/about"><i class="ss-standard ss-info"></i>about</a></li> <li><a href="/contact"><i class="ss-standard ss-ellipsischat"></i>contact</a></li> <li><a href="/home/projects"><i class="ss-standard ss-fork"></i>projects</a></li> <li><a href="/tags"><i class="ss-standard ss-tag"></i>tags</a></li> <li><a href="/articles?query=code"><i class="ss-standard ss-search"></i>search</a></li> </ul> </nav> <a href="#" class="ss-standard ss-list" id="nav-toggle" aria-hidden="true"></a> HTML render inline: "Hello world <%= @escape_me %>" end
With standard Rails:
Running 10s test @ http://localhost:3000/sidechannels/bench 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 35.40ms 3.55ms 64.70ms 91.98% Req/Sec 142.19 11.68 164.00 83.12% 2837 requests in 10.00s, 4.92MB read Requests/sec: 283.61 Transfer/sec: 503.34KB
With the escape_utils gem:
Running 10s test @ http://localhost:3000/sidechannels/bench 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 34.06ms 3.89ms 63.92ms 89.10% Req/Sec 148.65 13.36 180.00 75.94% 2960 requests in 10.00s, 5.46MB read Requests/sec: 295.98 Transfer/sec: 558.72KB
The fast_blank gem
Living under the impression that the blank? method is too slow? say no more and just try the fast_blank gem!
Just add gem 'fast_blank'
to your Gemfile and this should speed up quite nicely the String#blank? method as described in this article . For testing I just added this code:
fast_blank is a simple extension which provides a fast implementation of active support's string#blank? function
def fast_blank_test n = 1000 strings = [ "", "\r\n\r\n ", "this is a test", " this is a longer test", " this is a longer test this is a longer test this is a longer test this is a longer test this is a longer test" ] Benchmark.bmbm do |x| strings.each do |s| x.report("Fast Blank #{s.length} :") do n.times { s.blank? } end end end render nothing: true end
With standard Rails:
Running 10s test @ http://localhost:3000/sidechannels/bench 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 1.40s 207.72ms 1.58s 92.68% Req/Sec 3.10 2.11 6.00 53.66% 69 requests in 10.01s, 33.08KB read Requests/sec: 6.90 Transfer/sec: 3.31KB
With the fast_blank gem:
Running 10s test @ http://localhost:3000/sidechannels/bench 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 1.33s 179.56ms 1.41s 93.33% Req/Sec 3.07 0.80 4.00 40.00% 72 requests in 10.00s, 34.52KB read Requests/sec: 7.20 Transfer/sec: 3.45KB
The oj gem
# oj gem gem 'oj' gem 'oj_mimic_json' # we need this for Rails 4.1.x
The test logic is simple, just serialize all articles into JSON:
class SidechannelsController < ApplicationController def oj render json: Article.all end end
With standard Rails serializers:
Running 10s test @ http://localhost:3000/sidechannels/bench 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 108.37ms 5.12ms 134.90ms 83.33% Req/Sec 45.76 3.60 55.00 57.69% 922 requests in 10.00s, 57.41MB read Requests/sec: 92.17 Transfer/sec: 5.74MB
With oj gem:
Running 10s test @ http://localhost:3000/sidechannels/bench 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 78.06ms 4.43ms 92.83ms 81.31% Req/Sec 63.64 5.33 71.00 64.49% 1277 requests in 10.00s, 79.83MB read Requests/sec: 127.65 Transfer/sec: 7.98MB
Using jemalloc
OK, this is not really a gem, if you want to dig into it then do check out my gist . On initial testing it won't yield much performance gains, at least for my use case.
note: it will be included by default in Ruby at some point.
update: do try the jemalloc gem by kzk:
gem install jemalloc je -v rails s
Dig into your Rails app
Fear not and use MiniProfiler with the awesome FlameGraphs by Sam Saffron.
Conclusion
Depending on what your app is doing you might want to add to your Gemfile some of these gems, I usually add them all just for good measure (you might want to check your RAM usage and have a full test suite before doing this though).
The oj gem is just great for a Rails based JSON API where you can drop the views and just serialize using representers or your pattern of choice.