MÓDULO 06 · CONCEITO 05 DE 12

Profiling

Brendan Gregg e o flame graph (2011). Sampling vs instrumentation. dotnet-trace, pprof, py-spy, perf, eBPF. Continuous profiling. As ferramentas que separam quem otimiza no escuro de quem otimiza com luz.

Tempo de leitura ~22 min Pré-requisito Conceito 01 (performance como disciplina) Próximo Benchmarking

Em outubro de 2011, Brendan Gregg — então engenheiro de performance no Joyent, antes de Netflix e Intel — publicou um post no blog dele com um diagrama estranho: retângulos coloridos empilhados em camadas, formando uma silhueta irregular que lembrava chamas. O nome stick: flame graph. A ideia era simples e transformadora: visualizar dados de profiling de sampling de forma que a hierarquia de chamadas ficasse imediatamente legível, sem precisar ler tabelas de mil linhas. Em uma década, flame graph virou padrão visual da indústria — Netflix, Google, Facebook, Cloudflare, todos os grandes players usam.

Profiling é a prática que faz o "performance como disciplina" do conceito 01 ser possível. Sem profiler, otimização vira chute; com profiler, vira diagnóstico. As ferramentas modernas — pprof do Go, dotnet-trace do .NET, py-spy do Python, perf e eBPF do Linux — capturam o que acontece em produção com overhead tipicamente abaixo de 2%, e expõem o resultado em flame graphs ou equivalentes interativos. A barreira de entrada baixou tanto que sêniores que ainda otimizam "por instinto" estão usando ferramenta errada para o problema.

Este conceito articula a distinção entre profilers sampling e instrumentation, mostra a anatomia do flame graph (e como lê-lo), apresenta as ferramentas idiomáticas em três ecossistemas, e fecha com continuous profiling — a prática moderna de ter profiling ligado em produção sempre, capturando dados reais, não sintéticos. O conceito 06 (benchmarking) complementa com a outra metade da disciplina: medição controlada, não observacional.

A premissa: profiling é habilidade prática que se aprende fazendo. A teoria sem captura real é academia. Os exemplos do conceito todos rodam localmente em uma tarde — vale fazer cada um na linguagem onde você passa mais tempo, e capturar os flame graphs com seus próprios olhos.

Sampling vs instrumentation — duas filosofias

Profilers se dividem em duas filosofias com trade-offs distintos. Saber qual ferramenta é qual define se você consegue usar em produção ou não.

Profilers de instrumentation

Inserem código em cada função (no compile, no runtime, ou via bytecode rewrite) que registra entrada, saída, e tempo. Resultado: trace completo do que aconteceu, contagem exata de chamadas, tempo exato em cada função. Custos: overhead alto (10–500% dependendo do granularidade), distorção do comportamento (funções pequenas viram desproporcionalmente caras pelo overhead), tamanho de arquivo gerado grande. Usar em produção tipicamente é inviável. Examples: cProfile (Python, builtin), dotnet-trace com --clrevents Method, JetBrains dotTrace "tracing mode", line_profiler (Python).

Profilers de sampling

Capturam stack trace periódica (tipicamente 99 ou 100 vezes por segundo) e contam quantas vezes cada função apareceu no stack. Não medem tempo absoluto — medem frequência relativa de aparição, que é proporcional ao tempo de CPU gasto. Custos: overhead tipicamente < 2% (na frequência padrão); imprecisão em funções rápidas que podem aparecer ou não no sample; sem captura de chamadas individuais. Vantagens: baixo overhead permite uso em produção, robusto a sistemas complexos. Examples: perf (Linux), pprof (Go), dotnet-trace com sample profiler, py-spy, async-profiler (Java).

A regra prática: sampling para produção, instrumentation para microbenchmarks ou bugs específicos. Sampling te diz onde o tempo é gasto em sistema real; instrumentation responde "exatamente quantas vezes essa função foi chamada", que raramente é a pergunta certa.

Anatomia do flame graph

Flame graph é a forma visual canônica de apresentar dados de sampling. Vale entender exatamente como ler um, porque a leitura é contraintuitiva no início.

Eixo vertical (Y): profundidade do stack. A função no topo do retângulo é a função chamadora; a função abaixo é a chamada. Em alguns flame graphs (icicle graph), a orientação é invertida — chamadora embaixo. A barra base do gráfico é tipicamente main ou entry point.

Eixo horizontal (X): alfabético ou por agrupamento, não temporal. Esse é o erro mais comum de leitura: pensar que o eixo X é tempo. Não é. Cada retângulo agrega todas as amostras onde aquela função apareceu naquela posição do stack, independente de quando ocorreram.

Largura do retângulo: proporcional ao tempo de CPU gasto naquela função (e descendentes). Uma função que ocupa metade da largura do gráfico ocupou metade do tempo de CPU.

Cor: tipicamente sem significado (gerada para diferenciar visualmente). Algumas variantes usam cor para indicar tipo (vermelho = user code, azul = libraries, etc.). Sempre vale ler a legenda.

A leitura é portanto: "olhar o que é mais largo no meio/topo". Funções largas no topo do gráfico são folhas caras — código onde o tempo é gasto diretamente. Funções largas no meio são chamadores que delegam para várias coisas caras. A pergunta diagnóstica é "qual função folha está consumindo mais tempo?" — essa é onde otimizar.

Profiling em três linguagens

Cada linguagem tem ferramentas idiomáticas. Vale conhecer as três principais.

C# — dotnet-trace + dotnet-counters + PerfView
# instalação (uma vez, global)
dotnet tool install -g dotnet-trace
dotnet tool install -g dotnet-counters
dotnet tool install -g dotnet-dump

# captura sample profile (10 segundos)
dotnet-trace collect \
  --process-id $PID \
  --duration 00:00:10 \
  --providers Microsoft-DotNETCore-SampleProfiler \
  --output cpu.nettrace

# converte para speedscope (flame graph interativo)
dotnet-trace convert cpu.nettrace --format Speedscope
# abrir https://speedscope.app e arrastar o arquivo .speedscope.json

# counters em tempo real (tipo top mas com métricas .NET)
dotnet-counters monitor \
  --process-id $PID \
  System.Runtime \
  Microsoft.AspNetCore.Hosting

# captura heap (memory leak)
dotnet-dump collect --process-id $PID
dotnet-dump analyze core_*

# análise gráfica completa: PerfView (Windows-first, mas roda em Linux)
PerfView /OnlyProviders=*Microsoft-DotNETCore-SampleProfiler collect

dotnet-trace + speedscope é o caminho padrão em 2026 para flame graph rápido. Para análise mais profunda (alocação por tipo, GC stats), PerfView ainda é a ferramenta canônica. Para produção contínua, Pyroscope agent .NET.

Python — py-spy, scalene, cProfile
# py-spy: profiler sampling sem mexer no código
pip install py-spy

# top ao vivo
py-spy top --pid $PID

# captura flame graph (30 segundos)
py-spy record -o profile.svg --pid $PID --duration 30
# abre profile.svg no browser — flame graph nativo

# inclui modo "native" para C extensions
py-spy record -o profile.svg --pid $PID --duration 30 --native

# scalene: profiling com info de alocação por linha
pip install scalene
scalene minha_app.py
# gera HTML com tempo CPU + tempo GPU + memória por linha

# cProfile (instrumentation, builtin)
python -m cProfile -o profile.prof minha_app.py
python -c "import pstats; p = pstats.Stats('profile.prof'); \
  p.sort_stats('cumulative').print_stats(20)"

# converte para flame graph
pip install snakeviz
snakeviz profile.prof   # abre browser interativo

py-spy (Ben Frederickson, 2018) é revolucionário: profiler sampling em Rust que pega stacks de processo Python rodando, sem patch no código, sem precisar rodar como root em modo apropriado. Para produção, Pyroscope tem py-spy embutido.

Go — pprof embutido, go tool trace, perf+eBPF
// no código: importar net/http/pprof expõe endpoints
package main

import (
    _ "net/http/pprof"
    "net/http"
    "runtime"
)

func main() {
    runtime.SetMutexProfileFraction(1)   // habilita mutex profile
    runtime.SetBlockProfileRate(1)       // habilita block profile
    go http.ListenAndServe(":6060", nil) // /debug/pprof/* exposto
    // ... resto da aplicação
}

// captura CPU profile (30s) e abre browser interativo
go tool pprof -http=:8080 \
  http://localhost:6060/debug/pprof/profile?seconds=30

// captura heap (snapshot atual)
go tool pprof -http=:8080 \
  http://localhost:6060/debug/pprof/heap

// outros profiles disponíveis:
//   /debug/pprof/goroutine  - todas as goroutines + stacks
//   /debug/pprof/block      - operações que bloquearam
//   /debug/pprof/mutex      - contenção em mutex

// trace completo de execução (não é sample, é instrumentação leve)
curl http://localhost:6060/debug/pprof/trace?seconds=5 -o trace.out
go tool trace trace.out   # interface web mostrando goroutines/GC/eventos

// continuous profiling em produção: Pyroscope, Parca, Datadog

Go tem a melhor experiência out-of-the-box. Profile sempre disponível em endpoint HTTP, ferramenta oficial gera flame graph navegável. go tool trace é único — mostra goroutines individuais ao longo do tempo, eventos de GC, network, syscalls.

Os tipos de profile que importam

Profile não é uma coisa só. Há vários tipos, cada um respondendo pergunta diferente.

CPU profile — onde o tempo de CPU é gasto. Responde "qual função consome ciclo?". É o profile padrão. Sampling, baixo overhead.

Heap / allocation profile — onde memória é alocada. Responde "quem está enchendo o heap?". Útil para GC pressure, memory leak. Em Go /debug/pprof/heap; em .NET via dotnet-trace com providers de GC; em Python via scalene ou tracemalloc.

Block profile — onde goroutines / threads bloqueiam (channel, mutex, network). Responde "estou esperando o quê?". Em Go, /debug/pprof/block; em .NET, exemplificado por análise de wait events em PerfView.

Mutex profile — onde há contenção em mutex. Em Go, /debug/pprof/mutex. Em outras linguagens, frequentemente parte do block profile.

Goroutine / thread dump — snapshot do estado de todas as threads. Útil para diagnóstico de "sistema travou": qual thread está fazendo o quê agora? Em Go, /debug/pprof/goroutine?debug=2; em .NET, dotnet-dump; em Java, jstack.

Off-CPU profile — Brendan Gregg formalizou: profile que mostra onde o programa está esperando (não rodando CPU). Captura via eBPF, expõe esperas em I/O, sleep, locks. Em Linux moderno, é uma das ferramentas mais poderosas para diagnóstico de "está lento mas CPU está ociosa".

Continuous profiling — profiling em produção sempre

A grande mudança da década 2014–2024 em profiling foi a adoção de continuous profiling: profile capturado periodicamente (a cada 10s, por exemplo) em todas as instâncias de produção, agregado em backend especializado, navegável por timeline. A proposta foi articulada por Google em 2010 internamente ("Google-Wide Profiling", paper IEEE 2010), aberta com Pyroscope em 2020 (depois adquirida pela Grafana Labs em 2023), e por Parca (Polar Signals, 2022).

A diferença qualitativa: você não precisa esperar um problema para capturar. Quando algo dá errado às 2 da manhã de quinta-feira, você navega o profile daquele momento exato. Compara com profile de quinta passada às 2 da manhã. Vê a regressão diretamente. Sem continuous profiling, você está captando profile depois do incidente e tentando reproduzir o problema; com continuous profiling, você tem a captura do momento da falha já na mão.

# exemplo: instalando Pyroscope agent em Go
import "github.com/grafana/pyroscope-go"

func main() {
    pyroscope.Start(pyroscope.Config{
        ApplicationName: "catalog-api",
        ServerAddress:   "http://pyroscope:4040",
        ProfileTypes: []pyroscope.ProfileType{
            pyroscope.ProfileCPU,
            pyroscope.ProfileAllocObjects,
            pyroscope.ProfileAllocSpace,
            pyroscope.ProfileInuseObjects,
            pyroscope.ProfileInuseSpace,
        },
    })
    // ... resto da aplicação
}

Em .NET: Pyroscope.NET nuget, similar configuração. Em Python: pyroscope-io. Em Java: async-profiler + Pyroscope agent. O overhead em todos os casos é abaixo de 2%, e o ganho em diagnóstico é qualitativo.

Datadog Continuous Profiler é a alternativa proprietária com integração ampla e visualização rica; Parca é a alternativa open-source agnóstica de stack; Pyroscope é o caminho default em ecossistemas Grafana. Para sistemas críticos em produção, ter um deles configurado é higiene de senior.

Lendo flame graph — o workflow real

Captura o profile é o passo fácil. Ler o flame graph e tirar conclusão útil é onde o senior agrega valor. O workflow típico:

1. Olhe o todo primeiro. Antes de mergulhar em detalhe, identifique as três ou quatro funções mais largas no gráfico. Tipicamente, 80% do tempo está em ~5 funções. Essas são candidatas iniciais.

2. Navegue até as folhas. Para cada função larga, clique para fazer zoom (em ferramenta interativa). Veja o que ela está chamando. Continue até atingir folhas — funções que não chamam nada relevante. As folhas são o que de fato consome tempo.

3. Identifique o tipo de hot path. A folha cara é: (a) código seu? (b) library/framework? (c) syscall? (d) GC? Cada categoria tem padrão distinto de otimização. Código seu — ataca diretamente. Library — considera substituir, ou parametrizar (ex.: ORM gerando query lenta). Syscall — geralmente I/O excessivo, considera batching. GC — pressão de alocação, conceito 08.

4. Quantifique antes de mexer. Função que ocupa 50% do CPU profile vale a pena otimizar; função que ocupa 2% raramente vale, mesmo se fácil. 80/20: foca nos 20% que respondem por 80% do tempo.

5. Mexa, capture de novo, compare. Após mudança, novo profile. A região antes dominante diminuiu? Apareceu algo novo? Iteração.

Exemplos de padrões reconhecíveis em flame graph

Com tempo, sêniores reconhecem assinaturas visuais comuns que apontam direto para a causa. Os três padrões mais frequentes:

Pico em função pequena chamada milhões de vezes. Função aparece como retângulo médio-grande, mas as descendentes dela são pequenas e diversificadas. Sintoma: chamada em loop apertado. Solução típica: batching, ou eliminação da chamada em loop.

Pico em GC ou allocation. Funções como System.GC, runtime.gcBgMarkWorker, PyMem_Alloc aparecem desproporcionalmente grandes. Sintoma: pressão de GC. Solução: reduzir alocações (conceito 08).

Pico em syscall ou network. Funções como read, write, recv, ou no .NET Socket.ReceiveAsync. Sintoma: I/O dominante, comum em sistemas que poderiam paralelizar/batch I/O.

Profiling em CI — perfomance regression detection

Profiling tradicionalmente foi atividade ad-hoc — você captura quando precisa. A prática emergente em times maduros é integrar profiling em CI: cada PR que toca em hot path conhecido captura profile sob carga sintética, compara com baseline, falha o build se regrediu.

Ferramentas modernas tornam isso possível. Codspeed integra com benchmarks existentes (cargo bench, pytest-benchmark, BenchmarkDotNet) e roda em CI com instrumentation determinística (sem ruído de máquina). Bencher e Touca oferecem alternativas. A ideia: mensurar performance é parte de "passar nos testes".

armadilha em produção

Profile capturado em ambiente de teste com tráfego sintético, conclusões aplicadas em produção. Cenário recorrente: equipe captura profile sob load test que usa endpoint padrão com payload pequeno. Identifica função "hot" e otimiza. Em produção, payload é diversificado, alguns muito maiores, e a função otimizada não era o hot path real — outras eram. O esforço foi gasto otimizando 5% e ignorando os 50%. Defesa: continuous profiling em produção. Profile sob carga real revela o que sintético não — e otimização baseada em produção converge mais rápido.

Diferencial profiling — comparando dois states

Uma das técnicas mais úteis e menos conhecidas é differential flame graph: dois profiles comparados visualmente, mostrando o delta de tempo. Útil para diagnosticar regressões: "essa versão ficou mais lenta — onde?".

Em pprof: go tool pprof -base before.pprof after.pprof. Ferramentas como Pyroscope têm "compare view" entre dois períodos de tempo. Em Speedscope (para .NET), modo de comparação está disponível. A leitura é: vermelho = ficou mais lento, verde = ficou mais rápido. Uma única imagem responde "onde a regressão aconteceu".

heurística do sênior

Antes de propor otimização, capture profile da situação real e mostre na revisão. "Esse endpoint tem hot path em X.SerializeJSON que ocupa 40% do CPU profile sob carga típica" é argumento defensável. "Acho que esse endpoint está lento por causa da serialização" não é. Diferença entre as duas afirmações é se você pegou o profile. E se a otimização proposta vale o ônus, capture profile de novo após a mudança e mostre o delta — em mesma escala, mesmo cenário. A disciplina de "profile + delta" é o que separa arguments defensáveis de instinto.

Por que importa para a sua carreira

Profiling é habilidade prática que diferencia juniores de sêniores em qualquer time que toca produção. Em entrevistas de design para sistemas com escala, "como você descobriria onde o tempo está sendo gasto?" é pergunta direta — a resposta forte cita ferramentas por linguagem (pprof, dotnet-trace, py-spy, perf+eBPF), distingue sampling de instrumentation, menciona continuous profiling como prática moderna. Em revisão de código, exigir "qual profile suporta essa otimização?" é serviço ao time. Em pos-mortem, apresentar profiles capturados durante o incidente é argumento canônico — guia para a causa raiz, não especulação. E em onboarding de sistema novo, capturar profile é uma das formas mais rápidas de entender o que ele faz na prática.

Como praticar

  1. Flame graph do seu sistema. Em um projeto seu (mesmo simples), instale a ferramenta de profiling apropriada (dotnet-trace, py-spy, ou pprof). Aplique carga sintética simples (k6, Apache Bench, ou requests em loop). Capture profile de 30s. Visualize o flame graph. Identifique as três funções que mais consomem tempo. Faça hipótese de otimização para a primeira. Esse exercício, repetido em três projetos, calibra leitura de flame graph permanentemente.
  2. Continuous profiling local. Suba Pyroscope em docker localmente. Configure agent na sua aplicação. Rode workload variado por 20 minutos. Navegue na UI: timeline, comparação entre períodos, filtros por endpoint. Esse é o padrão moderno de operação de produção; sentir uma vez fixa o vocabulário.
  3. Differential profile. Pegue dois commits do seu projeto — antes e depois de uma otimização (ou regressão). Capture profile do mesmo endpoint sob mesma carga em cada um. Use ferramenta de comparação (pprof -base, ou Speedscope diff, ou Pyroscope compare). Identifique onde houve mudança. Esse é o tipo de evidência que torna PRs de performance defensáveis.

Referências para aprofundar

  1. artigo Flame Graphs — Brendan Gregg (blog post fundador, 2011). brendangregg.com/flamegraphs.html — A página canônica. Inclui o paper de 2016 e dezenas de variantes (icicle, differential, off-CPU). Referência viva, atualizada.
  2. paper The Flame Graph — Brendan Gregg (CACM, 2016). Versão peer-reviewed do conceito. Articulação madura, exemplos canônicos.
  3. paper Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers — Ren et al. (IEEE Micro, 2010). research.google/pubs/pub36575 — Paper que introduziu a prática de continuous profiling em escala. Antecedente direto de Pyroscope, Parca.
  4. livro Systems Performance: Enterprise and the Cloud (2ª ed.) — Brendan Gregg (Pearson, 2020). Cap. 6 e 14 cobrem profiling em Linux com profundidade rara. Inclui perf, eBPF, off-CPU profiling.
  5. livro BPF Performance Tools — Brendan Gregg (Pearson, 2019). A referência canônica de eBPF para profiling em Linux moderno. Centenas de receitas práticas.
  6. livro Pro .NET Performance — Sasha Goldshtein, Dima Zurbalev, Ido Flatow (Apress, 2012). Apesar da idade, ainda é referência para profiling em .NET. Cap. 6 cobre PerfView em detalhe.
  7. docs dotnet-trace + speedscope. github.com/dotnet/diagnostics e speedscope.app — Combo padrão para profiling .NET com flame graph em 2026.
  8. docs pprof. go.dev/blog/pprof e github.com/google/pprof — Documentação oficial. pprof é também usado em outras linguagens (Rust via pprof crate, Java via async-profiler com formato pprof).
  9. docs py-spy. github.com/benfred/py-spy — Documentação clara, instalação trivial. Vale ler o post de Ben Frederickson sobre como py-spy funciona internamente (extrai stacks de processo Python sem patch).
  10. docs Grafana Pyroscope. grafana.com/docs/pyroscope — A plataforma open-source de continuous profiling. Cobre instalação, agentes por linguagem, query language.
  11. docs Parca. parca.dev — Alternativa open-source da Polar Signals. Foca em eBPF para profiling agnóstico de linguagem.
  12. vídeo Brendan Gregg — Performance Tuning (várias palestras LISA, USENIX, 2014–2024). YouTube. Gregg apresenta em conferências regularmente. As mais recentes cobrem eBPF moderno, BCC tools, Linux profiling de produção.