Performance comparison for JSON generation in Ruby

This is the translation of this article originally posted by Rafael Barbolo.

During the last days, I've been optimizing a Ruby on Rails application of an API that responds to requests in JSON or XML formats.

I've identified memory bloats in some rare cases of XML generation and that’s why I’m investigating efficient ways to generate JSON and XML. Currently, this API uses the RABL gem to render response documents.

My first mission is to find a rendering solution with the best performance. I’m willing to sacrifice simplicity, aesthetics, standardization and code maintenance.

The API that is being optimized gets tens of millions of requests per month, so performance is key here.

TL;DR

The best solution I've found is using the Oj library to manipulate JSONs and not using any DSL renderer. In other words, I build a Hash from a Ruby object that gets converted directly to JSON.

Performance to convert a Hash to JSON

The first performance analysis will compare some supposedly efficient libraries that allow you to convert a Ruby Hash to a JSON:

In order to make this analysis easier, the gem MultiJSON will be used. It offers a common interface to the JSON transformation libraries.

The conversion of 3 types of Hash objects will be evaluated:

  1. hash1- very simple with few keys and basic types.
  2. hash2 - reasonable number of keys, “trees” with up to 3 nesting levels, values of Array of Hashes type, value with long String (simulating the contents of a file, for example). I suppose this is the most common Hash in typical Rails applications.
  3. hash3 - a thousand keys, “tree” with up to 10 nesting levels, values with type hash1 and hash2. This is an example of an extreme case.

Results

To evaluate the results, a script was used in order to measure how long it takes to convert a Hash to a JSON and also how much memory is allocated during the conversion process.

         ╔════════════╦═════════╦═════════╗
║ BIBLIOTECA ║ TEMPO ║ MEMÓRIA ║
╔════════╬════════════╬═════════╬═════════╣
║ hash1 ║ ║ ║ ║
║ ║ JSON ║ 0,01 ms ║ 1,5 K ║
║ ║ Yajl ║ 0,02 ms ║ 1,0 K ║
║ ║ Oj0,01 ms1,0 K
╠════════╬════════════╬═════════╬═════════╣
║ hash2 ║ ║ ║ ║
║ ║ JSON ║ 0,57 ms ║ 12,2 K ║
║ ║ Yajl ║ 0,39 ms ║ 11,8 K ║
║ ║ Oj0,33 ms11,8 K
╠════════╬════════════╬═════════╬═════════╣
║ hash3 ║ ║ ║ ║
║ ║ JSON ║ 327 ms ║ 6,62 M ║
║ ║ Yajl ║ 235 ms ║ 6,62 M ║
║ ║ Oj235 ms6,62 M
╚════════╩════════════╩═════════╩═════════╝

In all scenarios, the Oj library had the best performance in terms of time to convert a Hash to a JSON. No library presented significantly differences in memory allocation.

Performance for JSON rendering

Rendering a JSON typically involves interpreting conversion rules (in a template) of an input Ruby object to an output JSON.

The most popular JSON renderer in Ruby is JBuilder. In the past, a popular one was RABL. And a less conventional approach that has been evaluated consists of building a Hash from a Ruby object followed by the direct conversion of that Hash to JSON.

In this analysis, the Ruby object to be rendered represents an Article (post) with multiple Comments. 3 types of articles were evaluated:

  1. post1- article with approximately a thousand characters and 10 simple comments.
  2. post2 - article with approximately 1 million characters and 200 comments, each with more than a thousand characters.
  3. post3 - article with approximately 10 million characters and 5 thousand comments, each with more than 10 thousands characters.

Results

To evaluate the results, a script was used in order to measure how long it takes to render each article to JSON and also how much memory is allocated during the rendering process.

         ╔═════════════╦═════════╦═════════╗
║ Render ║ TEMPO ║ MEMÓRIA ║
╔════════╬═════════════╬═════════╬═════════╣
║ post1 ║ ║ ║ ║
║ ║ RABL ║ 0,96 ms ║ 82,7 K ║
║ ║ Jbuilder ║ 0,22 ms ║ 30,1 K ║
║ ║ Hash > JSON0,09 ms10,5 K
╠════════╬═════════════╬═════════╬═════════╣
║ post2 ║ ║ ║ ║
║ ║ RABL ║ 14,3 ms ║ 1,20 M ║
║ ║ Jbuilder ║ 6,8 ms ║ 0,53 M ║
║ ║ Hash > JSON4,6 ms0,19 M
╠════════╬═════════════╬═════════╬═════════╣
║ post3 ║ ║ ║ ║
║ ║ RABL ║ 475 ms ║ 29,4 M ║
║ ║ Jbuilder ║ 321 ms ║ 13,2 M ║
║ ║ Hash > JSON259 ms4,7 M
╚════════╩═════════════╩═════════╩═════════╝

In all scenarios, the Hash > JSON approach (conversion ofthe Ruby object to a Hash and then conversion of the Hash to a JSON) performed best in terms of execution time and memory allocation.

What really draws my attention is that the Hash > JSON approach uses about 6 times less memory than RABL and 3 times less memory than Jbuilder.

Final code

I’m still thinking about the organization of the final code. For now, my suggestion is:

Controller

# app/controllers/posts_controller.rb
respond_to do |format|
format.json { render json: Renderer::Post.json(@post) }
format.xml { ... }
end

Renderer

# app/views-api/renderer/post.rb
class Renderer::Post
def self.json(post)
MultiJson.dump({
'content' => post.content,
'created_at' => post.created_at,
'published' => post.published,
'author' => {
'name' => post.author.name,
'age' => post.author.age,
'email' => post.author.email,
},
'comments' => post.comments.map do |comment|
{
'created_at' => comment.created_at,
'message' => comment.message,
'user' => {
'name' => comment.user.name,
'age' => comment.user.age,
'email' => comment.user.email,
},
'attachment' => comment.attachment,
}
end
})
end
end

Environment used in this analysis

The execution time of each test case is an average of the execution times of one thousand runs of each scenario.

The source code needed to reproduce the scenarios can be found in this gist.
The environment used in the tests is described below:

  • Computer: MacBook Pro 15" 2017