Comparação de desempenho em geração de JSON em Ruby

Nos últimos dias tenho otimizado uma aplicação Ruby on Rails de uma API que responde requisições nos formatos JSON ou XML.

Identifiquei ocorrências de memory bloat na geração de XML em alguns casos raros e por isso estou investigando opções mais eficientes de gerar JSON e XML. Atualmente essa API utiliza a gem RABL para renderizar os documentos de resposta.

Minha primeira missão é encontrar a solução de renderização de JSON em Ruby com o melhor desempenho possível. Estou disposto a sacrificar simplicidade, estética, padronização ou manutenabilidade de código.

Essa API que está sendo otimizada recebe algumas dezenas de milhões de requisições por mês, então desempenho é a palavra-chave aqui.

TL;DR

A melhor solução que encontrei foi usar a biblioteca Oj para manipular JSONs e não usar nenhuma DSL de renderização, ou seja, para renderizar basta construir um Hash a partir do objeto Ruby e convertê-lo para JSON.

Desempenho para converter um Hash para JSON

A primeira análise de desempenho será entre algumas bibliotecas supostamente eficientes que permitem converter um Hash de Ruby para um JSON em texto:

Para facilitar essa análise, a gem MultiJSON será usada. Ela oferece uma interface comum para acessar as bibliotecas de transformação de JSON.

Na análise serão usados 3 tipos de Hash para a conversão.

  1. hash1 - muito simples com poucas chaves e tipos básicos.
  2. hash2 - quantidade razoável de chaves, "árvore" de até 3 níveis, valores do tipo Array de Hashes, valor com String muito longa (simulando o conteúdo de um arquivo, por exemplo). Suponho ser o Hash mais comum de se encontrar em aplicações Ruby.
  3. hash3 - mil chaves, "árvore" de até 10 níveis, valores do tipo hash1 e hash2. Exemplo de um caso extremo de Hash.

Resultados

Para análise dos resultados, um script foi executado para medir tempo de execução de conversões de Hash para JSON e também para medir quanta memória é alocada durante uma conversão.

         ╔════════════╦═════════╦═════════╗
║ 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
╚════════╩════════════╩═════════╩═════════╝

Em todos os casos, a biblioteca Oj teve o melhor desempenho no tempo para converter um Hash para JSON. Nenhuma biblioteca apresentou alocação de memória significativamente diferente das outras.

Desempenho para renderização de JSON

A renderização de um JSON tipicamente envolve a interpretação de regras de conversão (template) de um objeto Ruby de entrada para um JSON de saída.

O renderizador de JSON mais popular em Ruby é o JBuilder. Um que foi popular no passado é o RABL. E uma abordagem menos convencional que também foi avaliada consiste na construção de um Hash a partir de um objeto Ruby seguida da conversão direta desse Hash para JSON.

Na análise, o objeto Ruby a ser renderizado representa um Artigo (post) com múltiplos Comentários. Foram feitas análises de 3 tipos de artigos:

  1. post1 - artigo com aproximadamente mil caracteres e 10 comentários simples.
  2. post2 — artigo com aproximadamente 1 milhão de caracteres, 200 comentários, e cada comentário com mais de mil caracteres.
  3. post3 — artigo com aproximadamente 10 milhões de caracteres, 5 mil comentários, e cada comentário com mais de 10 mil caracteres.

Resultados

Para análise dos resultados, um script foi executado para medir tempo de execução das renderizações de cada artigo para JSON e também para medir quanta memória é alocada durante uma renderização.

         ╔═════════════╦═════════╦═════════╗
║ 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
╚════════╩═════════════╩═════════╩═════════╝

Em todos os casos, a abordagem Hash > JSON (converter o objeto Ruby para um Hash e em seguida converter o Hash para JSON) teve o melhor desempenho tanto em tempo quanto em alocação de memória.

O que fica de mais relevante para mim é que a abordagem Hash > JSON utiliza cerca de 6 vezes menos memória que o RABL e 3 vezes menos memória que o Jbuilder.

Código final

Ainda estou refletindo sobre a organização do código final. Por ora, uma sugestão é:

Controller

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

Renderizador

# 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

Ambiente utilizado nos testes

O tempo de execução de cada teste é uma média dos tempos de execução de mil execuções de cada cenário.

O código fonte necessário para reproduzir os códigos está presente neste gist. O ambiente utilizado nos testes está descrito a seguir:

  • Computador: MacBook Pro 15" 2017
Like what you read? Give Rafael Barbolo a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.