Geração de XML em Ruby: Builder vs Nokogiri vs Libxml vs Ox

Rafael Barbolo
Neuronio BR
Published in
4 min readJul 20, 2018

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.

Já escrevi sobre minha primeira missão: encontrar a forma mais eficiente de renderizar JSON nessa aplicação. Agora a missão é encontrar o renderizador de XML mais eficiente.

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

Ox e Libxml são as melhores opções. As duas consomem pouca memória e são muito rápidas. A geração de XML com Ox chega a ser 25% mais rápida do que com Libxml, mas aloca cerca do dobro de memória.

Desempenho para renderização de XML

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

As opções de renderização avaliadas foram:

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 os tempos de execuções das renderizações de cada artigo para XML e também para medir quanta memória é alocada durante uma renderização.

         ╔═════════════╦═════════╦═════════╗
║ Render ║ TEMPO ║ MEMÓRIA ║
╔════════╬═════════════╬═════════╬═════════╣
║ post1 ║ ║ ║ ║
║ ║ RABL ║ 3,29 ms ║ 345 K ║
║ ║ toXML ║ 2,58 ms ║ 270 K ║
║ ║ Builder ║ 0,93 ms ║ 99 K ║
║ ║ Nokogiri ║ 0,78 ms ║ 75 K ║
║ ║ StringC ║ 0,23 ms ║ 36 K ║
║ ║ StringI ║ 0,19 ms ║ 26 K ║
║ ║ Libxml ║ 0,19 ms ║ 12 K
║ ║ Ox 0,16 ms ║ 24 K ║
╠════════╬═════════════╬═════════╬═════════╣
║ post2 ║ ║ ║ ║
║ ║ RABL ║ 98,0 ms ║ 9,18 M ║
║ ║ toXML ║ 88,2 ms ║ 8,13 M ║
║ ║ Builder ║ 58,9 ms ║ 5,01 M ║
║ ║ Nokogiri ║ 33,1 ms ║ 2,76 M ║
║ ║ StringC ║ 16,6 ms ║ 1,10 M ║
║ ║ StringI ║ 12,7 ms ║ 0,89 M ║
║ ║ Libxml 9,6 ms 0,21 M
║ ║ Ox ║ 10,6 ms ║ 0,40 M ║
╠════════╬═════════════╬═════════╬═════════╣
║ post3 ║ ║ ║ ║
║ ║ RABL ║ 3,42 s ║ 291,3 M ║
║ ║ toXML ║ 3,32 s ║ 265,5 M ║
║ ║ Builder ║ 2,41 s ║ 187,9 M ║
║ ║ Nokogiri ║ 1,30 s ║ 144,7 M ║
║ ║ StringC ║ 0,69 s ║ 117,7 M ║
║ ║ StringI ║ 0,68 s ║ 114,8 M ║
║ ║ Libxml ║ 0,40 s ║ 5,2 M
║ ║ Ox0,31 s ║ 10,0 M ║
╚════════╩═════════════╩═════════╩═════════╝

Em todos os casos, Libxml e Ox foram as melhores opções. Elas tiveram desempenhos muito próximos em tempo de processamento. Em alocação de memória, a Libxml foi significativamente mais eficiente.

A escolha para a API que estou otimizando foi a Libxml. Meus cenários são parecidos com os dos posts 1 e 2, e a eficiência na alocação de memória me surpreendeu muito.

Destaques revelados na análise de desempenho:

  • Libxml usa cerca de 50x menos memória que RABL
  • Ox chega a ser cerca de 20x mais rápido que RABL
  • Libxml e Ox (C) são mais eficientes do que manipular String em Ruby
  • Interpolação é mais eficiente do que concatenação de Strings em Ruby

Código final

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

Controller

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

Renderizador

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

--

--