The performance of .to_json in Rails sucks and there’s nothing you can do about it
Originally posted on the Agworld Developers Blog, hosted on Tumblr, on February 9, 2013.
As the application we’re working on 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
endrequire ‘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, although I’m pretty sure it’ll be a hard fight to get any kind of fix through. I reckon the original bug 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!
This article was originally posted on the Agworld Developers Blog, hosted on Tumblr, on February 9, 2013, and is almost certainly out of date by now.