DEV Community

Cover image for Implementando árvores em Elixir
Vinicius Luiz for Ingresse

Posted on • Updated on

Implementando árvores em Elixir

Problema

Com o aumento na demanda de requisições feitas para a API da Ingresse alguns processos se tornaram um tipo de gargalo. O exemplo que usarei durante este artigo será o nosso processo de definir se a sessão (data/hora) de um evento está disponível para venda ou não.

Cada requisição feita pelo usuário a nossa API faz uma grande quantidade de buscas no nosso banco de dados e no Redis. São operações simples e que não deveriam ser muito custosas, porém são muitos dados que temos que buscar, e isso em grande escala de requests se torna bastante custoso.


Prova de conceito

Desde o início dos estudos para completar esta tarefa de refatoração nós optamos por dividi-la em duas partes, sendo elas:


Construir a árvore completa de um evento

A ideia nesta etapa é fazer todo o processo de buscas necessárias nos bancos de dados, percorrer esses dados e construir a árvore que será consumida por outros processos.

Aqui foram criados os dois principais módulos (ou classes para quem estiver mais habituado com linguagens OOP) que são os que representam os nós e a árvore em si. Esses dois módulos são uma implementação simples de uma árvore e com eles nós conseguimos criá-la a partir de uma estrutura de evento que é a entidade carregada do banco de dados com suas respectivas associações como sessões, grupo de ingressos e ingressos.

node.ex
tree.ex

Calcular os status de sessões a partir de uma árvore já construída

Esta etapa é a responsável por consumir uma árvore já criada e percorrer sobre os nós dela para calcular o status de cada um. Neste ponto nós usaremos a árvore criada pela etapa anterior a esta, assim, conseguiremos percorrer por todos os nós dentro da árvore utilizando o algoritmo Postorder Traversal.

Para fazer os cálculos dos status nós decidimos utilizar pesos neles, o que significa que, cada status terá um peso de acordo com o tipo de status que ele é, com o intuito de podermos definir qual status é mais relevante comparado com outros.

É importante deixar claro que nós temos dois grupos de status que são: Disponível e Indisponível.
Dentro do grupo Indisponível nós temos outros tipos de status, que podem ser: sem estoque, vendas não iniciadas, encerrado, etc, e esses sim tem o peso mencionado acima.

Nesta estapa também decidimos usar uma tabela ETS para guardar o status de cada nó e dar como resposta para nosso cliente.


Recursive Traversal

Depois de termos concluido essas duas etapas, começamos a pesquisar qual seria a melhor forma de construirmos a árvore e evitar ter que construí-la toda vez que alguém fizesse algum request para nosso serviço. Diante disto, pensamos em cachea-la no Redis como JSON, porém algumas árvores seriam muito grandes e parsear o JSON sempre seria algo custoso, depois de algumas pesquisas, achamos algo que poderia funcionar para a gente, em Elixir podemos facilmente transformar uma struct em um binário usando a função term_to_binary do Erlang, e tendo esse binário podemos usar um algoritmo de hash, assim teremos a hash de uma estrutura válida para guardarmos em algum tipo de cache e então reconstruir a árvore sem executar muitos processos usando a função binary_to_term do Erlang.


# iex

iex(1)> tree = Tree.build(event)
%Tree.Node{
  childs: [
    %Tree.Node{
      childs: [
        %Tree.Node{
          childs: nil,
          name: 55089
        }
      ],
      name:26190
     },
     %Tree.Node{
       childs: [],
       name: 26230
     }
  ],
  name: "Teste arena alterado"
}

iex(2)> binary = tree |> :erlang.term_to_binary()
<<131, 116, 0, 0, 0, 3, 100, 0, 10, 95, 95, 115, 116, 114, 117, 99, 116, 95, 95, 100, 0, 16, 69, 108, 105, 120, 105, 114, 46, 84, 114, 101, 101, 46, 78, 111, 100, 101, 100, 0, 6, 99, 104, 105, 108, 100, 115, 108, 0, 0, ...>>


iex(3)> hash = binary |> Base.encode64()
"g3QAAAADZAAKX19zdHJ1Y3RfX2QAEEVsaXhpci5UcmVlLk5vZGVkAAZjaGlsZHNsAAAAAnQAAAADZAAKX19zdHJ1Y3RfX2QAEEVsaXhpci5UcmVlLk5vZGVkAAZjaGlsZHNsAAAAAXQAAAADZAAKX19zdHJ1Y3RfX2QAEEVsaXhpci5UcmVlLk5vZGVkAAZjaGlsZHNkAANuaWxkAARuYW1lYgAA1zFqZAAEbmFtZWIAAGZOdAAAAANkAApfX3N0cnVjdF9fZAAQRWxpeGlyLlRyZWUuTm9kZWQABmNoaWxkc2pkAARuYW1lYgAAZnZqZAAEbmFtZW0AAAAUVGVzdGUgYXJlbmEgYWx0ZXJhZG8="
Enter fullscreen mode Exit fullscreen mode

Tendo esta hash, podemos voltar à estrutura usando o caminho inverso.

hash = "g3QAAAADZAAKX19zdHJ1Y3RfX2QAEEVsaXhpci5UcmVlLk5vZGVkAAZjaGlsZHNsAAAAAnQAAAADZAAKX19zdHJ1Y3RfX2QAEEVsaXhpci5UcmVlLk5vZGVkAAZjaGlsZHNsAAAAAXQAAAADZAAKX19zdHJ1Y3RfX2QAEEVsaXhpci5UcmVlLk5vZGVkAAZjaGlsZHNkAANuaWxkAARuYW1lYgAA1zFqZAAEbmFtZWIAAGZOdAAAAANkAApfX3N0cnVjdF9fZAAQRWxpeGlyLlRyZWUuTm9kZWQABmNoaWxkc2pkAARuYW1lYgAAZnZqZAAEbmFtZW0AAAAUVGVzdGUgYXJlbmEgYWx0ZXJhZG8="

decoded_hash = Base.decode64!(hash)
<<131, 116, 0, 0, 0, 3, 100, 0, 10, 95, 95, 115, 116, 114, 117, 99, 116, 95, 95, 100, 0, 16, 69, 108, 105, 120, 105, 114, 46, 84, 114, 101, 101, 46, 78, 111, 100, 101, 100, 0, 6, 99, 104, 105, 108, 100, 115, 108, 0, 0, ...>>

structure = :erlang.binary_to_term(decoded_hash)
%Tree.Node{
  childs: [
    %Tree.Node{
      childs: [
        %Tree.Node{
          childs: nil,
          name: 55089
        }
      ],
      name:26190
     },
     %Tree.Node{
       childs: [],
       name: 26230
     }
  ],
  name: "Teste arena alterado"
}
Enter fullscreen mode Exit fullscreen mode

Resultados Obtidos

Aqui devemos considerar os seguintes atributos

ips - iterations per second: a quantidade de iterações que podemos executar a cada segundo.
median: a mediana dos tempos de execução calculada no benchmark

Events

Id Session Qty. Groups Qty. Tickets Qty.
34392 1 3 12
31627 11 11 746
34477 2020 349 361

Queries

Performance Statistics

Name ips average deviation median 99th %
repo.by_id.34392 1.54 648.62 ms ±6.75% 631.75 ms 756.44 ms
repo.by_id.31627 1.20 833.57 ms ±9.18% 826.11 ms 930.56 ms
repo.by_id.34477 1.09 913.59 ms ±0.41% 913.63 ms 919.02 ms

Memory Usage Statistics

Name average deviation median 99th %
repo.by_id.34392 0.111 MB ±0.09% 0.111 MB 0.111 MB
repo.by_id.31627 1.08 MB ±0.30% 1.08 MB 1.08 MB
repo.by_id.34477 3.76 MB ±0.13% 3.76 MB 3.77 MB

Builds

Performance Statistics

Name ips average deviation median 99th %
tree.build.34392 215.20 K 4.65 μs ±425.27% 3.46 μs 19.62 μs
tree.build.31627 5.94 K 168.23 μs ±60.08% 137.62 μs 654.07 μs
tree.build.34477 1.98 K 506.14 μs ±59.40% 392.54 μs 1864.66 μs

Memory Usage Statistics

Name Memory usage
tree.build.34392 4.66 KB
tree.build.31627 188.73 KB - 40.53x memory usage +184.08 KB
tree.build.34477 429.12 KB - 92.16x memory usage +424.46 KB

Recursive Traversal

Performance Statistics

Name ips average deviation median 99th %
recursive.traversal.tree.34392 101.38 K 9.86 μs ±101.58% 9.18 μs 21.58 μs
recursive.traversal.tree.31627 10.18 K 98.19 μs ±51.78% 86.70 μs 264.75 μs
recursive.traversal.tree.34477 0.84 K 1188.36 μs ±3.93% 1183.79 μs 1387.74 μs

Memory Usage Statistics

Name Memory usage
recursive.traversal.tree.34392 6.18 KB
recursive.traversal.tree.31627 43.16 KB - 6.98x memory usage +36.98 KB
recursive.traversal.tree.34477 1010.42 KB - 163.51x memory usage +1004.24 KB

Tree to binary

Performance Statistics

Name ips average deviation median 99th %
tree.to.binary.34392 177.49 K 5.63 μs ±470.90% 4.18 μs 23.35 μs
tree.to.binary.31627 5.89 K 169.79 μs ±32.19% 152.80 μs 411.31 μs
tree.to.binary.34477 2.21 K 451.81 μs ±60.66% 344.38 μs 1628.34 μs

Memory Usage Statistics

Name Memory usage
tree.to.binary.34392 48 B
tree.to.binary.31627 48 B - 1.00x memory usage +0 B
tree.to.binary.34477 48 B - 1.00x memory usage +0 B

Binary to Hash

Performance Statistics

Name ips average deviation median 99th %
binary.to.hash.34392 22191.97 0.0451 ms ±53.24% 0.0379 ms 0.132 ms
binary.to.hash.31627 475.17 2.10 ms ±34.34% 1.83 ms 4.86 ms
binary.to.hash.34477 298.97 3.34 ms ±27.40% 3.02 ms 6.71 ms

Memory Usage Statistics

Name Memory usage
binary.to.hash.34392 304 B
binary.to.hash.31627 376 B - 1.24x memory usage +72 B
binary.to.hash.34477 376 B - 1.24x memory usage +72 B

Serialize

Performance Statistics

Name ips average deviation median 99th %
tree.serialize.34392 21510.67 0.0465 ms ±78.93% 0.0374 ms 0.149 ms
tree.serialize.31627 520.52 1.92 ms ±33.44% 1.69 ms 4.42 ms
tree.serialize.34477 280.24 3.57 ms ±30.92% 3.13 ms 7.16 ms

Memory Usage Statistics

Name Memory usage
tree.serialize.34392 424 B
tree.serialize.31627 424 B - 1.00x memory usage +0 B
tree.serialize.34477 424 B - 1.00x memory usage +0 B

Com esses resultados temos apenas uma preocupação no momento que é a quantidade de memória utilizada no algoritmo recursive traversal, por ser uma função recursiva nós já tinhamos em mente que ela usaria bastante memória e isso pode nos trazer problemas em um cenário de um evento gigante, o evento exemplo 34477 já é um evento considerado grande para nós devido a quantidade de ingressos e sessões que ele possui. Talvez iremos procurar resoluções para esse possível problema e acredito que se conseguirmos achar algo iremos trazer em um futuro post.


Conclusão

No final dessa POC conseguimos comprovar que nós conseguimos ter o mesmo cálculo que temos hoje, porém utilizando a estrutura de árvore que montamos para esse caso que se provou muito mais eficiente que o que temos hoje, pois os resultados que tivemos se provaram muito eficientes para diversos casos que temos hoje em nosso sistema.


Agradecimentos especiais ao Arthur por me orientar e acompanhar durante o processo de desenvolvimento!

Espero que gostem do post.

Até uma próxima!

Top comments (0)