Depuração e Profiling em Zig: GDB, LLDB, Valgrind, perf e Tracy

Depurar Zig é diferente de depurar uma aplicação escondida atrás de runtime pesado. O binário mostra mais do que você espera: stack trace, símbolo DWARF, erro de allocator, endereço de falha, custo de função, syscall lenta e diferença real entre Debug, ReleaseSafe, ReleaseFast e ReleaseSmall. Essa transparência é uma vantagem enorme, desde que você use as ferramentas certas.

Este guia reúne um fluxo prático para depuração e profiling em Zig em 2026. Vamos cobrir modos de build, stack traces, GDB e LLDB, Valgrind, perf, flamegraphs, Tracy, detecção de vazamento de memória, leitura de core dump e o que levar para produção sem transformar observabilidade em overhead permanente. Ele complementa os guias de benchmarking em Zig, observabilidade em Zig, OpenTelemetry em Zig, eBPF para observabilidade Linux e servidor HTTP em produção.

Resposta rápida: qual ferramenta usar?

ProblemaPrimeira ferramentaPor quê
Crash local ou panicStack trace do Zig + GDB/LLDBMostra arquivo, linha, frames e variáveis sem instrumentação pesada
Vazamento ou uso inválido de memóriaGeneralPurposeAllocator em Debug, depois Valgrind/ASan quando fizer sentidoPega leak simples cedo e confirma comportamento nativo fora do teste
Função lenta em CPUperf record + flamegraphMede o binário real no Linux, inclusive syscalls e bibliotecas C
Latência intermitenteTracy ou logs estruturados com IDs de operaçãoMostra timeline, zonas e correlação entre etapas
Regressão após refatoraçãoBenchmark pequeno + ReleaseFast/ReleaseSafe comparadosEvita otimizar ruído ou modo de build errado
Incidente em produçãologs, métricas, health checks, core dump e símbolosInvestiga sem recompilar às cegas

A regra prática é simples: comece pelo mecanismo mais barato que responde à pergunta. Não abra Tracy para um erro óbvio de índice. Não rode benchmark sintético para um problema de DNS. Não use ReleaseFast para investigar UB que só aparece quando as verificações de segurança somem.

Modos de build mudam o diagnóstico

Antes de culpar a ferramenta, confirme o modo de compilação. Zig muda bastante entre desenvolvimento, produção segura e otimização agressiva.

ModoUso recomendadoO que você ganhaO que você perde
Debugdesenvolvimento e reprodução localsafety checks, stack traces completos, asserts úteisperformance realista
ReleaseSafeprodução conservadora e stagingotimização com várias verificações de segurançaparte do detalhe de debug, dependendo da configuração
ReleaseFasthot path já validadomáxima performancechecks removidos; bugs podem virar corrupção silenciosa
ReleaseSmallbinário mínimo, embarcado, CLI distribuídatamanho reduzidomenos informação para inspeção

Para depurar, compile primeiro em Debug:

zig build -Doptimize=Debug
zig build test -Doptimize=Debug

Para comparar comportamento de produção:

zig build -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseFast

Se um bug só aparece em ReleaseFast, trate como sinal de comportamento indefinido, dado não inicializado, corrida, lifetime errado ou pressuposto quebrado. Reproduza em ReleaseSafe com asserts extras antes de tentar adivinhar pela performance.

Stack traces e erros explícitos

Zig costuma entregar stack traces legíveis quando o binário mantém símbolos e quando o erro passa por caminhos rastreáveis. Isso já resolve muitos problemas sem debugger interativo.

Exemplo mínimo:

const std = @import("std");

fn divide(a: i32, b: i32) i32 {
    return @divTrunc(a, b);
}

pub fn main() void {
    const value = divide(10, 0);
    std.debug.print("valor={d}\n", .{value});
}

Em Debug, o erro aponta para a linha relevante. Em projetos maiores, preserve nomes de funções descritivos e evite engolir erros com catch unreachable fora de limites muito controlados. Um try que propaga erro com contexto claro costuma ser mais útil que uma mensagem genérica no topo.

Quando capturar erro para logar, registre operação, entrada sanitizada, duração e causa. Para serviços HTTP, use o mesmo ID de requisição nos logs e nas métricas. Esse padrão conversa com o guia de observabilidade em Zig e evita que depuração local vire uma disciplina separada da produção.

GDB e LLDB para inspeção interativa

Zig gera DWARF, então GDB e LLDB funcionam bem para inspeção de frames, variáveis, ponteiros e chamadas C. O fluxo típico é:

zig build-exe src/main.zig -O Debug -femit-bin=zig-debug-demo

gdb ./zig-debug-demo
# ou
lldb ./zig-debug-demo

Com GDB:

break main
run
bt
frame 2
info locals
print minha_struct
next
step
continue

Com LLDB:

breakpoint set --name main
run
thread backtrace
frame variable
next
step
continue

Dicas práticas:

  • mantenha um caso de reprodução pequeno antes de abrir o debugger;
  • coloque breakpoints em fronteiras: parser, alocação, chamada externa, handler HTTP, função de serialização;
  • use watchpoints quando uma variável muda sem você saber onde;
  • confirme se o binário inspecionado é o mesmo que falhou;
  • ao depurar código com C, instale símbolos das bibliotecas quando possível.

Para aplicações servidoras, também vale iniciar o processo com configuração local mínima e chamar uma rota específica com curl. Isso reduz ruído e facilita repetir o mesmo passo após cada hipótese.

Allocators como ferramenta de depuração

Em Zig, allocator não é detalhe: é parte do contrato. Isso torna vazamentos e lifetimes mais visíveis.

Durante desenvolvimento, use GeneralPurposeAllocator e verifique deinit():

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const leaked = gpa.deinit();
        if (leaked == .leak) {
            std.debug.print("vazamento detectado\n", .{});
        }
    }

    const allocator = gpa.allocator();
    const buffer = try allocator.alloc(u8, 1024);
    defer allocator.free(buffer);

    @memset(buffer, 0);
}

Use ArenaAllocator para lifetimes de requisição, parsing ou jobs curtos. Isso simplifica limpeza quando todos os objetos morrem juntos. Mas não use arena como desculpa para esconder crescimento ilimitado: em serviço long-running, arena por requisição deve ser destruída ao fim da operação.

Checklist de memória:

  • quem aloca também documenta quem libera;
  • buffers de resposta têm limite explícito;
  • parsing de JSON define tamanho máximo;
  • cache tem política de expiração;
  • testes exercitam caminhos de erro, não só sucesso;
  • logs não imprimem segredos nem buffers enormes.

Valgrind, sanitizers e confirmação externa

O allocator de Zig pega muitos vazamentos em desenvolvimento, mas Valgrind ainda é útil para confirmar comportamento nativo, especialmente quando há interoperabilidade com C.

zig build -Doptimize=Debug
valgrind --leak-check=full --show-leak-kinds=all ./zig-out/bin/app

Quando o projeto usa bibliotecas C, rode um teste que passe pela fronteira C/Zig. Vazamento pode estar em uma função de inicialização, em callback, em buffer retornado por C ou em caminho de erro que pula limpeza.

Sanitizers também podem ajudar dependendo da versão do Zig, target e dependências. Se o build com sanitizer ficar instável ou não suportado para o target, não trate isso como bloqueio para investigar: volte para Debug, ReleaseSafe, Valgrind e teste de reprodução menor.

perf para CPU real no Linux

Quando a pergunta é CPU, perf costuma ser o melhor primeiro passo em Linux. Ele mede o binário real, com otimização real, no kernel real.

zig build -Doptimize=ReleaseFast
perf record -F 99 -g -- ./zig-out/bin/app --bench
perf report

Para flamegraph:

perf script > perf.out
# depois gere o flamegraph com a ferramenta que você usa no ambiente

O que procurar:

  • função aparecendo larga no topo sem motivo claro;
  • parser ou formatação consumindo mais CPU que a regra de negócio;
  • alocação em hot path;
  • syscalls frequentes demais;
  • lock ou espera que parece CPU por amostragem ruim;
  • diferença grande entre entrada pequena e entrada real.

Não otimize o primeiro frame chamativo sem confirmar a hipótese. Às vezes a função aparece porque tudo passa por ela; o problema real está no número de chamadas, não no custo de cada chamada. Adicione contadores ou métricas antes de reescrever arquitetura.

Tracy para timeline e zonas

Tracy é valioso quando você precisa enxergar tempo por zona, frame, thread ou operação. Ele é mais intrusivo que perf, mas muito bom para responder perguntas como: “qual etapa de uma requisição atrasou?”, “o parser está bloqueando?”, “o worker passa tempo esperando IO?”

Use Tracy quando:

  • o problema é latência, não apenas CPU total;
  • você precisa comparar fases de uma operação;
  • há múltiplas threads ou workers;
  • a sequência temporal importa.

Instrumente zonas pequenas e nomeadas. Evite marcar cada linha. O excesso de zona aumenta overhead e transforma a visualização em ruído. Bons nomes são de domínio: parse_config, fetch_api, render_template, write_cache, handle_request, flush_metrics.

Em produção, mantenha essa instrumentação desligada por padrão ou limitada a builds especiais. Para sinal contínuo, prefira logs estruturados, métricas e traces leves como discutido no guia de OpenTelemetry em Zig.

Core dumps e falhas que só aparecem fora da sua máquina

Alguns bugs só aparecem no host real. Nesses casos, configure core dump e preserve o binário com símbolos correspondente ao commit em produção.

Fluxo de investigação:

  1. registrar commit, versão do Zig, target, flags e ambiente;
  2. guardar binário e core dump;
  3. abrir com GDB ou LLDB;
  4. extrair backtrace, thread atual e variáveis locais;
  5. reproduzir localmente com a menor entrada possível;
  6. adicionar teste ou assert antes do fix.

Exemplo com GDB:

gdb ./zig-out/bin/app core.1234
(gdb) bt
(gdb) info threads
(gdb) thread apply all bt

Nunca dependa apenas de “reiniciou e parou de acontecer”. Se o processo caiu, transforme a queda em caso reproduzível, teste ou pelo menos alerta mais preciso.

Debug local e observabilidade de produção devem se encontrar

A fronteira entre debugging e observabilidade é prática. Debugger responde “o que acontece agora nesta execução?”. Observabilidade responde “o que aconteceu em muitas execuções, inclusive quando ninguém estava olhando?”.

Para serviços Zig, um conjunto mínimo saudável inclui:

  • logs estruturados com nível, operação e ID de correlação;
  • métricas de latência, taxa de erro, fila, memória e conexões;
  • health check separado de readiness;
  • build info exposto de forma segura;
  • limites claros para body, resposta, timeout e concorrência;
  • core dumps ou crash reports controlados;
  • alerta baseado em sintoma, não em ruído interno.

Esse conjunto reduz a necessidade de entrar no servidor para “dar uma olhada”. Quando um incidente acontece, você já sabe qual rota, qual versão, qual dependência e qual sintoma merecem depuração local.

Checklist antes de chamar de otimizado

Antes de declarar vitória em performance:

  • o teste usa dados parecidos com produção?
  • o modo de build é o modo que você pretende comparar?
  • há benchmark antes e depois?
  • perf confirma a hipótese?
  • alocação em hot path foi medida, não presumida?
  • a mudança mantém legibilidade e segurança?
  • o ganho compensa a complexidade?
  • zig build test continua passando?
  • logs, métricas e limites continuam intactos?

Zig facilita otimizações profundas, mas também facilita micro-otimizações desnecessárias. O melhor fluxo é medir, mudar pouco, medir de novo e registrar o motivo. Para um binário de produção, previsibilidade vale tanto quanto velocidade bruta.

Perguntas frequentes

Devo depurar sempre em Debug?

Comece em Debug, porque os checks e stack traces ajudam muito. Depois confirme em ReleaseSafe ou ReleaseFast se o bug depende de otimização. Problemas que só aparecem em ReleaseFast merecem suspeita extra de comportamento indefinido, dado não inicializado ou lifetime incorreto.

Valgrind substitui o GeneralPurposeAllocator?

Não. Use o GeneralPurposeAllocator durante desenvolvimento e testes porque ele integra naturalmente com Zig. Use Valgrind para confirmação externa, interoperabilidade com C e investigações em que você quer observar o processo nativo por fora.

perf ou Tracy?

Use perf para CPU real e amostragem no Linux. Use Tracy para timeline, zonas, threads e latência por etapa. Em muitos casos, os dois se complementam: perf aponta onde a CPU foi embora; Tracy mostra quando e em qual fase isso aconteceu.

Posso deixar instrumentação pesada em produção?

Evite. Logs, métricas e traces leves podem ficar sempre ligados com limites de cardinalidade e amostragem. Profiling pesado, Tracy detalhado e dumps amplos devem ser acionados com cuidado, em ambiente controlado ou build específico.

Como isso se conecta com observabilidade?

Depuração corrige um caso. Observabilidade ajuda a encontrar e priorizar os casos. Um serviço Zig bem instrumentado entrega logs, métricas e health checks suficientes para você reproduzir localmente o problema certo, em vez de adivinhar pelo sintoma errado.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.