Profiling
Como medir onde o tempo e a memória são gastos antes de otimizar.
O problema: a intuição erra o gargalo
Um relatório está lento. A equipe olha o código e aposta: "é a formatação dos valores", "é o JSON", "é o banco". Cada aposta vira uma otimização, e o relatório continua lento, porque o tempo estava em outro lugar.
Profiling é medir, com o programa rodando, onde o tempo e a memória vão parar, função por função. A ferramenta é o profiler, e toda linguagem tem pelo menos um.
Um gargalo escondido, medido em cinco linguagens
Escrevemos o mesmo relatório em TypeScript, Python, Go, Java e C#: 100 mil pedidos, os clientes únicos (20 mil), o total de cada um e uma linha formatada por cliente. A versão lenta tem um erro comum: para saber se o cliente já está na lista, percorre a lista inteira (includes, not in, contains). Com 20 mil clientes, são bilhões de comparações. A versão corrigida usa um conjunto (Set, dict, map, HashSet), que responde em tempo constante.
| Versão lenta | Versão corrigida | |
|---|---|---|
| TypeScript (Node 24) | 894 ms | 28 ms |
| Python 3.13 | 11.374 ms | 30 ms |
| Go 1.25 | 286 ms | 12 ms |
| Java 21 | 1.011 ms | 177 ms |
| C# (.NET 9) | 172 ms | 10 ms |
De 6 a 380 vezes mais lento, por uma linha. Um código com esse erro passa em qualquer teste com poucos dados e só aparece em produção, com volume.
O que cada profiler mostrou
Rodamos a versão lenta sob o profiler de cada linguagem:
| Linguagem | Profiler | O que apareceu no topo |
|---|---|---|
| Go | pprof | slices.Index, 92,6% do tempo próprio, chamado por clientesUnicosLento |
| Python | cProfile | clientes_unicos_lento, 11,4 s dos 11,5 s totais |
| Java | JDK Flight Recorder | ArrayList.indexOfRange e Integer.equals, o código de dentro do contains |
| C# | dotnet-trace | SpanHelpers.NonPackedIndexOfValueType, 74,9%, o código de dentro do List.Contains |
| TypeScript | node --cpu-prof | relatorio, 88,4% |
Os cinco apontaram o lugar certo, cada um do seu jeito. Duas lições estão na tabela:
- Tempo próprio e tempo acumulado. Em Java e C#, o topo da lista era uma função da biblioteca padrão, onde o processador de fato estava. Para chegar ao código da aplicação, é preciso olhar quem a chamou: o tempo acumulado (cumulative, inclusive) de
clientesUnicosLentoinclui o tempo das funções que ela chama. Opprofdo Go mostrou as duas colunas lado a lado. - Funções embutidas pelo compilador. No Node, o tempo foi atribuído a
relatorio, e não à função lenta: o compilador do V8 embutiu (inlining) a função lenta dentro da que a chamava. O profiler mostra o código como ele roda, não como foi escrito.
Amostragem e instrumentação
Os profilers funcionam de dois jeitos:
| Por amostragem | Por instrumentação | |
|---|---|---|
| Como mede | Olha a pilha de chamadas centenas de vezes por segundo e conta onde o programa estava | Registra a entrada e a saída de cada chamada de função |
| Exemplos | pprof, JFR, dotnet-trace, node --cpu-prof, py-spy | cProfile |
| Peso no programa | Baixo | Proporcional ao número de chamadas |
| Precisão | Estatística: funções muito rápidas podem não aparecer | Conta cada chamada, com o número de chamadas |
| Em produção | Viável, com cuidado | Raramente |
Medir sempre pesa. No teste de memória, o tracemalloc do Python, que rastreia cada alocação, deixou a versão corrigida do relatório mais de quatro vezes mais lenta: 132 ms, contra 30 ms sem ele. Os números absolutos de um programa sob profiler não valem como medida de desempenho; o que vale é a proporção entre as partes.
Memória
O tempo é metade da história. A outra é onde a memória é alocada e quem a segura. O mesmo relatório, sob o tracemalloc:
| Linha | Memória |
|---|---|
| A geração dos 100 mil pedidos, como tuplas | 11,1 MB |
| As 20 mil linhas formatadas | 1,3 MB |
| A lista final | 0,2 MB |
O profiler de memória responde perguntas como "por que o processo usa 2 GB?" e "o que cresce sem parar?" (um vazamento). Em linguagens com coletor de lixo, ele mostra também quanto o programa aloca por segundo, que pesa no tempo gasto coletando, como mostra o tópico de gerenciamento de memória.
Gráficos de chama
A forma mais útil de ler um perfil é o gráfico de chama (flame graph): cada barra é uma função, a largura é a fração do tempo, e as barras de cima são chamadas pelas de baixo. O gargalo aparece como a barra mais larga no alto da pilha, e o caminho até ele fica visível abaixo. O go tool pprof -http, o Speedscope, o painel de desempenho do Chrome (para os perfis do Node) e o JDK Mission Control desenham esses gráficos.
Por que isso importa quando a IA escreve o código
Um assistente que otimiza código costuma otimizar sem medir:
- Microotimizações em trechos que não pesam, deixando o gargalo de verdade intocado.
- Buscas em listas dentro de laços (
includes,in,contains), O(n²) escondido. - Conclusões tiradas de tempos medidos com o profiler ligado.
- Leitura do tempo próprio sem olhar o acumulado, otimizando a biblioteca padrão em vez do código que a chama.
- Cache acrescentado para esconder uma consulta ou um laço lento, sem entender a causa.
O próximo nível compara os profilers de cada linguagem, quando usar cada tipo e como medir em produção.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Profiling em cinco minutos
Explique em no máximo cinco minutos de leitura o que é profiling, para alguém que sempre otimizou código por intuição. Cubra só o essencial: o que um profiler mede, amostragem e instrumentação, tempo próprio e acumulado, gráficos de chama e por que medir pesa no programa. Use o exemplo de um relatório lento. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
Lendo um perfil de CPU
Quero aprender a ler o resultado de um profiler de CPU. Aja como um mentor e vá por etapas, esperando a minha resposta em cada uma: 1. Me mostre a saída de um profiler com tempo próprio e acumulado e peça que eu ache o gargalo. 2. Explique por que a função no topo pode ser da biblioteca padrão, e como subir até o código da aplicação. 3. Me descreva um gráfico de chama e peça que eu diga onde está o problema. 4. Explique o efeito de funções embutidas pelo compilador no perfil. 5. Mostre um perfil de um processo que está esperando o banco, e peça que eu diga o que ele não mostra.
Perfis de memória
Quero entender como investigar o uso de memória de uma aplicação. Explique: 1. A diferença entre memória alocada, memória retida e memória do processo. 2. Instantâneos do heap e como compará-los para achar um vazamento. 3. Perfis de alocação e a relação com o coletor de lixo. 4. As ferramentas da minha linguagem (pergunte qual é). 5. Os vazamentos mais comuns em aplicações web. Depois me descreva um sintoma e pergunte como eu investigaria.
Por que a intuição erra o gargalo
Quero treinar a intuição sobre onde o tempo de um programa vai parar, e entender por que ela erra. Me mostre, uma de cada vez, cinco trechos de código com algum problema de desempenho escondido (busca linear em laço, consulta em laço, serialização repetida, expressão regular compilada a cada chamada, cópia de coleção grande). Para cada um, peça que eu aponte o problema e estime o impacto, e depois explique o que um profiler mostraria.