Agrupamentos — pequenos ajustes/tunnings que fazem diferença 2
Seguindo a série de pequenos ajustes e tunnings que fazem a diferença (leia o anterior também :D), nesse aqui vou falar sobre um problema real que tive com relação ao tempo de resposta de um serviço que estava criando.
Contexto
O problema em questão aconteceu num projeto que participo fora do meu trabalho (no GrupoZAP). Trabalho nesse ecossistema (“freela”) há mais de um ano e sempre aprendo muitas coisas por conta da criticidade do projeto. O trabalho se resume a fazer cotações, pedidos e faturamento de produtos farmacêuticos e perfumaria em diversos fornecedores.
Não usamos nenhum repositório de dados (banco de dados, redis, mongodb, etc) porque os dados são muito voláteis, o dado muda para cada cliente, a manutenção disso seria caótica e seria muito caro também. Preferimos fazer tudo em memória juntamente com redis para alguns caches e controles.
Problema
O problema estava no sistema que faz a cotação dos produtos. Recebemos a requisição de cotação contendo X produtos. Batemos no serviço do fornecedor diversas vezes pegando as “tabelas” de produtos, descontos, taxas, entre outras. Infelizmente a maioria dos fornecedores tem serviços “arcaicos” que não há previsão de mudar tão cedo. Não é estruturado e não temos escolha.
No exemplo abaixo, coloquei como se a base tivesse 10 milhões de produtos. No cenário real, o número de produtos, descontos, etc muda de cliente para cliente e chega a cerca de 1 milhão. Coloquei muitos a mais para conseguimos ver a diferença. Não posso colocar o código aqui mas basicamente tínhamos o seguinte código.
Está bem simplista porque fazemos muito mais coisas quando estamos fazendo a cotação. Calculamos taxas e fazemos a iteração para as taxas igual os descontos (o que torna ainda mais doloroso o processamento utilizando o código acima). Não lembro exatamente o tempo que levava mas chegou a durar mais de 1 hora, dependendo da quantidade de itens que eram requisitados. Não precisava de muitos itens para demorar.
Nesse exemplo levou um tempo razoável para processar, 54 segundos:
there are 10000000 items and 10000000 discounts
item found 800
discounts found for this item: 191797
item found 8000
discounts found for this item: 192410
item found 80000
discounts found for this item: 192566
item found 800000
discounts found for this item: 191904
finish, elapsed time 54.1443350315094 seconds
Solução
Agrupamentos. Justamente por não existir um repositório precisamos colocar tudo agrupado em memória. Agrupamos os produtos por id e os descontos por product_type.
O tempo gasto dessa vez foi 12 segundos:
there are 10000000 items and 10000000 discounts
item found 800
discounts found for this item: 191909
item found 8000
discounts found for this item: 192480
item found 80000
discounts found for this item: 192842
item found 800000
discounts found for this item: 192507
finish, elapsed time 12.628214120864868 seconds
Resumindo, o agrupamento que fizemos aqui:
Nos possibilita a acessar o item e os descontos dele diretamente, como se estivesse utilizando índices:
grouped_items.get(qi['id'], None)
discounts_found = grouped_discounts.get(item['type'], [])
Bom, isso reduziu o tempo drasticamente. Detalhe importante: contamos o tempo de agrupamento também no segundo código. A complexidade do código é bem diferente nos dois cenários, o primeiro era O(n²) e o segundo apenas O(n), uma diferença gritante. Para um programador a complexidade do código é sempre muito importante pois impacta diretamente no tempo e no dinheiro (de várias formas).
No cenário real, a maior parte do tempo que temos hoje é da requisição para os serviços do fornecedor (nesse caso).
Conclusão
Agrupamentos podem ser muito úteis quando você não tem uma base de dados com índices e todo poder da ferramenta que poderia estar utilizando (como um banco de dados, por exemplo).
É importante sempre estar atento para loops aninhados pois isto pode afetar a performance da sua aplicação consideravelmente.
O Alisson R. Perez além de ser parceiro nesse projeto, revisou este texto. Obrigado manolo!
Configurações da minha máquina
SO: macOS High Sierra
Processor Name: Intel Core i5
Processor Speed: 2,4 GHz
Number of Processors: 1
Total Number of Cores: 2
Memory: 8 GB 1600 MHz DDR3