Por que profiling importa em Rust
Rust tem reputação de linguagem rápida, mas reputação não substitui medição. Um serviço pode estar escrito em Rust e ainda gastar CPU demais serializando JSON, alocar memória em excesso no caminho quente, bloquear o runtime async, fazer queries lentas, copiar buffers sem necessidade ou esperar uma dependência externa. A diferença é que Rust oferece ferramentas excelentes para transformar essa investigação em trabalho disciplinado, não em palpite.
Profiling em Rust é a prática de observar onde o programa gasta tempo, memória e espera. Não é a mesma coisa que sair trocando clone() por referência em todo lugar. Um bom perfil responde perguntas concretas: qual função domina CPU? O gargalo é computação, I/O, lock, alocação ou latência de rede? A mudança reduziu p95 ou apenas melhorou um microbenchmark? O custo apareceu depois de um deploy, de um volume maior de dados ou de uma mudança de dependência?
Para quem busca vagas Rust em backend, plataforma, infraestrutura, dados, fintech ou segurança, essa habilidade pesa muito. Empresas não adotam Rust apenas porque a sintaxe é interessante. Elas adotam Rust quando precisam de previsibilidade, controle de recursos e confiança operacional. Saber provar performance com dados aproxima você das equipes que operam serviços reais, CLIs internas, pipelines, agentes e sistemas de baixa latência.
Comece pela pergunta de negócio
Antes de abrir qualquer ferramenta, defina qual problema você está tentando resolver. “Deixar mais rápido” é vago. “Reduzir p95 do endpoint de busca de 420 ms para menos de 180 ms”, “diminuir RSS do worker de 900 MB para 450 MB” ou “evitar regressão de 20% no parser” são objetivos que orientam a investigação.
Essa pergunta muda o tipo de medição. Se o problema é latência de API, você precisa olhar rota, dependências, pool de conexões, runtime async e percentis. Se é CPU em batch, flamegraph e perf podem ser suficientes. Se é memória, procure alocações, retenção, buffers, caches e cardinalidade. Se é throughput de fila, investigue backpressure, concorrência, tamanho de lote, retries e idempotência.
Também separe ambiente de desenvolvimento, staging e produção. Um benchmark local em notebook ajuda a comparar algoritmos, mas não captura ruído de rede, disco, TLS, banco, cardinalidade real e contenção do host. Por outro lado, produção exige cuidado: não colete payload sensível, não derrube o serviço com instrumentação pesada e não rode experimentos que mudem comportamento sem controle.
Benchmark primeiro quando o cenário é isolável
Quando o gargalo é uma função, parser, codec, algoritmo ou transformação de dados, comece com benchmark. A crate criterion é a opção mais comum no ecossistema Rust porque aplica análise estatística, aquece o código, mostra variação e ajuda a comparar versões. Ela é útil para evitar a armadilha de medir uma única execução com Instant::now() e tomar decisão sobre ruído.
Um benchmark mínimo pode comparar duas formas de normalizar eventos:
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn normalizar_evento(input: &str) -> String {
input.trim().to_lowercase().replace(' ', "-")
}
fn bench_normalizar(c: &mut Criterion) {
c.bench_function("normalizar_evento", |b| {
b.iter(|| normalizar_evento(black_box(" Pedido Criado Com Sucesso ")))
});
}
criterion_group!(benches, bench_normalizar);
criterion_main!(benches);
O ponto não é decorar esse exemplo. O ponto é criar um cenário repetível antes de mexer no código. Se você troca uma estrutura de dados, altera serialização ou adiciona cache, o benchmark precisa mostrar ganho consistente. Se o ganho é pequeno, avalie legibilidade, risco e manutenção. Rust permite micro-otimizações sofisticadas, mas nem toda otimização merece entrar no produto.
O tutorial de testes em Rust já apresenta Criterion como parte do fluxo de qualidade. Para trabalho profissional, trate benchmark como teste de hipótese: ele deve ter nome claro, entrada representativa e comparação antes/depois. Quando possível, salve relatórios em CI ou ao menos rode benchmarks críticos antes de releases importantes.
Flamegraph para enxergar CPU de verdade
Quando o programa é grande demais para isolar a hipótese, use flamegraph. Um flamegraph mostra pilhas de chamada agregadas: quanto mais largo o bloco, mais tempo foi gasto ali. Em Rust, ferramentas como cargo flamegraph em Linux ajudam a visualizar CPU com base em perf.
O fluxo típico é compilar em release com símbolos de debug suficientes, executar uma carga representativa e gerar o gráfico:
cargo build --release
cargo flamegraph --bin meu-servico -- --modo-carga exemplo
Em serviços HTTP, muitas vezes você roda o binário e gera tráfego separado com uma ferramenta de carga. O importante é que o perfil represente o problema. Se produção sofre com payload grande, não use payload pequeno. Se o gargalo aparece depois de 30 minutos por cache quente, não faça um teste de 5 segundos e declare vitória.
Ao ler o flamegraph, procure blocos largos inesperados. Pode ser serialização com serde_json, regex recompilada a cada chamada, compressão, parsing de data, formatação de string, cópias de buffer, hash pesado ou função de dependência externa. Depois confirme com alteração pequena e benchmark. Não otimize tudo que aparece; todo programa gasta tempo em algum lugar. Otimize o que impede o objetivo.
Memória: alocação, retenção e cache
Performance em Rust não é só CPU. Muitos problemas aparecem como consumo de memória alto, latência por pressão de alocador ou cache que cresce sem limite. O sistema de ownership evita classes enormes de bug, mas não impede você de manter dados vivos por mais tempo do que precisa ou criar String nova em cada iteração.
Comece observando RSS, heap, cardinalidade de caches e tamanho de filas. Em código, procure padrões comuns: collect() em listas grandes sem necessidade, clone() em payloads pesados, to_string() em hot path, buffers recriados a cada requisição, HashMap sem capacidade inicial em loops conhecidos, cache sem TTL e canal async acumulando mensagens mais rápido do que consome.
Nem todo clone() é problema. Às vezes ele simplifica ownership com custo irrelevante. O erro é não saber. Se um clone() aparece no flamegraph ou em perfil de alocação, investigue. Se não aparece e deixa o código mais claro, talvez ele seja o trade-off correto. Rust profissional é equilíbrio entre segurança, clareza e custo medido.
Para serviços com alta alocação, também vale testar alocadores como jemalloc ou mimalloc, mas trate isso como experimento, não como reflexo. Mudar alocador pode melhorar throughput em uma carga e piorar outra. Registre versão, carga, métrica e rollback. Esse cuidado conversa com o guia de release engineering em Rust: performance também precisa de rastreabilidade.
Async Rust: quando o gargalo é espera
Em aplicações Tokio, nem todo problema de latência aparece como CPU. Às vezes a task está esperando lock, pool, rede, canal ou uma operação bloqueante que entrou no runtime errado. Um serviço pode ter CPU baixa e ainda responder mal porque poucas tasks conseguem avançar.
Use tracing para marcar etapas importantes e correlacionar duração. O artigo sobre OpenTelemetry em Rust mostra como spans e métricas ajudam a enxergar o caminho da requisição. Para investigação async local, tokio-console pode revelar tasks, recursos, wakes e operações que ficam penduradas. Ele é especialmente útil quando a suspeita envolve deadlock, lock longo, excesso de tasks ou espera invisível.
Algumas regras práticas ajudam. Não rode CPU pesada dentro de handler async sem considerar spawn_blocking ou fila dedicada. Não segure Mutex durante .await sem necessidade. Não crie concorrência ilimitada para chamadas externas. Não esconda timeout. Não deixe fila crescer sem backpressure. Em Rust, o compilador ajuda muito, mas ele não decide sua política de carga.
Quando a aplicação usa Tower com Axum, camadas de timeout, limite de concorrência, rate limit e trace precisam ser medidas juntas. Reduzir latência média aumentando rejeições pode ser ruim para produto. Melhorar throughput às custas de p99 pode quebrar SLO. Por isso, olhe percentis, taxa de erro e saturação, não apenas “requisições por segundo”.
Proteja performance em CI e release
Depois de corrigir um gargalo, proteja o ganho. Documente a hipótese, o perfil observado, a mudança aplicada e a métrica final. Quando possível, adicione benchmark de regressão. Nem todo benchmark deve bloquear CI, porque ambientes compartilhados têm ruído, mas benchmarks críticos podem rodar em job dedicado, máquina fixa ou comparação manual antes de release.
Também inclua checks simples no fluxo normal: cargo fmt, cargo clippy, cargo test, build release e smoke test. Para performance, um smoke test não prova tudo, mas pega falhas óbvias: binário não sobe, endpoint não responde, flag mudou, feature não compila ou dependência quebrou. Em projetos com SLO, conecte isso a métricas de produção: versão nova, p95, p99, erros, CPU, RSS, filas e saturação.
Uma prática madura é criar um orçamento. Por exemplo: o parser não pode regredir mais de 10% sem justificativa; o worker não pode passar de 512 MB em carga padrão; o endpoint crítico deve manter p95 abaixo de 200 ms em teste de carga. Orçamento evita discussões subjetivas e transforma performance em contrato de engenharia.
Projeto de portfólio: profiler aplicado a uma API Rust
Um projeto forte para currículo não precisa ser enorme. Crie uma API Axum que recebe eventos, valida JSON com Serde, grava em PostgreSQL com SQLx e expõe uma consulta agregada. Depois faça três versões: uma ingênua, uma medida e uma otimizada. Mostre flamegraph, benchmark Criterion, métricas antes/depois e README explicando trade-offs.
Inclua um gargalo realista: parsing repetido, clone() pesado, consulta sem índice, cache sem limite ou concorrência externa sem timeout. Corrija com uma mudança pequena e prove. Esse tipo de projeto é mais convincente do que dizer “Rust é rápido”. Ele mostra que você sabe operar performance como processo.
Se você também compara Rust com Go em times brasileiros, vale estudar materiais do Golang Brasil. Muitas empresas avaliam as duas linguagens para backend e infraestrutura; saber explicar onde Rust entrega controle de memória, onde Go entrega simplicidade operacional e como medir a diferença é uma vantagem real.
Checklist de profiling em Rust
Antes de chamar uma otimização de concluída, revise:
- existe uma pergunta mensurável, não apenas vontade de “melhorar performance”;
- a carga de teste representa o problema observado;
- benchmark e profiling foram usados para papéis diferentes;
- flamegraph ou telemetria apontaram o gargalo principal;
- a mudança é menor que o problema, não uma reescrita impulsiva;
- p95, p99, CPU, memória e taxa de erro foram avaliados quando fazem sentido;
- dados sensíveis não foram parar em logs, traces ou dumps;
- o ganho foi documentado e pode ser reproduzido;
- existe rollback ou caminho de reversão se a otimização piorar produção.
Rust ajuda a construir software rápido, mas a maturidade está em saber medir. Profiling transforma performance em engenharia: hipótese, evidência, mudança pequena, validação e proteção contra regressão. Para o mercado brasileiro, essa combinação é exatamente o que diferencia um dev que conhece a linguagem de um dev capaz de operar Rust em produção.