The performance of .to_json in Rails sucks and there’s nothing you can do about it
by Jason Hutchens

As the application we’re working evolves away from a monolithic Rails app and towards a loosely-coupled collection of services, we find that we’re dealing more and more with large JSON blobs.

Recently it became apparent that the performance of our JSON wrangling wasn’t up to snuff. Which was weird, because I proved to myself quite early on that we were most definitely using the json/ext variant of the json gem, which is implemented in C, and should be plenty fast enough.

Just to make sure, I fired up a Rails console and checked. Yep, json/ext was in play. So I then ran an experiment to make sure that it was as fast as I supposed, by writing a simple test:

#!/usr/bin/env ruby

require 'benchmark'

def profile_json(num = 10)
  data = Hash.new
  key = 'aaa'
  1000.times { data[key.succ!] = data.keys }
  times = num.times.map do
    1000 * Benchmark.realtime { data.to_json }
  end
  times.reduce(:+)/num.to_f
end

require 'json/pure'
puts "'json/pure' -> #{profile_json} ms"

require 'json/ext'
puts "'json/ext' -> #{profile_json} ms"

Running this from the command line, outside of rails, convinced me of the speed advantage:

'json/pure' -> 1651.64189338684 ms
'json/ext' -> 55.1620960235596 ms

We should have be getting reasonable JSON performance in Rails. But we weren’t. To investigate further, I ran the same test with rails runner instead (which is pretty much the equivalent of copy-pasting it into the rails console; it basically fires up Rails before executing your code). This is what I got:

'json/pure' -> 1912.54553794861 ms
'json/ext' -> 1919.99847888947 ms

Whaaaa? Abysmal performance. And changing the test to use yajl/json_gem made no difference to performance when the test was performed within our Rails app.

A bit of investigation revealed the culprit. In 2010, in order to fix a low priority bug (that ActiveModel::Errors#to_json generated invalid JSON), the following code was introduced to active_support:

# Hack to load json gem first so we can overwrite its to_json.
begin
  require 'json'
rescue LoadError
end

# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
# their default behavior. That said, we need to define the basic to_json method in all of them,
# otherwise they will always use to_json gem implementation, which is backwards incompatible in
# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
  klass.class_eval do
    # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
    def to_json(options = nil)
      ActiveSupport::JSON.encode(self, options)
    end
  end
end

Mmmm-mmmm, I love me my hacks.

Here, the json gem is shanghaid into loading, even if you are deliberately requiring it after rails when trying to figure out how to make your .to_json performance not suck. Then, the to_json method is patched for all common classes, including Array and Hash, effectively neutering it.

You’ll find this code in active_support/core_ext/object/to_json. If you do anything in your app to cause that file to be parsed (such as, say, using the authlogic gem, like we do, or, say, using active_record) then your JSON performance will be irrevocably poor if you use .to_json on objects to JSONify them.

Yes, I know you can generate JSON in other ways that wouldn’t fall foul of this hackery. But we use .to_json a lot, and it’s not just us; many of the gems that we use also use .to_json. Indeed, there are 40,000 public repositories on GitHub that use .to_json in Ruby code. It’s popular. And gems or frameworks shouldn’t neuter these kinds of things if they can at all help it.

I reported a bug (https://github.com/rails/rails/issues/9212), although I’m pretty sure it’ll be a hard fight to get any kind of fix through. I reckon the original bug (https://rails.lighthouseapp.com/projects/8994/tickets/4890) should have a more specific fix that doesn’t nix JSON performance for everyone. If that’s not possible, then the existing fix should be made optional, with a flag in configuration for disabling it. We’ll see how it goes.

In the meantime we’ve monkeypatched around the problem by applying a similar change as the one in active_support, but this time using multi_json and oj (which is the fastest and the most rails-compatible JSON gem I could find) in concert.

And we’re happy to report that, in initial tests, we’re seeing JSON views render in 8ms instead of 500ms. Good times!

Short URL for this post: http://tmblr.co/Zs13hvdgKo-3
blog comments powered by Disqus