Introdução
Quando aplicações são executadas, elas geram uma quantidade enorme de informações que na maioria das vezes são ignoradas, e quando essas aplicações enfrentam situações desagradáveis, como lentidão, alto consumo de memória, alta demanda de poder de processamento e outras coisas, essas informações passam a ser relevantes para que seja possível tornar o uso da aplicação viável operacionalmente e financeiramente. Uma forma de entender os problemas citados, é a partir do profiling.
Em um nível mais fundamental, Patel e Rajwat (2013) destacam que para manter um software eficiente, é essencial conhecer as necessidades de integrações, hardware e rede que um software demanda, esse conhecimento é necessário para avaliar qual o tipo de alteração e será necessária para cumprir com os requisitos do negócio, tendo este conhecimento, também é possível chegar a mais ideal das várias alternativas de profiling.
Se tratando especificamente do profiling de software, Obregon (2023) diz que o profiling é uma maneira dinâmica de realizar a análise de aplicações, é o processo de monitorar o comportamento de aplicações em tempo de execução, o que envolve coletar dados sobre performance, alocação de memória, uso de threads e outras métricas críticas. Esses dados fornecem ideias sobre como a aplicação está sendo executada e ajuda desenvolvedores a identificar aspectos ineficientes e potenciais pontos de melhoria.
Um profiling de software abrange diferentes componentes que uma aplicação utiliza, os principais são:
• CPU: revela os pontos que mais demandam recursos de processamento, permitindo identificar pontos de melhoria;
• Memória: possibilita identificar memory leak e entender como a memória é alocada e utilizada;
• Thread: em cenários onde multithread é normal, os desenvolvedores precisam entender o comportamento e o ciclo de vida das threads, visando evitar erros de sincronização, deadlocks e demais possíveis problemas.
No Java, também é possível considerar a análise do garbage collector (GC) como sendo um ponto de aplicar o profiling, visto que o GC é utilizado como gerenciador de memória de aplicações Java.
De maneira geral, pode-se dizer que a definição de profiling se consiste em um processo contínuo de coleta de dados de aplicações, que, quando realizada de maneira adequada, gera informações importantes para otimizar sistemas, aumentar o retorno financeiro e melhorar a satisfação de clientes.
Melhores práticas para o profiling de software
O profiling normalmente surge em cenários ruins para aplicações, portanto, antes de mudar qualquer comportamento, é apropriado fazer um snapshot sobre o funcionamento do sistema a ser analisado, de forma que que existam dados o suficiente para comparação após uma eventual correção ou melhoria. Ademais, é importante abordar o profiling de forma estruturada. Isso inclui: ideias claras sobre os problemas a serem resolvidos, coletas dos dados comparativos adequados (antes e depois de uma eventual melhoria) e interpretação clara sobre o resultado das comparações entre os dados coletados.
Chen (2024) e Obregon (2023) sugerem que o profiling deve ser empregado no cotidiano do fluxo de trabalho dos desenvolvedores, não apenas quando algo não vai bem. Eles argumentam que ajuda a captar melhorias possíveis de forma proativa no fluxo de desenvolvimento. No entanto, também destacam que é necessário focar principalmente na resolução dos gargalos essenciais, que resultarão em impactos mais significantes no desempenho e uso das aplicações.
Escolher as ferramentas certas também é essencial para conduzir um profiling adequadamente, existem diversas ferramentas que possibilitam o profiling de aplicações Java, algumas dessas ferramentas são: VisualVM, JConsole, JProfiler, IntelliJ IDEA Profiler e outros. A escolha da ferramenta dependerá da necessidade e complexidade da aplicação a ser analisada.
Principais componentes e técnicas de profiling no Java
O profiling de um software pode ter como alvo diferentes componentes que o software utiliza, portanto, existem técnicas adequadas para cada componente.
Profiling de CPU
É necessário entender como o CPU está sendo utilizado pela aplicação, isso envolve identificar métodos e gargalos que consomem muito tempo de processamento. Neste profiling, o ideal é buscar por métodos com muitas invocações ou com grandes tempos de execução.
• Técnica de amostragem: envolve coletar snapshots temporários da call stack (ou pilha de execução) e agregar as informações coletadas para identificar “hot spots” no código. É uma técnica leve para a aplicação e simples de se aplicar, ideal para avaliar cenários em ambiente produtivo, contudo, é menos preciso e pode omitir pequenas execuções de métodos.
• Técnica de instrumentalização: é uma técnica que modifica o bytecode da aplicação para incluir registros de tempo no momento da entrada e saída de processamento de métodos. Concede detalhes mais precisos do que a técnica anterior, mas, sobrecarrega a aplicação, podendo ocasionar em lentidões inesperadas.
Profiling de Memória
O profiling de memória é importante para identificar possíveis vazamentos de memória, e entender como a aplicação aloca e utiliza a memória. Neste profiling, é almejado encontrar padrões indesejados de uso de memória.
• Análise de heap: envolve verificar os objetos presentes na heap para identificar vazamentos e consumidores de muita memória.
• Análise de GC: ajuda a entender como a limpeza efetuada pelo GC atua e afeta a performance da aplicação.
Profiling de Threads
Técnicas de profiling de threads foca em identificar o que está acontecendo na thread no momento de execução, bem como providenciar dados sobre elas. Este profiling busca identificar o comportamento das threads, monitorar o estado e verificar problemas de sincronização ou deadlock delas.
• Monitoramento da JVM: envolve monitorar diversas métricas em nível da JVM, como uso da CPU e memória, contagem de threads e estatísticas do GC.
• Profiling diagnóstico: técnicas como uso do Java Flight Recorder permitem coletar dados detalhados de determinado tempo de execução sem grandes sobrecargas na aplicação, é útil para diagnosticar problemas em ambientes produtivos.
Estudo de caso aplicando profiling em aplicação Java
O profiling será conduzido com o VisualVM sobre uma aplicação que possui apenas uma API para gerar QRCode. Durante este profiling, serão apresentados dois pontos de melhoria fictícios, o primeiro, é sobre a velocidade de processamento do fluxo de gerar QRCode, e o segundo, sobre a otimização do consumo de memória heap deste mesmo fluxo.
As ferramentas e aplicações utilizadas durante este profiling serão: IntelliJ IDEA 2024.1.4, VisualVM 2.1.10, OpenJDK 21.0.1, Spring Boot 3.3.4, esta aplicação (payment-api), Apache Maven 3.9.6 e Postman v11.23.1 e Docker para levantar o banco de dados (Postgres) utilizado pela aplicação.
Após inicializar a aplicação, é possível inicializar também o VisualVM através de um plugin disponível para download no IntelliJ, o VisualVM Launcher.
O botão para inicializar o VisualVM no Intellij IDEA pode ser encontrado na barra lateral esquerda, no canto inferior, próximo ao console.
Após inicializar o VisualVM, será exibida a página inicial da aplicação e, ao lado esquerdo, um menu, que permite a visualização de todas as aplicações Java em execução no ambiente.
Após selecionar a aplicação “PaymentApiApplication”, será exibida a seguinte tela:
Esta tela apresenta algumas informações de execução da aplicação, o PID, host, classe principal, a JVM no qual a aplicação está rodando e outras coisas. Para iniciar o profiling, é necessário clicar em “Profiler”, informar os pacotes ou classes que devem ser monitorados em “CPU Settings” e em “Memory Settings”.
Profiling de CPU
Após configurar os pacotes e classes a serem monitorados, é necessário clicar em “CPU” para inicializar o profiling através da técnica de instrumentalização do bytecode. Após clicar em ” CPU”, é necessário começar a gerar as requisições para a aplicação, de forma que gere insumos a serem analisados. Após inicializar o profiling e executar cinco requisições para a aplicação, todas as chamadas serão listadas pelo VisualVM, resultando na seguinte tela:
Ao analisar as chamadas, é possível notar que existe um tempo muito grande de processamento no fluxo de geração do QRCode, principalmente na classe PaymentController, no método create:
Partindo da análise das informações nesta tela, é possível considerar que algo no método create não vai bem, e para este cenário, de fato não está bem, pois existe um Thread.sleep(3000) dentro dele:
Considerando o estado atual da aplicação, é ideal fazer um snapshot da versão “pré-correção” para que seja possível comparar com a versão da aplicação após a correção.
Após a correção e reinicialização da aplicação, é necessário inicializar o profiling novamente. Ao fazer as mesmas requisições, o seguinte resultado é apresentado:
Nota-se que a aplicação obteve um ganho de três segundos sobre o tempo total de geração de QRCode, portanto, é possível verificar que o profiling de CPU através da instrumentalização do bytecode fornece informações valiosas sobre o tempo que o CPU passa em determinados locais da aplicação, podendo facilitar muito a análise e identificação de pontos de melhoria de código fonte em cenários produtivos.
Profiling de Memória
Para realizar o profiling sobre o uso de memória da aplicação será utilizada a técnica de análise de heap, e é necessário informar os pacotes a serem analisados em “Memory settings”, após isso, é preciso clicar em “Memory”, ao lado de “CPU”. Para este profiling, também serão realizadas cinco requisições para a API de geração de QRCode. Ao executar as cinco requisições, a seguinte tela será apresentada:
Para este caso, não existem grandes otimizações a serem feitas, contudo, é possível notar que instâncias da classe PaymentDTOBuilder está tomando espaço na memória, mesmo não sendo necessário. Simulando uma pequena otimização e considerando que o ambiente depende dela, o ideal é realizar um snapshot após as requisições, remover o uso do builder do código e testar novamente. Após isso, o resultado alcançado será o seguinte:
Sem empregar o builder, é possível notar que houve uma redução no consumo de memória.
Além deste profiling, também é possível acompanhar métricas gerais da aplicação (como uso do CPU, heap, threads e classes carregadas) na aba “monitor”, como visto abaixo:
Conclusão
Com base no estudo de caso e nos resultados obtidos, conclui-se que o uso de técnicas de profiling simplifica a identificação de pontos de melhoria em aplicações. Além de ser uma prática acessível, o uso adequado do profiling através das ferramentas apropriadas permite resolver inúmeros problemas produtivos antecipadamente e aumentar o grau de maturidade dos desenvolvedores que aplicam esta técnica regularmente. Ademais, promover o uso do profiling como parte do fluxo de desenvolvimento pode elevar a qualidade dos produtos desenvolvidos, melhorar a reputação de desenvolvedores e a consolidar a posição das empresas no mercado.
Referência bibliográfica
OBREGON, Alexander. Profiling Java Applications: Tools and Techniques. [S. l.], 10 nov. 2023. Disponível em: https://medium.com/@AlexanderObregon/profiling-java-applications-tools-and-techniques-3569b32862f4. Acesso em: 18 dez. 2024.
CHEN, Lily. Why profiling should be part of regular software development workflow. [S. l.], 17 mar. 2024. Disponível em: https://medium.com/performance-engineering-for-the-ordinary-barbie/why-profiling-should-be-part-of-regular-software-development-workflow-8b19b7f52b38. Acesso em: 18 dez. 2024.
PATEL, Rajendra; RAJWAT, Arvind. A Survey of Embedded Software Profiling Methodologies. [S. l.], 10 dez. 2013. Disponível em: https://arxiv.org/abs/1312.2949. Acesso em: 18 dez. 2024.
Links
Repositório do projeto utilizado: https://github.com/Zodh/payment-api
Imagem docker para o banco de dados utilizado: guilhermemmnn/fastfood-db:latest.
Top comments (0)